typescript在redux-react项目中的应用

开头

这开头呢,我只想说一句 如何过好这一生。

关于React实际上早在很久很久之前写过一篇基于Dva方案的文章,如果不考虑typecript和版本的话其实一切用起来至今也倒尚好。

可能typecript自己至今尚未学好,关于react、redux、typescript三者的结合,一直碎碎念,打磨不好。

这其中,有redux写起来繁琐的缘故、有typescript不到家的缘故。

直到几个月前看到github上react-redux-typescript-boilerplate,终于觉得这是自己想要的方案,拖延好几个月,这里就简单整理一下,顺便把之前的Dva方案升级一下。

方案简析

Redux

这里redux,有很多概念,譬如Action/Reducer/Store/Middleware。在项目里面有对应的actions/reducers/store/middleware目录对应。这里不是redux的安利文所以不做特别说明。

关于方案的优势这里必须提一下其使用上的便利。一般情况下,我们使用redux时候dispatch一个状态都需要经过引用,然后在mapDispatchToProps里面注册,最后再this.props.dispatch({type: Action, {payload: {}}})进行状态触发。

在本方案中,redux和react的连接部分 代码是这样的:

1
2
3
4
5
6
7
8
9
10
@connect(
(state: RootState, ownProps): Pick<App.Props, 'todos' | 'filter'> => {
const hash = ownProps.location && ownProps.location.hash.replace('#', '');
const filter = FILTER_VALUES.find((value) => value === hash) || TodoModel.Filter.SHOW_ALL;
return { todos: state.todos, filter };
},
(dispatch: Dispatch): Pick<App.Props, 'actions'> => ({
actions: bindActionCreators(omit(TodoActions, 'Type'), dispatch)
})
)

在这个小结,这里仅仅注意到bindActionCreators这个API的便利性即可。

Typescript

谈到ts,这里感觉巧妙的地方有些多。

比如actions/todo.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createAction } from 'redux-actions';
import { TodoModel } from 'app/models';

export namespace TodoActions {
export enum Type {
ADD_TODO = 'ADD_TODO',
EDIT_TODO = 'EDIT_TODO',
DELETE_TODO = 'DELETE_TODO',
COMPLETE_TODO = 'COMPLETE_TODO',
COMPLETE_ALL = 'COMPLETE_ALL',
CLEAR_COMPLETED = 'CLEAR_COMPLETED'
}

export const addTodo = createAction<PartialPick<TodoModel, 'text'>>(Type.ADD_TODO);
export const editTodo = createAction<PartialPick<TodoModel, 'id'>>(Type.EDIT_TODO);
export const deleteTodo = createAction<TodoModel['id']>(Type.DELETE_TODO);
export const completeTodo = createAction<TodoModel['id']>(Type.COMPLETE_TODO);
export const completeAll = createAction(Type.COMPLETE_ALL);
export const clearCompleted = createAction(Type.CLEAR_COMPLETED);
}

export type TodoActions = Omit<typeof TodoActions, 'Type'>;

这里有一个namespace和一个type。其中type这里使用了Omit = Pick<T, Exclude<keyof T, K>>这个高级类型,将namespace中的enum Type进行了排除。它的作用是给Container组件设置props类型的接口。

而namespace里面除了addTodo这些的引用,因为可以访问到Type,还可以在Reducer里面获得引用。

然后我们看看namespace这里面的addTodo这些。

1
export const addTodo = createAction<PartialPick<TodoModel, "text">>(actionType: string): ActionFunction1<PartialPick<TodoModel, "text">, Action<PartialPick<TodoModel, "text">>>

这里PartialPick<TodoModel, "text">用来约束后面dispatch事件时候的传值。其中PartialPick是Partial<T> & Pick<T, K>组成的联合类型。这里的意义是将TodoModel里面除了‘text’之外的属性改成选填状态。

然后是App/index.js文件。这里摘录一些关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export namespace App {
export interface Props extends RouteComponentProps<void> {
todos: RootState.TodoState;
actions: TodoActions;
filter: TodoModel.Filter;
}
}
@connect(
(state: RootState, ownProps): Pick<App.Props, 'todos' | 'filter'> => {
const hash = ownProps.location && ownProps.location.hash.replace('#', '');
const filter = FILTER_VALUES.find((value) => value === hash) || TodoModel.Filter.SHOW_ALL;
return { todos: state.todos, filter };
},
(dispatch: Dispatch): Pick<App.Props, 'actions'> => ({
actions: bindActionCreators(omit(TodoActions, 'Type'), dispatch)
})
)
export class App extends React.Component<App.Props> {}

这其中,type TodoActions用在了interface Props中。

然后mapDispatchToProps函数中则是使用了bindActionCreators这个API并根据之前提到的Omit构建了一个函数排除‘Type’生成了我们常见的dispatch到props的注册。

小结

实际上在这个方案中,并没有太多特别的地方。只不过,在这里有几个地方处理得很好。

  • typescript的namespace 和 type用得巧妙,namespace内部的enum这个也用的很巧妙
  • 配合bindActionCreators方便了mapDispatchToProps函数的恼人操作

改造

Antd按需加载

官方推荐的按需加载用的是babel-import-plugin,不过这里因为用了Typescript,所以不推荐用它了,转而推荐其Ts版本的ts-import-plugin

另外,按需加载依赖less。所以也需要配置一下less-loader这些。

最后就是extract-text-webpack-plugin在webpack4这里无法使用,所以转而使用了mini-css-extract-plugin,这里已经自带,不过注意不要将其和style-loader一同使用,这是冲突操作。

首先是依赖:

1
npm i ts-import-plugin less less-loader postcss-less antd --save

其次修改webpack配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
...
{
test: /\.tsx?$/,
use: [
!isProduction && {
loader: 'babel-loader',
options: {
plugins: [
'react-hot-loader/babel'
]
}
},
{
loader: 'ts-loader',
options: {
transpileOnly: true,
getCustomTransformers: () => ({
before: [ tsImportPluginFactory({
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}) ]
}),
compilerOptions: {
module: 'es2015'
}
}
}
].filter(Boolean)
},
...
// Rules for Ant-Design
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // replaces extract text plugin in webpack 4
`css-loader`,
{loader: 'postcss-loader', options: postcssOptions},
{loader: 'less-loader', options: { javascriptEnabled: true }},
],
include: [/[\\/]node_modules[\\/].*antd/],
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // replaces extract text plugin in webpack 4
`css-loader?modules&importLoaders=1&localIdentName=[local]_[hash:base64:5]&-autoprefixer`,
{loader: 'postcss-loader', options: postcssOptions},
{loader: 'less-loader', options: { javascriptEnabled: true }},
],
include: [projectPath],
},

这里仅仅标出代码,详细可以直接看相关代码。

添加异步库

这里使用redux-sagas。详细直接看代码。不作详细解说。

API请求处理

这里根据之前代码简单封装whatwg-fetch。

不过这个请求库一般需要根据自己业务自行整体处理。这里聊作参考。

另外mock服务这里暂时不做了,如果考虑要做mock实质上还是推荐直接使用devServer.proxy来直接对请求进行反向代理,然后是如果远程rap方案的mock这里就不消说,如果是预备本地代理那就找个mockServer配合nodemon的watch和自动重启来做(不过mockServer也可能自带这个功能)。

代码生成

这块实质上是demo完成后补上的。

用意是虽然在Typescript体验上ok了,但是每次手动写那么多代码 建立那么多文件还是过于繁琐,于是参考别的项目使用plop做了若干配置。这里运行npm run g之后会有询问框,可以自行生成component && container代码,component支持es6和stateless模块。

这里代码生成这里最麻烦的是container这块的redux相关配置,除了script/generators里面的配置、模版外,需要注意不要删除各个文件里面的/* PREPEND IMPORT HERE */、/* PREPEND ATTR HERE */之类,这里是插入相关引用必须的插入点。

按需加载

临push时候想起还没这个生产级别的基础配置,这里简单配置了一下。主要是loadable-components && @babel/plugin-syntax-dynamic-import && @babel/react

这里集成到plop模版了,默认开启,使用npm run g创建container时候会默认开启这个功能。

代码

1
2
3
4
5
6
7
8
// 推荐 方便看master代码
git clone https://github.com/que01/react-redux-typescript-boilerplate
cd react-redux-typescript-boilerplate
git checkout -b demo
git branch --set-upstream-to=origin/demo demo
git pull
// 直接
git clone -b demo https://github.com/que01/react-redux-typescript-boilerplate