基于 umi3 + dva + react + antd-mobile 搭建项目。
功能
- [x] umi + dva + react hooks + antd-mobile
- [x] eslint + stylelint + prettierrc
- [x] sass/less
- [x] global 样式 + mixin 样式
- [x] px 自动转 vw
- [x] 资源上传七牛
- [x] 错误统一拦截
目录结构介绍
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
└── config/
├── config.js // umi 配置,同 .umirc.js,二选一
├── routers.js // 路由配置
└── src/
├── layouts/index.js // 全局布局
├── assets // 静态资源
├── components // 公共组件
├── models // 全局 models
├── services // services
├── styles // 样式文件
├── utils // 工具文件
└── pages/ // 页面目录
├── global.less // 自动引入的全局样式
├── app.js // 运行时配置文件
├── gulpfile.js // 静态资源上传七牛配置
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .editorconfig // 维护代码风格的配置文件
├── .eslintignore // eslint 忽略文件
├── .eslintrc.js // eslint 配置文件
├── .gitignore // git 要忽略的文件
├── .prettierignore // Prettier 要忽略的文件
├── .prettierrc.js // Prettier 代码格式化配置文件
├── .stylelintrc.js // css 代码审查的配置文件
├── jsconfig.jso // js 配置文件
├── package.json // npm 依赖记录文件
├── yarn.lock // yarn 版本锁定文件
├── eevee.config.js // eevee 项目配置
├── jenkins-ci // jenkins 打包上传 eevee 配置
└── resourceGenerator.js // 打包资源上传 eevee 配置
搭建步骤
参考: UmiJs官网、
JsConfig.js、
JestConfig.js、
puppeteer、
stylelint、
Prettier、
Editorconfig
1、使用 umi 创建项目
node版本>=10.13, 推荐nvm管理node版本,yarn管理npm依赖(使用国内源)
# 国内源
$ npm i yarn tyarn -g
# 后面文档里的 yarn 换成 tyarn
$ tyarn -v
$ 1.16.0
先找个地方建个项目目录并进入
$ mkdir myapp && cd myapp
创建项目并安装依赖
$ yarn create @umijs/umi-app
$ yarn
$ yarn start
此时,项目已经启动,通过 http://localhost:8000 访问
2、配置 Eslint 规则
umi 维护了一个 prettier,eslint,stylelint 的配置文件合集--umi-fabric
这里直接添加 .eslintrc.js、.prettierrc.js、.stylelintrc.js ,配置如下:
1、.eslintrc.js 配置
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
rules: {
'react/jsx-first-prop-new-line': 'error',
semi: ['error', 'never'],
indent: ['error', 2, { SwitchCase: 1 }],
'no-plusplus': 'off',
'react/sort-comp': 'off',
'no-unused-expressions': 'off',
},
}
2、.prettierrc.js 配置
const fabric = require('@umijs/fabric')
module.exports = {
...fabric.prettier,
semi: false,
}
3、.stylelintrc.js 配置
const fabric = require('@umijs/fabric')
module.exports = {
...fabric.stylelint,
}
你的 vscode 要安装这三个同名扩展插件,这时候分别去更改 js、less 文件,会发现已经有风格校验了,保存会自动修复。
3、样式配置
umi3 默认支持 less,如果需要 sass,需要安装 node-sass 依赖,推荐使用 less。
global.less 会默认引入,所以这里可以用来写一些全局样式,src 下建立 styles 目录,添加 base.less 和 mixins.less, base.less 可以作为导出各种变量和 mixin 给其他 less 文件使用的一个出口。
global.less 添加 css reset 和一些样式自定义,如下:
@import "~@/styles/base.less";
body, dl, dt, dd, ul, ol, li, pre, form, fieldset, input, p, blockquote, th, td {
margin:0;
padding:0;
font-weight:400;
font-family:Helvetica,Arial,sans-serif;
text-align:left;
background-color:#FFF;
}
html, body, #root {
height: 100%;
margin:0;
padding:0;
color: @text-color;
font-size: 28px;
}
p, h1, h2, h3, h4, h5, h6 {
margin:0;
padding:0;
}
fieldset, img {
border:0 none;
}
address, caption, em, strong, th, i {
font-weight:400;
font-style:normal;
}
ol, ul {
list-style-position:outside;
list-style-type:none;
}
根据项目添加...
4、px 转 vw
之所以用 vw 不用当前非常通用的 rem, 是因为 rem 方案有几个缺点:
- rem 不容易理解,字体单位却用来处理长度问题
- 需要 js 设置根元素 font-size,样式和行为耦合不是很好
- rem 设置的 css 长度,经过四合五入,存在最后 1 像素问题
这里借助 postcss-px-to-viewport 帮我们自动转 px 为 vw ,先安装依赖:
yarn add postcss-px-to-viewport
然后在 umirc.js 中添加配置如下:
export default defineConfig({
extraPostCSSPlugins: [
postcssPx2vw({
viewportWidth: 750,
unitPrecision: 5,
viewportUnit: 'vw',
minPixelValue: 1,
}),
],
});
这样你发现 css文件中的 px 已经自动转为 vw 了。
5、静态资源上传 CDN
有些资源较大可能要上传到 cdn 来使用,我这里使用 gulp 上传资源到七牛来使用,没有图床的同学可以参考我的另一篇文章:图床搭建
安装依赖
yarn add gulp gulp-qiniu --dev
添加配置
根目录新建 qiniuAssets 目录和 gulpfile.js ,配置如下:
/* eslint-disable import/no-extraneous-dependencies */
const gulp = require('gulp')
const path = require('path')
const qiniu = require('gulp-qiniu')
// file upload tasks
const imageSrc = path.resolve(__dirname, 'qiniuAssets/*.*')
gulp.task('qiniu', (done) => {
gulp
.src(imageSrc)
.pipe(
qiniu(
{
accessKey: '你的七牛 accessKey',
secretKey: '你的七牛 secretKey',
bucket: 'maihaoche',
private: false,
},
{
dir: 'kunkka/assets/',
version: false,
},
),
)
.on('finish', done)
})
// combine tasks
gulp.task('upload',gulp.series(['qiniu'], (done) => {
done()
}))
module.exports = gulp
添加命令行
在 package.json 中 scripts 中添加一行如下:
"upload": "gulp qiniu",
将要上传的资源放到 qiniuAssets 目录中,然后执行命令:
yarn upload
就可以使用上传后的资源url,如:https://img.XXX.com/kunkka/as...
6、请求异常统一拦截
大多数接口请求,与后台约定统一响应数据结构,统一错误处理,可以简化业务层处理逻辑,只处理正常流即可。
添加 request 配置
此处使用 umi-request、antd-mobile 处理请求,首先添加依赖:
yarn add umi-request antd-mobile
然后添加 utils/request.js, 内容如下:
import { extend } from 'umi-request'
import { Toast } from 'antd-mobile'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
}
function errorHandler(error) {
// 请求已发送但服务端返回状态码非 2xx 的响应
if (error.response) {
const { status, statusText } = error.response
const errortext = codeMessage[status] || statusText
Toast.error(errortext, 2)
// 请求初始化时出错或者没有响应返回的异常
} else {
const msg = error.message || error.errDesc || '系统异常'
Toast.info(msg, 2)
}
throw error
}
// umi request 实例
const request = extend({
errorHandler,
})
// 响应拦截器
request.interceptors.response.use(async (res, req) => {
const { resultCode, success, message, errDesc } = await res.clone().json()
const msg = message || errDesc || '系统错误'
const { noThrow } = req || {}
if (errDesc === '用户不存在' || msg?.includes('登陆失效') || msg?.includes('通过session无法找到用户')) {
Toast.info(msg)
window.location.href = '/login'
const err = new Error(msg)
throw err
}
// 业务上错误的简单处理,适用绝大多数接口,可以在 req 的 config 中传递 noThrow 不走这一层拦截
if (!(resultCode === '1' || success === true) && !noThrow) {
Toast.info(msg)
const err = new Error(msg)
throw err
}
return res
})
export default request
大部分业务场景都可以这样过滤一层,然后业务中支处理正常流即可。
异常流的处理方式
这里 dva 提供了一个 onError 的 hook 来全局错误处理,在 src/app.js 中添加 dva 配置:
import { login } from '@/services/index'
import { getQueryString } from './utils/utils'
// 配置 dva 创建时的参数 , 参见:https://dvajs.com/api/#app-dva-opts,可以捕获 effects 和 subscriptions 中的错误
export const dva = {
config: {
onError(err) {
err.preventDefault() // 阻止 error 的继续抛出?目前看是这样,不知是不是 dva 封装了 err 的方法
},
},
}
export async function render(oldRender) {
const sessionId = getQueryString('sessionId')
await login({ sessionId })
oldRender()
}
如果有特殊的接口需要单独处理异常,可以在 models 通过 try catch 捕获错误继续处理,如下:
effects: {
*getUserInfo({ payload }, { call, put }) {
try {
const { data } = yield call(getUserInfo, payload)
yield put({
type:'setData',
payload: { customerInfo: data}
})
} catch (error) {
console.log('获取失败', error)
}
},
}
或者不通过 models 调用的话,可以在 promise catch 中处理,如下:
// 获取客户信息
getUserInfo().then((userInfo) => {
console.log('获取用户信息成功1', userInfo)
}).catch((err) => {
console.log('获取用户信息失败1', err)
})
还有最坏的情况,后台返回的响应根本没有约定好响应的结构,且拒绝修改,那只能一边骂骂咧咧一边修改代码了,这个接口单独在业务中处理!如下传个标志给 request:
import request from '@/utils/request'
// 不走统一拦截的接口,传个 'noThrow' 标志到 request 拦截层
export const login = (data) => request.post('/api/login.json', { data, noThrow: true })
// 统一异常处理的接口
export const getCustomerInfo = (data) => request.post('/api/companyInfo.json', { data })
request.js 根据这个标志将这种接口的响应直接返回到前台,自己在业务中单独处理这种接口的异常情况。
总结:
- 大部分接口统一拦截请求异常,只在业务中处理正常流
- 接口在 models 中通过 try catch 单独处理异常后的操作
- 不通过 models 的可以直接在 promise catch中处理异常
- 需要单独处理的可以传个标志给request,不走响应拦截的条件
如此这般,就可以完整建立一个单页面移动端项目了。