基于 Umi 的最佳实践

  • 本项目使用 yarn 进行包管理
  • 全局安装的命令为 jsdoc

本项目是基于 Web 开发最佳实践 的扩展

目录结构以及解释

目录结构

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
├── build           # 打包目录
├── config # 配置目录
│ ├── config.dev.js
│ ├── config.js
│ └── ...
├── docs # 文档目录
│ ├── html
│ └── jsdoc.json
├── jsconfig.json # IDE 的代码提示 @/
├── package.json # 版本以及依赖文件
├── public # 附加到 public 目录 的文件
│ └── robots.txt
└── src
├── assets # 资源文件
│ ├── images # 图片文件
│ └── less # 样式文件
│ ├── style.less # 样式主文件
│ └── ...
├── build.md # 版本定义说明
├── layout # 布局文件夹
│ └── index.js
├── models # 模型文件, 用户全局的数据管理
│ └── poppy.js
├── pages # 所有页面
├── services # api 服务定义页面
│ └── poppy.js
└── utils # util 包, 多个项目内容应该一致
├── conf.js # 配置
├── request.js # 基于 axios 的请求
├── routes.js # 路由定义
├── ui # UI 部件
│ └── IconFont.js # IconFont 字体
└── util.js # 辅助功能

配置文件

config.test.js

1
2
3
4
5
6
7
8
9
10
import { defineConfig } from "umi";

export default defineConfig({
// 打包路径
outputPath: "build/proj-test",
define: {
// 定义的URL被覆盖
"process.env.API_URL": "https://t.proj.domain.com",
},
});

配置中使用的变量说明, 其他变量间 umi 官方文档
config.js

1
2
3
4
5
6
7
8
9
10
11
export default defineConfig({
// ...
define: {
// 版本号, 取 package.json 中的版本
"process.env.VERSION": version,
// 接口请求主地址, 尾部不包含斜线
"process.env.API_URL": process.env.API_URL || "",
// 打包环境, 用户 sentry 提交错误对应正确的环境
"process.env.UMI_ENV": process.env.UMI_ENV || "",
},
});

打包以及运行说明

命令放置在 package.json 文件中

1
2
3
4
# 运行本地环境(参考package.json启动其他环境)
$ yarn start:test
# 构建测试环境(参考package.json构建其他环境)
$ yarn build:dev
  • UMI_ENV : 代表的是运行环境, 可以在配置中取到相应的文件并进行相应的处理
1
2
3
4
5
6
7
8
9
10
 {
"..."
"scripts": {
"...",
"start:test": "cross-env UMI_ENV=test yarn umi-dev",
"build:test": "cross-env UMI_ENV=test umi build",
"..."
}
}

打包出来的路径为 build/proj-test

文档

文档使用的 jsdoc. 这里的配置文件使用的是 ink-docstrap  模板, 配置如下
使用如下命令生成

1
$ jsdoc -c docs/jsdoc.json

jsdoc.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc", "closure"]
},
"source": {
"include": ["./src/", "README.md"],
"exclude": [],
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": [],
"opts": {
"template": "./node_modules/ink-docstrap/template",
"encoding": "utf8",
"destination": "./docs/html",
"recurse": true
}
}

版本定义

项目版本放在 version 中, 每次上线或者切换分支的时候都需要更新, 便于后端进行统计
package.json

1
2
3
4
5
6
{
"name": "proj-mobile",
"version": "1.1.0",
"..."
}

服务的定义以及使用

使用 api 作为前缀, 便于区分
src/services/poppy.js

1
2
3
4
5
6
7
8
9
10
/**
* 国别码
*/
export function apiPamLogin(params) {
return request({
url: "/api_v1/system/pam/login",
method: "post",
data: params,
});
}

使用

1
2
3
4
5
6
apiPamLogin({ passport: country + "-" + mobile, captcha: captcha }).then((resp, status, message, data) => {
// resp : 包含完整的请求结果
// status : 错误码信息
// data : data 中的数据
// message : 返回的信息
});

模型的定义以及说明

模型定义

src/models/poppy.js

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 { Toast } from "antd-mobile";
import { apiAreaCountry } from "@/services/poppy";
import { storageKey } from "@/utils/conf";
import { sessionStore } from "@/utils/util";

const poppyModel = {
namespace: "poppy",
state: {
// 1. 初始数据值
countryCode: [],
},

effects: {
// 2. 定义的请求
*reqAreaCountry({ payload: params }, { call, put }) {
// 3. 获取数据
let storeData = sessionStore(storageKey.PY_AREA_COUNTRY);
if (!storeData) {
// apiAreaCountry 在 模型中的使用, 参数可以在第二个参数传入
const { data, success, message: msg } = yield call(apiAreaCountry, {});
if (success && data) {
sessionStore(storageKey.PY_AREA_COUNTRY, data);
storeData = data;
} else {
Toast.fail(msg);
}
}
// 4. 更新数据
yield put({
type: "UPDATE_COUNTRY_CODE",
payload: {
countryCode: storeData,
},
});
},
},

reducers: {
// 5. 将数据写入 redux store 中
UPDATE_COUNTRY_CODE: (state, { payload }) => {
return {
...state,
...{
countryCode: payload.countryCode,
},
};
},
},
};
export default poppyModel;

数据的调用

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import React, { Component } from 'react';
import $ from 'jquery';
import { CloseCircleOutlined } from '@ant-design/icons'
import BScroll from 'better-scroll';
import { get } from 'lodash-es';
import { connect } from 'umi';

// 1. 连接 store
@connect(({ poppy }) => {
return {
countryCode: poppy.countryCode
}
})
class CountryCode extends Component
{
scroll = '';
touch = {};

constructor(props) {
super(props);
this.state = {
list: {},
alphaList: []
}
}

componentDidMount() {
// 2. 触发数据更新
// 这里的 dispatch 必须使用 store 之后才可以触发, 毕竟读取的是store 里边的数据
this.props.dispatch({
type: 'poppy/reqAreaCountry'
})
}

// 3. 接收数据更新
static getDerivedStateFromProps(nextProps, prevState) {

const countryCode = get(nextProps, 'countryCode');
const alphaList = get(prevState, 'alphaList');

if (countryCode && alphaList.length === 0) {
return CountryCode.initList(countryCode)
}

return null;
}

static initList(countryCode) {
let alpha_li = [];
let list_li = {};
countryCode.length > 0 && countryCode.forEach((item) => {
let key = item['py'];
alpha_li.push(key);
list_li[key] = list_li[key] ? list_li[key].concat(item) : [item];
})
return {
alphaList: [...new Set(alpha_li)],
list: list_li
}
}


render() {
const { list, alphaList } = this.state;
return (
// 数据渲染
);
}
}

export default CountryCode;

React 常见问题

1. Uncaught Error: Invariant failed

Uncaught Error: Invariant failed , You should not use <Route> outside a <Router>
参考地址 : https://www.freeformatter.com/url-parser-query-string-splitter.html

出现原因: 系统中存在两个实例, 例如

1
2
3
4
5
import { Route } from "react-router";
import { Switch } from "react-router-dom";

// 应该修改为
import { Switch, Route } from "react-router-dom";

2. 加载自定义 Url

将加载的 URL 放到 div 中, 从而实现远程地址的加载来加载变量

1
2
3
4
5
6
7
8
9
10
11
import postscribe from "postscribe";

let script = serverUrl + "/" + apiUrl.pamWechatConfig + "?url=" + window.location.href + "&type=base";
postscribe("#J_wxAuth", '<script src="' + script + '"></script>', {
error: (e) => {
console.log(e);
},
done: (e) => {
wx.config(window.wxAuthConfig);
},
});

3. 关掉线上 map 生成

参考文章 : 什么时候 create-react-app 会混淆或缩小代码?

如果有 map 生成, 则许多源码对外来讲便是可视化的. 例如之前的一个代练项目

项目代码一览无余, 所以对于项目上线打包的时候必须要关闭源码 map

对于 cra 项目, 使用了 react-app-rewired, 则可以在 config-overrides.js 中增加如下相关配置, 来关闭 sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const {override, addLessLoader, fixBabelImports, addWebpackAlias} = require('customize-cra');
const alias = require('./alias');

+ process.env.GENERATE_SOURCEMAP = "false";

function myOverrides(config) {
// do stuff to config
return config
}

module.exports = override(
myOverrides,
addLessLoader({
javascriptEnabled : true
}),
fixBabelImports('import', {
libraryName : 'antd-mobile',
style : true
}
),
addWebpackAlias(alias.resolve.alias)
)

关掉 map 之后则看起来放心多了

4. uncaught at check call: argument [object Promise] is not a function

主要原因在 yield call(addGroup(payload)); 的使用姿势问题,对于需要传递参数的去哪个,不能直接这么干,应该改为

1
yield call(addGroup, payload);