快速上手

https://github.com/stkevintan/ServiceX

ServiceX是一个Typescript Project Only的基于React hooks api之上的轻量级状态管理工具,主要的特点和解决的痛点有:

  1. 灵活多store:可以自由粒度的定义store,避免了redux在大项目中单一store越来越庞大的问题,并且提供一套强大的依赖注入功能来管理store

  2. 严格ts类型:项目本身有严格ts编写,同时对外接口处处都有TS类型推断,再也不用担心redux中action type拼错或者payload类型不兼容等隐形bug,同时更容易进行代码定位。

  3. 强大的rxjs集成:所有副作用都是通过rxjs进行描述和管理,与redux-saga相比,rxjs更通用,更强大,ts类型提示和约束做的更好。

安装

npm i service-x immer inversify reflect-metadata rxjs

使用

开启ts中的编译flag:

编辑tsconfig.json中的complerOptions:

{
  "compilerOptions": {
    "strict": true, // 严格模式,建议开启
    "emitDecoratorMetadata": true, // 开启metadata支持
    "experimentalDecorators": true,  // 开启decorator支持
    //...
  }
}

experimentalDecorators虽然是实验性质的,但是这个flag已经稳定好几个版本了,大名鼎鼎的Angular也使用到了这个特性,所以不必担心之后的兼容问题。

第一个Service

service代表了一个store,其中定义了store的state以及与redux概念相似的reducer, effect等action:

// entry-service.ts
import {
  Injectable,
  Service,
  Reducer,
  ImmerReducer,
  Effect,
  EffectAction,
  DefineAction
} from "service-x";
import { Entry, fetchEntries } from "./fetch-entries";
import { Observable, merge } from "rxjs";
import {
  map,
  distinctUntilChanged,
  combineLatest,
  switchMap,
  debounceTime
} from "rxjs/operators";

interface State {
  entries: Entry[];
  keyword?: string;
}

@Injectable()
export class EntryService extends Service<State> {
  defaultState: State = {
    entries: []
  };
  // trigger loadEntries reload
  @DefineAction()
  reload$!: Observable<void>;

  @ImmerReducer()
  setKeyword(state: State, keyword: string) {
    state.keyword = keyword;
  }

  @ImmerReducer()
  setEntries(state: State, entries: Entry[]) {
    state.entries = entries;
  }

  @Reducer()
  reset(): State {
    return this.defaultState;
  }

  @Effect()
  loadEntries(
    trigger$: Observable<void>,
    state$: Observable<State>
  ): Observable<EffectAction> {
    const keyword$ = state$.pipe(
      map(state => state.keyword),
      distinctUntilChanged(),
      // debounce the input
      debounceTime(500)
    );

    return merge(trigger$, this.reload$).pipe(
      combineLatest(keyword$, (_, keyword) => keyword),
      switchMap(keyword => fetchEntries(keyword)),
      map(entries => this.actions().setEntries(entries))
    );
  }
}

与React结合

import React from "react";
import ReactDOM from "react-dom";
import { useService } from "service-x";
import { EntryService } from "./entry-service";
import "./index.less";

const App = () => {
  const [state, actions] = useService(EntryService, { resetOnUnmount: true });
  React.useEffect(() => {
    // load the entries on component mounted
    actions.loadEntries();
  }, [actions]);

  return (
    <>
      <input
        className="search"
        value={state.keyword}
        onChange={e => actions.setKeyword(e.target.value)}
        placeholder="Enter keywords to search"
      />
      {state.entries.length ? (
        <ul className="entries">
          {state.entries.map(entry => (
            <li className="entry" key={entry.name}>
              <span>{entry.name}</span>
              <small>{entry.age}</small>
            </li>
          ))}
        </ul>
      ) : (
        <p className="empty">Nothing found</p>
      )}
      <button onClick={() => actions.reload$()}>刷新</button>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));

一个简单的人名搜索应用就OK啦,这里有一个更加高级的在线示例:

https://codesandbox.io/s/service-x-demo-9dqw6

Last updated