React2021最佳实践

创建项目

npx create-react-app ** --template typescript,其中**是项目名字,如果在当前空文件夹下面创建,直接输入一个点就可以了。
如果出现安装依赖失败,仔细阅读提示,很有可能是使用的 node 版本不对,其中有一个包要求指定版本或者大约 15 版本的 node,这个要注意。
如果运行时依赖有问题,很可能是因为上一步中断导致的问题,删除全部文件重新执行创建命令。

项目配置

  1. baseUrl根路径设置。打开 tsconfig.json文件,在compilerOptions对象当中添加baseUrl: './src'即可设置文件根绝对路径。

  2. 添加项目级的 prettier。文档
    yarn add --dev --exact prettier
    echo {}> .prettierrc.json
    创建.prettierignore文件然后写入
    js build coverage
    添加 commit 钩子自动格式化代码 npx mrm lint-staged
    应该在 package.json 里面自动插入了一条lint-staged命令,查看一下有没有 hook 命令,没有的话手动加一下
    js "husky": { "hooks": { "pre-commit": "lint-staged" } },

  3. 添加 Eslint。yarn add eslint-config-prettier -D,在 package 里面 eslintConfig->extends 里面添加一条 prettier。这条的意思是 prettier 格式化和 eslint 时有冲突,这里我们用 prettier 的部分规则覆盖了 eslint 的部分规则。注意,部分情况下可能出现 prettier 已经解决了 eslint 的冲突,但是格式化的情况下没有自动修复,你需要右键 格式化代码(vscode),给文件(ts) 配置默认的格式化程序,选择 prettier 就可以了。

  4. 添加commitlint,用于 git 提交检查。npm install --save-dev @commitlint/{config-conventional,cli}先装两个插件,echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js执行创建一个文件。没有husky的话,先装一个 husky

    1
    2
    3
    4
    5
    6
    "husky": {
    "hooks": {
    "pre-commit": "lint-staged",
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
    },

    加一个 hook 命令,然后提交的时候就按照规范提交,例如feat: some msg注意feat: 是固定的,别忘了空格不然也不通过。文档
    npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'执行这个命令,加到 hook 里面才行

  5. 添加json-server用于 mock 数据。npm install -g json-server,项目里面创建一个 db.json 用于存储数据,然后json-server --watch db.json启动一个服务器。此 mock 方式主要是简单快捷,并且严格遵循 restful api 方式,前后端数据同步。文档

跨域处理

之前写了个命令启动 json-server 提供 mock 数据"server": "json-server --watch ./__json_server_mock__/db.json --port=3002",但是由于接口不同调用会跨域,可以在 package.json 里面添加一行"proxy": "http://127.0.0.1:3002"用以消除跨域。

Hooks

大部分情况都可以看文档。熟练使用自定义 hook

css in js

安装一个 vscode 插件
vscode-styled-components
ws 插件
Styled components & Styled JSX
安装 emotion

安装 Antd 和配置主题

Antd

  1. yarn add antd

  2. 在 index.tsx 文件引入 less 文件 import 'antd/dist/antd.less'

  3. yarn add @craco/craco 安装 craco yarn add craco-less

  4. 新建根目录 craco.config.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const CracoLessPlugin = require('craco-less')

    module.exports = {
    plugins: [
    {
    plugin: CracoLessPlugin,
    options: {
    lessLoaderOptions: {
    lessOptions: {
    modifyVars: { '@primary-color': '#1DA57A' },
    javascriptEnabled: true,
    },
    },
    },
    },
    ],
    }
  5. 更改完配置之后重新启动一下

自定义配置

React代码分割

使用之前:

1
import OtherComponent from './OtherComponent';

使用之后:

1
const OtherComponent = React.lazy(() => import('./OtherComponent'));

文档

高花费组件可以使用React.memo 进行处理,与其props无关的状态改变不影响此组件的渲染

也可以使用 Profiler Api组件进行性能追踪,同样的因为有开销不建议在生产环境中使用

React测试

除了默认装有的测试工具还需要安装一个 yarn add @testing-library/react-hooks msw -D

Jest测试文件示例:

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
import { setupServer } from "msw/node";
import { rest } from "msw";
import { http } from "utils/http";

const apiUrl = process.env.REACT_APP_API_URL;

const server = setupServer();

// jest 是对react最友好的一个测试库
// beforeAll 代表执行所有的测试之前,先来执行一下回调函数
beforeAll(() => server.listen());

// 每一个测试跑完以后,都重置mock路由
afterEach(() => server.resetHandlers());

// 所有的测试跑完后,关闭mock路由
afterAll(() => server.close());

test("http方法发送异步请求", async () => {
const endpoint = "test-endpoint";
const mockResult = { mockValue: "mock" };

server.use(
rest.get(`${apiUrl}/${endpoint}`, (req, res, ctx) =>
res(ctx.json(mockResult))
)
);

const result = await http(endpoint);
expect(result).toEqual(mockResult);
});

test("http请求时会在header里带上token", async () => {
const token = "FAKE_TOKEN";
const endpoint = "test-endpoint";
const mockResult = { mockValue: "mock" };

let request: any;

server.use(
rest.get(`${apiUrl}/${endpoint}`, async (req, res, ctx) => {
request = req;
return res(ctx.json(mockResult));
})
);

await http(endpoint, { token });
expect(request.headers.get("Authorization")).toBe(`Bearer ${token}`);
});

有个 npm 包可以自定义配置 customize-cra

React 路由

首先安装 6 版本及以上的 react-router yarn add react-router@6 react-router-dom@6
之后在根组件内部

1
2
import { BrowserRouter as Router, Link } from 'react-router-dom'
import { Routes, Route } from 'react-router'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<div className='App'>
<Router>
<Link to={'/r1'}>to R1</Link>
<Link to={'/r2'}>to R2</Link>
<Routes>
<Route path={'/r1'} element={<R1 />} />
<Route path={'/r2'} element={<R2 />} />
</Routes>
</Router>
</div>
)
}

之后参考文档即可完成开发 React-router

Redux

redux-thunk

这个中间件的作用就是把输入的函数执行一遍,起到异步 dispath 和普通的 dispatch 一样的写法。

redux-toolkit

这里使用了这个库,这个库相当于 redux 的结合写法,合并了一些中间件然后对写法做了更改。
首先进行安装:yarn add react-redux @reduxjs/toolkit

创建 store.ts 文件

1
2
3
4
5
6
7
8
9
10
import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
reducer: {},
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

根组件传入 store

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

对应的数据切片创建对应的文件

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
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
value: number
}

const initialState: CounterState = {
value: 0,
}

export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

在 store 文件收集并注入 reducers

1
2
3
4
5
6
7
8
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
reducer: {
counter: counterReducer,
},
})

使用方式

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
import React from 'react'
import { RootState } from '../../app/store'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch()

return (
<div>
<div>
<button
aria-label='Increment value'
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label='Decrement value'
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
)
}
使用 react-query 来进行处理服务器返回的缓存 或者 swr
Author: XavierShi
Link: https://blog.xaviershi.com/2021/08/28/React2021最佳实践/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.