# react-pc-template
**Repository Path**: carry_hu/react-pc-template
## Basic Information
- **Project Name**: react-pc-template
- **Description**: 基于react的Pc开发模板
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2021-09-09
- **Last Updated**: 2025-01-31
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# H5项目-基于React+antd+Webpack搭建PC端项目
对于PC端项目,使用[React](https://zh-hans.reactjs.org/)+[Ant Design of React](https://3x.ant.design/docs/react/introduce-cn)+[Webpack](https://www.webpackjs.com/)搭建基础框架:
1. 初始化项目。
2. 配置Webpack、Ant Design of Vue、Mobx、React-Router。
## 设计思想
### 1 核心架构
[React](https://doc.react-china.org) 是用于构建用户界面的 JavaScript 库。**当前版本:16.13.1。**
[Webpack](https://www.webpackjs.com/)是最流行的打包工具。**当前版本:4.44.2。**
[Ant Design of React](https://3x.ant.design/docs/react/introduce-cn)是React中最流行的UI框架。**当前版本:3.26.18。**
[Mobx](https://cn.mobx.js.org)是React中最流行的轻量级状态管理框架。**当前版本:4.15.7。**
[React-Router](https://reactrouter.com/web)是React官方的路由框架。**当前版本:5.2.0。**
**注意:因为需要兼容IE9,所以antd使用3.X,Mobx使用4.X。**
### 2 目录结构
```
|app -- 开发目录
├── config -- 项目的公共配置
└── config.js -- 公共配置入口
└── configData.js -- 公共配置:常量数据
└── configTheme.js -- antd的定制主题
├── mock -- 网络请求mock数据
├── dist -- 打包后的文件夹
├── node_modules -- 安装的依赖库
├── public -- 外部依赖js、css、icon、font等资源,会复制到输出目录
├── src -- 项目源码
└── assets -- 图片资源
└── components -- 公共组件
└── constants -- 公用常量配置
└── layouts -- 布局,包括基础布局、菜单布局、权限管理
└── locales -- 国际化相关资源
└── models -- 状态管理(应用的数据)
└── pages -- 路由页面
...
└── Home.js -- 目录页面
└── router -- 路由配置
└── services -- 网络请求
└── utils -- 工具目录
└── App.js -- 顶层组件
└── App.scss -- 顶层组件样式
└── index.js -- 项目入口
├── .eslintrc -- eslint规则配置
├── .gitignore -- git忽略规则
├── .prettierrc.js -- prettierrc规则配置
├── .babel.config.js -- Babel规则配置
├── index.html -- Html模板
├── readme.md -- 项目使用基础文档
├── package.json -- 项目配置和依赖
├── webpack.base.js -- webpack的公共配置
├── webpack.dev.js -- webpack的开发配置
├── webpack.prod.js -- webpack的生产配置
```
### 3 兼容性
| IE / Edge | Firefox | Chrome | Safari | Opera | Electron |
| --------------------- | --------------- | --------------- | --------------- | --------------- | --------------- |
| IE9, IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 快速开始
### 1 创建项目
第一步,新建目录:demo
```
mkdir demo
cd demo
npm init //初始化package.json
```
第二步,新建html文件:index.html
```
<%= htmlWebpackPlugin.options.title %>
```
第三步,新建顶层组件:src/App.js
```
import React from 'react';
import './App.scss';
/**
* 顶层组件
*/
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return 第一个React项目!
;
}
}
```
第四步,新建顶层组件的样式文件:src/App.scss
```
html,
body,
#root {
height: 100%;
}
```
第五步,新建项目入口:src/index.js
```
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
/**
* 项目入口:用html的root元素渲染React组件
*/
ReactDOM.render(, document.getElementById('root'));
```
### 2 创建公共配置
实际项目中,host、publicKey等由打包环境决定的常量数据,都是保存在公共配置文件中,然后打包时,根据打包环境获取对应配置信息,然后保存在全局变量中。这样,发送网络请求时,直接从全局变量取host等,而不需要再判断打包环境。
创建项目公共配置文件:config/config.js和config/configData.js
config/config.js:
```
//配置项常量
const configData = require('./configData');
/**
* 定义项目的公共参数
* 通过webpack.dev.config.js配置的window._ENV_调用,比如:window._ENV_.site。
* @param env process.env(Node的环境变量),可以通过webpack赋值。注意:process.env只在打包、启动服务时有效,在代码运行时无效。
*/
module.exports = function (env) {
let mode = env.api; //环境模式
const config = {
mode,
title: 'ReactPC', //项目名称
fileName: 'dist', //打包后的文件名
publicParams: {clientId: 'P_AIAS_ROS'} //公共参数
};
//开发、测试环境
if (mode === configData.modeDev || mode === configData.modeStg) {
config.host = configData.hostStg;
config.secret = configData.secretStg;
config.publicKey = configData.publicKeyStg;
//生产环境
} else if (mode === configData.modeProd) {
config.host = configData.hostProd;
config.secret = configData.secretProd;
config.publicKey = configData.publicKeyProd;
}
return config;
};
```
config/configData.js:
```
/**
* 配置项常量
*/
module.exports = {
/**
* 环境模式:
* mock是mock环境(前端为本地环境,mock后台数据)
* dev是开发环境(前端为本地环境,后台为测试环境)
* stg是测试环境(前端为测试环境,后台为测试环境)
* prod是生产环境(前端为生产环境,后台为生产环境)
*/
modeMock: 'mock',
modeDev: 'dev',
modeStg: 'stg',
modeProd: 'prod',
//域名:测试环境。
hostStg: 'http://wthrcdn.etouch.cn',
hostProd: 'http://wthrcdn.etouch.cn',
//公钥:测试环境
publicKeyStg:'XXX',
publicKeyProd:'XXX',
//私密信息:测试环境,用于防止别人冒充签名
secretStg: 'XXX',
secretProd: 'XXX'
};
```
## 配置打包
打包使用Webpack,官方文档:https://www.webpackjs.com/,**当前版本:4.44.2。**
### 1 安装依赖
```
npm i react react-dom -S
//用于打包和启动本地服务
npm i webpack webpack-cli webpack-dev-server -D
//用于合并webpack的配置文件
npm i webpack-merge -D
//用于打包时加载html文件
npm i html-webpack-plugin -D
//用于打包时清空文件、拷贝静态文件
npm i clean-webpack-plugin copy-webpack-plugin -D
//用于分析打包后的文件大小
npm i webpack-bundle-analyzer -D
//用于打包时加载css文件
npm i style-loader css-loader -D
//用于打包时加载scss文件
npm i sass sass-loader -D
用于打包时处理css文件:自动补全webkit前缀等
npm i postcss-cssnext postcss-flexbugs-fixes postcss-loader -D
//用于打包时加载图片。
npm i file-loader url-loader -D
//用于将css打包到html外,提高加载速度
npm i mini-css-extract-plugin -D
//用于支持IE9等老式浏览器使用新的JS语法(比如箭头函数)
npm i @babel/core @babel/preset-env @babel/preset-react @babel/plugin-proposal-decorators babel-plugin-transform-class-properties babel-loader -D
//用于支持IE9等老式浏览器使用新的API(比如Map对象)
npm i core-js -S
```
### 2 打包命令
首先,在package.json中配置打包命令:
```
{
"scripts": {
"start": "webpack-dev-server --env.api dev --config webpack.dev.js",//启动开发环境的本地服务
"mock": "webpack-dev-server --env.api mock --config webpack.dev.js",//启动mock环境的本地服务
"build": "webpack --progress --profile --env.api stg --config webpack.prod.js",//打测试包
"prod": "webpack --progress --profile --env.api prod --config webpack.prod.js"//打生产包
}
}
```
然后,解析环境变量:
```
let api=process.env.api;//process是node的全局变量
```
然后,根据api变量从config目录获取对应的项目配置
### 3 配置Webpack
#### 3.1 webpack.config.js
Webpack基础配置文件:webpack.config.js
```
const webpack = require('webpack');
const path = (name) => require('path').resolve(__dirname, name); //获取绝对路径
/**
* webpack的基础配置文件
* 中文文档:https://www.webpackjs.com/concepts/
* @param env Node的环境变量:process.env。注意:webpack打包是在node环境中运行的。
*/
module.exports = (env) => {
const publicParam = require('./config/config.js')(env); //项目的公共配置
//入口文件
let entry = './src/index.js';
//出口文件
let output = {
path: path(publicParam.fileName), //出口文件的路径
filename: 'js/[name].[hash:8].js' //出口文件的文件名。注意:因为浏览器会缓存js文件,所以使用hash名来自动更新缓存。
};
//指定需要解析的模块
let resolve = {
modules: [path('src'), 'node_modules'], //需要解析的模块
alias: {'@': path('src')} //指定别名
};
//对模块的源代码进行转换。注意:webpack自身只理解JavaScript。
let module = {
//解析规则
rules: [
{
test: /\.(png|svg|jpg|gif|pdf)$/,
include: [path('src')],
loader: 'url-loader', //file-loader的加强版,用于将图片转为base64,减少网络请求
options: {
name: 'img/[name].[hash:8].[ext]',
limit: 8192 //将小于limit的图片,转为base64。单位为byte。
}
}
]
};
let plugins = [
//定义全局变量,webpack编译后生效。'window._ENV_':在window对象中定义_ENV_属性,开发时没警告。直接定义_ENV_时,开发时会有警告。
new webpack.DefinePlugin({'window._ENV_': JSON.stringify(publicParam)})
];
//webpack的基础配置参数
let config = {entry, output, resolve, module, plugins};
return config;
};
```
#### 3.2 webpack.dev.js
webpack开发环境配置文件:webpack.dev.js
```
const webpack = require('webpack');
const {merge} = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const configWebpack = require('./webpack.config.js');
const path = (name) => require('path').resolve(__dirname, name); //获取绝对路径
/**
* webpack的开发环境配置文件
* 中文文档:https://www.webpackjs.com/concepts/
* @param env Node的环境变量:process.env。注意:webpack打包是在node环境中运行的。
*/
module.exports = (env) => {
let publicParam = require('./config/config.js')(env); //项目的公共配置
//指定开发环境。会将process.env.NODE_ENV的值设为development。启用NamedChunksPlugin和NamedModulesPlugin。
let mode = 'development';
//用来追踪源代码。source-map是最详细的。
let devtool = 'source-map';
//配置webpack服务器的参数
let devServer = {
//热替换,当本地文件更新后,浏览器页面自动刷新
hot: true,
//使用https协议,默认为false。
https: false,
//启动服务后,自动打开页面
open: true,
//显示打包进度
progress: true,
//用于定位打包报错
profile: true,
/**
* 指定主机,默认为localhost(即'127.0.0.1')。
* 默认无法通过ip地址访问webpack服务,除非指定host为‘0.0.0.0’或ip地址。
* ‘0.0.0.0’代表任意地址。指定host为‘0.0.0.0’时,还可以通过localhost或ip地址访问webpack服务。
* 注意:在window系统中指定host为‘0.0.0.0’时,只能通过localhost或ip地址访问webpack服务。
*/
host: '127.0.0.1',
//host: '0.0.0.0',
//指定端口号
port: 9100,
//设置代理
proxy: {
//对以'/weather_mini'开头的请求进行代理,注意必须以/开头
'/weather_mini': {
target: publicParam.host, //需要代理的地址
secure: true, //允许https请求
changeOrigin: true //允许跨域
}
}
};
//对模块的源代码进行转换。注意:webpack自身只理解JavaScript。
let module = {
//解析规则
rules: [
{
test: /\.jsx?$/, //需要解析的文件类型
include: path('src'), //需要解析的文件目录
loader: 'babel-loader' //解析器
},
{
test: /\.s?css$/, //加载css、scss文件
include: path('src'),
use: [
'style-loader', //处理css文件:将css添加到html的style标签中
'css-loader', //处理css文件:解释并引用css中的@import和url()
'postcss-loader', //处理css文件:自动补全webkit前缀等
'sass-loader' //将scss文件转为css文件
]
}
]
};
let plugins = [
//热替换插件
new webpack.HotModuleReplacementPlugin(),
//html加载插件
new HtmlWebpackPlugin({
title: publicParam.title, //项目名称
template: 'index.html', //Html模板
inject: true //注入打包后生成的文件
})
];
//开发环境配置参数
let config = {mode, module, devtool, devServer, plugins};
return merge(configWebpack(env), config);
};
```
#### 3.3 webpack.prod.js
webpack生产环境配置文件:webpack.prod.js
```
const {merge} = require('webpack-merge');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const copyWebpackPlugin = require('copy-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
const webpackBundleAnalyzer = require('webpack-bundle-analyzer');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin'); //用于替换momentjs,从而减少打包体积
const miniCssPlugin = require('mini-css-extract-plugin'); //用于将css打包到html外,提高加载速度
const configWebpack = require('./webpack.config.js');
const path = (name) => require('path').resolve(__dirname, name); //获取绝对路径
/**
* webpack的生产环境配置文件
* 中文文档:https://www.webpackjs.com/concepts/
* @param env Node的环境变量:process.env。注意:webpack打包是在node环境中运行的。
*/
module.exports = (env) => {
let publicParam = require('./config/config.js')(env); //项目的公共配置
/**
* 指定生产环境。会将process.env.NODE_ENV的值设为production,启用UglifyJsPlugin等插件。
* 注意:如果optimization.minimizer非空,则需要手动设置UglifyJsPlugin。
*/
let mode = 'production';
//对模块的源代码进行转换。注意:webpack自身只理解JavaScript。
let module = {
//解析规则
rules: [
{
test: /\.jsx?$/, //需要解析的文件类型
include: [path('src'), path('node_modules/antd-dayjs-webpack-plugin'), path('node_modules/asn1.js/lib')], //需要解析的文件目录
loader: 'babel-loader' //解析器
},
{
test: /\.s?css$/, //加载css、scss文件
include: path('src'),
use: [
{
loader: miniCssPlugin.loader, //用于将css打包到html外,提高加载速度
options: {publicPath: '../'} //指定相对路径
},
'css-loader', //处理css文件:解释并引用css中的@import和url()
'postcss-loader', //处理css文件:自动补全webkit前缀等
'sass-loader' //将scss文件转为css文件
]
}
]
};
//插件
let plugins = [
//用来自动清空打包目录
new CleanWebpackPlugin(),
//用来复制文件,将webpack不打包的文件复制到打包目录
new copyWebpackPlugin({
patterns: [{from: path('public'), to: path(publicParam.fileName + '/public')}]
}),
//生成打包分析文件
new webpackBundleAnalyzer.BundleAnalyzerPlugin({analyzerMode: 'static'}),
new miniCssPlugin({filename: 'css/[name].[hash:8].css'}),
new AntdDayjsWebpackPlugin({preset: 'antdv3'}),
//html加载插件
new htmlWebpackPlugin({
//项目名称
title: publicParam.title,
//Html模板
template: 'index.html',
//注入打包后生成的文件
inject: true,
//压缩配置
minify: {
removeComments: true, //是否移除注释
collapseWhitespace: true, //是否去掉空白
removeAttributeQuotes: true //是否去掉属性引号
}
})
];
//生产环境配置参数
let config = {mode, module, plugins};
return merge(configWebpack(env), config);
};
```
### 4 配置Babel
Babel用于支持IE9等老式浏览器使用新的JS语法和API(比如Map对象)。其中,@babel/preset-env用于支持新的JS语法,core-js用支持新的API(注意:polyfill已过时)。
首先,创建babel的配置文件:babel.config.js
```
/**
* Babel的配置文件
*/
module.exports = function(api) {
api.cache(true); //缓存babel的配置文件,不再重复调用当前函数。
//预设转码
const presets = [
['@babel/env'],//用于支持IE9等老式浏览器使用新的JS语法,需要安装:@babel/preset-env
['@babel/react'] //用于支持IE9等老式浏览器使用React的语法,,需要安装:@babel/preset-react
];
//插件
const plugins = [
[
'@babel/plugin-proposal-decorators', //用来支持修饰器,需要安装:@babel/plugin-proposal-decorators
{legacy: true} //注意:legacy需要为true。
],
'transform-class-properties' //用于在class中声明属性(包含静态属性),需要安装:babel-plugin-transform-class-properties
];
return {presets, plugins};
};
```
然后,在src/index.js的首行引用core-js:
```
import 'core-js/stable';
import 'regenerator-runtime/runtime';
```
### 5 配置Postcss
Postcss用于处理css文件::自动补全webkit前缀等。
首先,创建Postcss的配置文件:postcss.config.js
```
//postcss的配置。PostCSS是一个用JavaScript工具和插件转换CSS代码的工具,类似babel对js的处理。
module.exports = {
plugins: {
//postcss插件:用于支持下一代的css,包含autoprefixer(自动补全webkit等前缀)
'postcss-cssnext': {
browsers: ['last 2 versions', '> 5%']
},
//postcss插件:修复flex的bug
'postcss-flexbugs-fixes': {}
}
};
```
然后,在webpack的配置文件中指定loader。详情见:webpack.dev.js
### 6 启动项目
启动项目,需要运行命令:
```
npm start
```
然后,就可以在浏览器上看到我们创建的页面了。
## 配置antd
### 1 基本配置
首先,安装依赖:
```
//因为需要兼容IE9,所以antd使用3.X版本
npm i antd@3.26.18 -S
//用于配置按需加载
npm i babel-plugin-import -D
//用于主题定制
npm i less less-loader -D
```
然后,在babel.config.js中做按需加载:
```
/**
* Babel的配置文件
*/
module.exports = function(api) {
const plugins = [
[
'import', //按需加载,需要安装babel-plugin-import
{libraryName: 'antd', libraryDirectory: 'es', style: "css"} // `style: true` 会加载 less 文件
]
];
...
};
```
然后,就可以使用antd了:
```
import {Button} from 'antd'
```
### 2 主题定制
主题定制用于自定义antd的UI风格。
首先,新建antd的主题配置文件:config/configTheme.js:
```
/**
* antd的定制主题样式
* 参考:https://ant.design/docs/react/customize-theme-cn
*/
module.exports = {
'primary-color': '#2286ff', //主题色
'btn-height-base': '32px',
'btn-default-color': '#2286ff
};
```
然后,在babel.config.js中修改按需加载:
```
{
...
[
'import', //按需加载,需要安装babel-plugin-import
{libraryName: 'antd', libraryDirectory: 'es', style: true}
]
...
}
```
然后,在webpack.config.js中做主题定制:
```
module.exports = env => {
const modifyVars = require('./config/configTheme.js'); //antd的主题定制
let module = {
rules: [
{
test: /\.(css|less)$/, //加载less文件:用于antd主题定制
include: /node_modules/,
use: [
'style-loader',//处理css文件:将css添加到html的style标签中
'css-loader',//处理css文件:解释并引用css中的@import和url()
{
loader: 'less-loader',//将less文件转为css文件
options: {
lessOptions: {
modifyVars,
javascriptEnabled: true //必须为true
}
}
}
]
}
]
};
};
```
### 3 dayjs
你可以使用 [antd-dayjs-webpack-plugin](https://github.com/ant-design/antd-dayjs-webpack-plugin) 插件用 dayjs 替换 momentjs, 来大幅减小打包大小。
首先,安装依赖:
```
npm i dayjs -S
npm i antd-dayjs-webpack-plugin -D
```
然后,在webpack.config.js中配置:
```
//用于替换momentjs,从而减少打包体积
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
module.exports = {
...
let plugins = [
new AntdDayjsWebpackPlugin({preset: 'antdv3'})
];
};
```
注意:antd-dayjs-webpack-plugin不兼容IE9,需要使用babel-loader转码。
```
module.exports = (env) => {
let module = {
rules: [
{
test: /\.jsx?$/, //需要解析的文件类型
include: [path('src'), path('node_modules/antd-dayjs-webpack-plugin')], //需要解析的文件目录
loader: 'babel-loader' //解析器
},
]
};
};
```
## 状态管理
状态管理使用Mobx,官方文档:https://cn.mobx.js.org,**当前版本:4.15.7。**
### 1 基本配置
首先,安装依赖
```
//因为需要兼容IE9,所以mobx使用4.X版本
npm i mobx@4.15.7 mobx-react@6.3.1 -S
```
然后,新建Store:src/models/Order.js
```
import {observable, computed, action, configure} from 'mobx';
//启用严格模式。严格模式下,只能通过action来修改state。默认状态下,可以直接修改。
configure({enforceActions: 'observed'});
/**
* 订单模块的Store(状态管理器)
*/
export default class Order {
//@observable修饰器用来声明需要观察的属性。当该属性变化后,对应的View也会刷新。
@observable dataList = [];
//@observable修饰器用来声明计算属性。当被计算的属性变化时,该属性也会自动变化。
@computed get dataListLength() {
return this.dataList.length;
}
//@observable修饰器用来声明action:用来修改state。严格模式下,只能通过action来修改state。
@action addData = (value) => {
this.dataList.push(value);
};
@action deleteData = (index) => {
this.dataList.splice(index, 1);
};
}
```
然后,新建Page:src/order/index.js
```
import React from 'react';
import {Button} from 'antd';
import {inject, observer} from 'mobx-react';
/**
* 订单页面
* @inject 用来将Store添加到组件的属性中。参数一为Store的名称。
* @observer 用来声明组件为mobx的观察者。当Store的可观察属性改变时,刷新页面。注意:先使用@inject,后使用@observer。
*/
@inject('order')
@observer
export default class Order extends React.Component {
//构造函数
constructor(props) {
super(props);
//使用state保存数据
this.state = {};
}
//界面渲染函数
render() {
return (
{this.renderList()}
);
}
/**
* 加载列表
*/
renderList() {
let {dataList} = this.props.order;
return dataList.map((item, index) => {
return (
{item.title}
);
});
}
addData = () => {
let {dataListLength, addData} = this.props.order;
let data = {title: `Ant Design Title ${dataListLength}`};
addData(data);
};
deleteData = (index) => {
this.props.order.deleteData(index);
};
}
```
然后,在src/App.js中引用Store:
```
import React from 'react';
import {Provider} from 'mobx-react';
import Order from './pages/order';
import OrderStore from './models/Order';
import './App.scss';
/**
* 顶层组件
*/
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
);
}
}
```
### 2 单一Store
Mobx正常情况下,使用多个Store。但是,在大型项目中,多个Store会导致项目的可维护性变差。所以,这里使用单一Store。
首先,创建根Store:src/models/index.js
```
import {observable, action, configure} from 'mobx';
import Order from './Order';
//启用严格模式。严格模式下,只能通过action来修改state。默认状态下,可以直接修改。
configure({enforceActions: 'observed'});
/**
* 根Store:用于创建单一Store
*/
export default class Store {
constructor() {
this.order = new Order(); //子Store
}
//@observable修饰器用来声明需要观察的属性。当该属性变化后,对应的View也会刷新。
@observable dataBase = {};
//@observable修饰器用来声明action:用来修改state。严格模式下,只能通过action来修改state。
@action setData = (value) => {
this.dataBase = value;
};
}
```
然后,在src/App.js中引用根Store:
```
import Store from './models';
```
然后,在src/order/index.js使用根Store:
```
@inject('store')
@observer
export default class Order extends React.Component {
...
addData = () => {
let {dataListLength, addData} = this.props.store.order;
let data = {title: `Ant Design Title ${dataListLength}`};
addData(data);
};
deleteData = (index) => {
this.props.store.order.deleteData(index);
};
}
```
### 3 刷新浏览器
一般情况下,我们希望刷新浏览器后,Store的数据还在。
首先,在src/models/index.js中改造:
```
export default class Store {
/**
* 构造函数
* @param isInit 是否根据sessionStorage初始化状态
*/
constructor(isInit = true) {
let keys = Object.keys(toJS(this)); //根Store自身状态的key
this.order = new Order(); //子Store
this.initState(keys, isInit);//根据sessionStorage初始化状态
}
/**
* 初始化Store状态
* 注意:只支持对两层数据接口的Store初始化
* @param keys 根Store自身状态的key
* @param isInit 是否根据sessionStorage初始化状态
*/
initState(keys, isInit) {
let data = JSON.parse(sessionStorage.getItem('store')); //保存在sessionStorage里的状态信息
if (!data || !isInit) return;
Object.keys(data).forEach((key) => {
let item = data[key];
//初始化根Store自身状态
if (keys.includes(key)) {
this[key] = item;
return;
}
//初始化子Store的状态
Object.keys(item).forEach((key2) => {
if (this[key]) this[key][key2] = item[key2];
});
});
}
/**
* 保存Store状态:保存在sessionStorage里
*/
saveState() {
//注意:Store不能直接转JSON,需要先使用toJS函数转换
let data = toJS(this);
sessionStorage.setItem('store', JSON.stringify(data));
}
}
```
然后,在src/App.js中监听页面刷新:
```
export default class App extends React.Component {
constructor(props) {
super(props);
this.store = new Store(); //如果不需要初始化状态,则参数传fasle
}
componentDidMount() {
//在页面刷新时将mobx里的信息保存到sessionStorage里
window.addEventListener('beforeunload', () => {
this.store.saveState();
});
}
render() {
return (
);
}
}
```
### 4 Mobx与Redux对比
[Mobx](https://cn.mobx.js.org)与[Redux](https://www.redux.org.cn/)是React开发中常用的两个状态管理框架。
- 使用目的:都是用来管理应用状态的
- 学习成本:Mobx更容易入门。Mobx需要30分钟入门,Redux需要3小时。
- 使用难度:Mobx使用更方便。同样的功能,Mobx可能需要7行代码,而Redux需要12行代码。
- 内存对比:Mobx内存更少。因为Mobx更新State是操作原对象,而Redux是创建新对象。
- 数据层级:Mobx既支持多个Store,也支持单一Store;Redux只支持单一Store。
- 代码规范:Mobx默认可以随意修改State,严格模式下只允许通过Action修改State;Redux只允许通过Action修改State。
综上,小型项目推荐Mobx,大型项目推荐单一Store和严格模式的Mobx
## 路由管理
路由管理使用React Router,官方文档:https://reactrouter.com/web,**当前版本:5.2.0。**
### 1 基本配置
首先,安装依赖:
```
//react-router-dom是react-router的web版,支持浏览器操作路由
npm i react-router-dom -S
```
然后,创建403、404页面:src/pages/403.js、src/pages/404.js。**注意:代码太多,这里就省略了**
然后,创建布局组件。实际项目中,页面由Layout(布局组件)和Page(页面组件)构成。Layout为一级路由,通过react-router的Switch组件嵌套Page(即二级路由)。**注意:代码太多,这里只给出部分**
- src/layouts/components/Header.js:导航栏组件。
- src/layouts/components/Slider.js:左侧菜单组件。
- src/layouts/LayoutHome.js:默认页面布局,不包含左侧菜单、导航栏、面包屑
- src/layouts/LayoutView.js:公共组件布局,包含左侧菜单、导航栏,不包含面包屑
- src/layouts/LayoutMenu.js:菜单页面布局,包含左侧菜单、导航栏、面包屑
src/layouts/LayoutHome.js:
```
import React from 'react';
import {Switch} from 'react-router-dom';
/**
* 默认页面布局
* 属性:
* type:布局类型。类型:'view'|'home'|'menu'。
* routes:子路由组件数组。类型:ReactNode[]。
*/
export default class LayoutHome extends React.Component {
constructor(props) {
super(props);
}
render() {
return this.renderRoutes();
}
/**
* 子路由页面
*/
renderRoutes() {
let {routes} = this.props;
return {routes};
}
}
```
然后,创建React Router的配置文件:src/router/Config.js。
```
//布局配置
const configLayout = {
view: {
label: '公共组件', //一级路由的标题
path: '/view', //一级路由的地址,以'/'开头
view: 'layouts/LayoutView' //二级路由组件:相对于src目录
},
home: {label: '默认页面', path: '/home', view: 'layouts/LayoutHome'},
menu: {label: '菜单页面', path: '/menu', view: 'layouts/LayoutMenu'}
};
//路由配置
const configRouter = {};
//公共组件布局的路由配置
configRouter.view = [
{
label: '公共组件', //二级路由的标题,同时是一级左侧菜单的标题
path: 'com', //二级路由的地址,不以'/'开头,同时是一级左侧菜单的Key
icon: 'wode', //一级左侧菜单的图标类型
auths: [1, 2], //权限列表:1是管理员,2是开发,3是测试,4是产品。如果未定义auths,则表示不需要校验权限。
children: [
{
label: '表单', //二级路由的标题,同时是二级左侧菜单的标题
path: 'form', //二级路由的地址,不以'/'开头,同时是二级左侧菜单的Key
view: 'components/form/Demo', //二级路由组件:相对于src目录
auths: [1, 2]
}
]
}
];
//默认页面布局的路由配置
configRouter.home = [
{
label: '目录',
path: 'list',
view: 'pages/Home' //一级路由组件:相对于src目录
},
{label: '登录', path: 'login', view: 'pages/login'}
];
//菜单页面布局的路由配置
configRouter.menu = [
{
label: '菜单页面',
path: 'demo',
icon: 'wodeguanzhu',
auths: [1, 2, 3, 4],
children: [
{label: '订单', path: 'order', view: 'pages/order', auths: [1, 2, 3, 4]},
{label: '登录', path: 'login', view: 'pages/login'}
]
}
];
export {configRouter, configLayout};
```
**注意:路由层级由UI决定,左侧菜单层级由逻辑关系决定**
然后,创建React Router的工具类src/utils/ToolRoute.js:用于格式化路由配置、生成懒加载的路由组件。
```
import React from 'react';
import {Route} from 'react-router-dom';
import {Tool, ToolHistory} from './index';
import {configRouter, configLayout} from '../router/Config';
/**
* 路由管理工具类
*/
export default {
/**
* 格式化路由
* 注意:当用户角色类型改变时,需要重新调用该方法
* @param auth 当前用户的角色类型。如果auth为空,则不校验权限。
* @param lvMax 左侧菜单的最大层级。默认值:2
*/
formatRouter(auth, lvMax = 2) {
let routerMenu = {}; //左侧菜单的路由列表
let routerFormat = {}; //格式化的路由列表
Object.keys(configRouter).forEach((key) => {
let item = configRouter[key];
let dataLayout = configLayout[key];
if (!item || item.length == 0) return;
routerMenu[key] = routerMenu[key] || [];
routerFormat[key] = routerFormat[key] || {};
item.forEach((item2, index) => {
let {label, path, auths, children, ...other} = item2;
if (auth && auths && !auths.includes(auth)) return;
//如果父级路由为'/',则不拼接父级路由
path = `${dataLayout.path === '/' ? '' : dataLayout.path}${path.startsWith('/') ? '' : '/'}${path}`;
let data = {index, label, path, ...other};
routerMenu[key].push(data);
routerFormat[key][path] = data;
//当前的菜单层级列表
data.routes = [{label, path}];
//指定菜单项的key
if (children && children.length > 0) {
data.keyGroup = path;
data.children = [];
this.formatRouterRecursion(routerFormat[key], data, children, auth, lvMax);
} else {
data.keyItem = path;
}
});
});
return {routerMenu, routerFormat};
},
/**
* 格式化路由:递归遍历
* @param route 格式化的路由信息
* @param data 上级路由
* @param list 子路由列表
* @param auth 当前用户的角色类型
* @param lvMax 左侧菜单的最大层级
* @param lv 左侧菜单的当前层级。默认值:2
*/
formatRouterRecursion(route, data, list, auth, lvMax, lv = 2) {
if (!list || list.length == 0) return;
list.forEach((item, index) => {
let {label, path, auths, children, ...other} = item;
if (auth && auths && !auths.includes(auth)) return;
path = `${data.path}${path.startsWith('/') ? '' : '/'}${path}`;
let data2 = {index, label, path, ...other};
data2.routes = [...data.routes, {label, path}];
if (lv <= lvMax) {
data2.keyGroup = data.keyGroup;
data2.keyItem = path;
data.children.push(data2);
} else {
data2.keyGroup = data.keyGroup;
data2.keyItem = data.keyItem;
}
route[path] = data2;
this.formatRouterRecursion(route, data2, children, auth, lvMax, lv + 1);
});
},
/**
* 生成路由页面列表
*/
routes() {
let result = [];
let list = this.formatRouter().routerFormat;
this.key = 0; //重置路由组件的key
Object.keys(list).forEach((type) => {
let item = list[type];
let {path, view} = configLayout[type];
let routes = [];
Object.keys(item).forEach((key2) => {
let item2 = item[key2];
if (item2.view) routes.push(this.route(item2.path, item2.view));
});
result.push(this.route(path, view, false, {type, routes}));
});
return result;
},
/**
* 生成懒加载的路由页面
* 当view为组件名时,使用模板字符串传入二级路径的变量,实现懒加载.注意:webpack解析时,一级路径需要使用别名。
* 注意:如果在路由组件外部,import了该组件,那么懒加载就会失效。因为import是编译时加载。
* @param path url的路径
* @param View 组件或组件名。如果是组件名,则为src目录下的相对路径。
* @param exact 表示相同path的Route只会渲染第一个,这里设置默认true。注意:使用嵌套路由时,外出路由的exact必须为false。
* @param props 组件属性
*/
route(path, View, exact = true, props) {
let component;
if (typeof View === 'string') {
component = this.lazyLoad(() => import(`@/${View}`), props); //生成懒加载的页面
} else {
component = props ? () => : View;
}
this.key = this.key || 0;
return ;
},
/**
* 路由懒加载
* @param view 通过import函数得到的组件
* @param props 组件属性
*/
lazyLoad(view, props = {}) {
//懒加载组件
class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = {component: null};
}
componentDidMount() {
view().then((mod) => {
this.setState({component: mod.default || mod});
});
}
render() {
const C = this.state.component;
return C ? : null;
}
}
return AsyncComponent;
}
};
```
然后,创建React Router:src/router/index.js。
```
import React from 'react';
import {HashRouter, Switch, Redirect} from 'react-router-dom'; //路由框架:https://reacttraining.com/react-router
import {ToolRoute} from '../utils';
/**
* 获取路由
*/
export default function () {
let pageHome = ; //首页使用重定向。因为没有做菜单与权限控制。
let page403 = ToolRoute.route('/403', 'pages/403'); //没有访问权限时,手动跳转403页面
let page404 = ToolRoute.route('*', 'pages/404'); //路由不匹配时,自动跳转404页面
return (
{pageHome}
{ToolRoute.routes()}
{page403}
{page404}
);
}
```
然后,在src/App.js中引用React Router:
```
import React from 'react';
import {Provider} from 'mobx-react';
import getRouter from './router';
export default class App extends React.Component {
...
render() {
return {getRouter()};
}
}
```
### 2 路由权限与左侧菜单
实际项目中,会检查登录态,然后根据用户角色获取左侧菜单,根据当前路由地址高亮左侧菜单项。
首先,在src/utils/ToolRoute.js增加检查登录和路由访问权限的逻辑:
```
import {Tool, ToolHistory} from './index';
import {configRouter, configLayout} from '../router/Config';
/**
* 路由管理工具类
*/
export default {
/**
* 检查是否登录
*/
checkLogin() {
//如果没有登录,则跳转登录页面
if (!Tool.getUser().token) {
ToolHistory.href('/home/login');
return false;
}
return true;
},
/**
* 检查路由访问权限
* @param routerFormat 格式化的路由列表
*/
checkRoute(routerFormat) {
let path = ToolHistory.getRouteAddress(); //当前页面的路由地址
let result;
Object.keys(routerFormat).some((key) => {
let pathLayout = `/${key}`; //Layout组件的路由
if (path === pathLayout) {
result = {keyGroup: path}; //keyGroup用于展开Layout的左侧菜单
} else {
let item = routerFormat[key];
result = item[path];
}
return result;
});
//如果没有访问权限,则跳转403页面
if (!result) ToolHistory.href('/403');
return result;
}
};
```
然后,在src/App.js中检查登录态和路由访问权限,并监听路由改变:
```
import React from 'react';
import {toJS} from 'mobx';
import {Provider} from 'mobx-react';
import {Tool, ToolRoute} from './utils';
import './App.scss';
/**
* 顶层组件
*/
export default class App extends React.Component {
componentDidMount() {
...
//检查路由访问权限
this.checkRoute();
//监听路由改变
window.onhashchange = (e) => {
//切换路由后,需要将滚动条的位置恢复到初始值
let d = document.getElementsByClassName('ant-layout-content')[0];
if (d) d.scrollTop = 0;
this.checkRoute();
};
}
componentWillUnmount() {
//页面销毁时,取消路由监听
window.onhashchange = (e) => null;
}
...
/**
* 检查路由访问权限
*/
checkRoute() {
if (!ToolRoute.checkLogin()) return;
let routerFormat = this.store.routerFormat; //格式化的路由列表
//如果routerFormat为空,就重新获取
if (!routerFormat || !routerFormat.menu) {
let router = ToolRoute.formatRouter(Tool.getUser().auth); //根据角色类型获取左侧菜单列表和格式化路由列表
routerFormat = router.routerFormat;
this.store.setRouterMenu(router.routerMenu); //在根Store中保存左侧菜单
this.store.setRouterFormat(router.routerFormat);
} else {
routerFormat = toJS(routerFormat);
}
let result = ToolRoute.checkRoute(routerFormat);
this.store.setRouterData(result);
}
}
```
**注意:根据角色类型获取到左侧菜单列表和格式化路由列表后,会保存在根Store中,从而刷新左侧菜单组件**
然后,在src/layouts/components/Slider.js展示左侧菜单,并根据根路由的routerData(当前路由信息)高亮当前菜单项。
```
import React from 'react';
import {Menu, Layout,Icon} from 'antd';
import {inject, observer} from 'mobx-react';
import {ToolHistory} from '../../utils';
/**
* 左侧菜单组件
* 属性:
* type:布局类型。类型:'view'|'home'|'menu'。
*/
@inject('store')
@observer
export default class Sider extends React.Component {
render() {
let {collapsed} = this.state;
return (
{this.renderMenu()}
);
}
/**
* 左侧菜单
*/
renderMenu() {
let {routerData, routerMenu} = this.props.store;
if (routerMenu) routerMenu = routerMenu[this.props.type];
//注意:routerData只能维护菜单项的key,所以菜单的key使用defaultOpenKeys属性
const {keyGroup, keyItem} = routerData||{};
//由于菜单的key使用defaultOpenKeys属性,所以需要keyGroup非空,才能正常展示
if (!keyGroup) return;
return (
);
}
/**
* 菜单列表
*/
renderMenuList(list) {
let result = [];
list.forEach((item) => {
let {children, label, icon} = item;
let list2 = [];
children.forEach((item2) => {
if (!item2.view) return;
let viewItem = (
ToolHistory.href(item2.path)}>
{item2.label}
);
list2.push(viewItem);
});
let viewMenu = (
{label}
}
>
{list2}
);
result.push(viewMenu);
});
return result;
}
}
```
### 3 路由懒加载
路由懒加载就是打开路由时,才加载对应的页面组件,从而提高首页加载速度。
上面的代码ToolRoute.lazyLoad()就是用来生成路由懒加载组件的,这里说下路由懒加载的原理。
首先,ToolRoute.lazyLoad()返回了一个包装组件(包含了实际的页面组件),在创建路由时,加载的就是包装组件。包装组件虽然在打开首页时会加载,但是因为它几乎没有什么代码,所以不会影响首页加载速度。
然后,打开对应的路由时,包装组件会执行render函数,这个时候就加载实际的页面组件(从服务器获取对应的js、css文件),然后展示实际的页面组件。
### 4 面包屑
面包屑是一个导航条,用来展示并跳转上层页面(父级、父级的父级...)。
在src/layouts/LayoutMenu.js(菜单页面布局)中,需要展示面包屑:根据根路由的routerData的routes展示。
```
import React from 'react';
import {Layout, Breadcrumb} from 'antd';
import {inject, observer} from 'mobx-react';
import LayoutHome from './LayoutHome
import {ToolHistory} from '../utils';
@inject('store')
@observer
export default class LayoutMenu extends LayoutHome {
/**
* 主体的面包屑
*/
renderContentBreadcrumb() {
let views = [];
let {routes} = this.props.store.routerData||{};//当前的菜单层级列表
let size = routes && routes.length;
for (let i = 0; i < size; i++) {
let item = routes[i];
let view = null;
view = ToolHistory.href(item.path)}>{item.label};
views.push({view});
}
return (
{views}
);
}
}
```
### 5 标签页
#### 5.1 介绍
一般老项目的Layout的子页面,不是面包屑,而是标签页。
标签页的特点:
- 标签页通过点击左侧菜单生成
- 切换标签页后,上一个标签页的内容还存在。
实现原理:
- 首先,在Layout中保存二级路由组件列表,但是不添加到页面中。
- 然后,点击左侧菜单,跳转二级路由页面。这个时候,虽然浏览器上输入了二级路由地址,但是由于二级路由不存在,所以页面还是停留在一级路由(即Layout)
- 然后,在Layout中获取二级路地址,根据二路由地址从二级路由组件列表取出对应的二级路由组件,然后生成对应的标签页。每次点击左侧菜单,都会新增一个标签页。
#### 5.2 实现
首先,创建标签页面布局:src/layouts/LayoutTab.js。
```
import React from 'react';
import {Layout, Tabs, Icon} from 'antd';
import {inject, observer} from 'mobx-react';
import LayoutHome from './LayoutHome';
import {ToolHistory, ToolRoute} from '../utils';
import './LayoutTab.scss';
/**
* 标签页面布局
*/
@inject('store')
@observer
export default class LayoutTab extends LayoutHome {
constructor(props) {
super(props);
this.tabs = []; //标签列表
}
...
/**
* 子路由页面
*/
renderRoutes() {
//二级子路由列表
const {routes} = this.props;
//当前路由信息
const {path, label} = this.props.store.routerData||{};
if(!path) return;
//添加未点击的二级路由组件
if (!this.tabs.find((item) => item.path === path)) {
let route = routes.find((item) => item.props.path === path);
if (!route) return;
//生成懒加载组件
let view = this.renderLayload(route.props.view);
this.tabs.push({path, label, view});
}
//创建标签页
let panes = this.tabs.map((item, index) => {
return (
{item.view}
);
});
return (
{panes}
);
}
/**
* 懒加载组件
* @param path 组件名:为src目录下的相对路径。
*/
renderLayload(path) {
let View = () => import(`@/${path}`);
View = ToolRoute.lazyLoad(View, {path});
return ;
}
/**
* 标签栏
* @param item 组件数据
* @param index 组件索引
* @param path 当前路由地址
*/
renderTab(item, index, path) {
let style = {display: path === item.path ? 'none' : ''};
let click = () => {
this.tabs.splice(index, 1);
this.setState({});
};
return (
path !== item.path && ToolHistory.href(item.path)}>{item.label}
);
}
}
```
然后,在src/router/Config.js中配置路由信息:
```
//布局配置
const configLayout = {
...
tab: {label: '标签页面', path: '/tab', view: 'layouts/LayoutTab'}
};
//路由配置
const configRouter = {};
//标签页面布局的路由配置
configRouter.tab = [
{
label: '标签页面',
path: 'demo',
icon: 'wodeguanzhu',
auths: [1, 2, 3, 4],
children: [
{label: '订单示例', path: 'order', view: 'pages/order', auths: [1, 2, 3, 4]},
{label: 'Webrtc示例', path: 'webrtc', view: 'pages/webrtc', auths: [1, 2, 3, 4]}
]
}
];
```
## 跨域访问
跨域访问是指不同域名的资源进行通信。不同域名是指协议、主域名、子域名和端口号中任意一个不相同。资源是指前端页面和后端服务。
由于JS的同源策略,浏览器不允许跨域访问。**注意:跨域不是请求发不出去,而是请求发出、服务端响应后,响应结果被浏览器拦截了。**
### 1 本地代理
实现:在webpack.dev.js的devServer中配置。
原理:欺骗浏览器,让浏览器以为是同源访问。
用途:用于本地调试,推荐使用。
### 2 禁用谷歌安全机制
原理:关闭谷歌浏览器的跨域拦截功能。
实现:先完全退出谷歌浏览器,然后运行命令:
```
open -a Google\ Chrome --args --disable-web-security --user-data-dir
```
用途:用于本地调试,但会影响浏览器性能。
### 3 CORS
原理:在服务端指定允许访问的域名,浏览器就不会拦截了。
实现:服务器端做手脚,在响应头header中添加"Access-Control-Allow-Origin",指定允许访问的源。
用途:一般用于生产环境解决跨域问题。
### 4 JSONP
原理:欺骗浏览器,让浏览器以为是脚本请求。脚本请求是指访问js文件,浏览器不会进行拦截。
用途:JSONP只支持get请求,除非要兼容老式浏览器,否则推荐使用CORS进行跨域访问。
## 调试
### 1 PC调试:添加断点
方法一:
在代码中,添加代码`debugger;`,即在此处添加断点;
方法二:
在浏览器调试界面,选择`Sources`-`top`-`webpack-internal://`-`.`-`src`目录下,找到对应的js文件,然后在js页面左侧,左键点击提交断点。
### 2 H5调试:vconsole
安装:npm install vconsole
使用:在项目的入口初始化即可
```
if (process.env.NODE_ENV === `development`) {
var vConsole = new VConsole(); // init vConsole
console.log('Hello world');
}
```
### 3 Android调试
在Android应用中调试H5页面,需要Stetho和Chrome DevTools。
首先,在Android App上安装Stetho:https://github.com/facebook/stetho。注意:一般手机上的浏览器,都自带类似Stetho的功能,即可以直接调试。
然后,开启手机的开发者选项和调试功能,并打开App上的H5页面。
然后,在谷歌浏览器上打开`chrome://inspect`,即可看到App上的H5页面,选中一个H5页面点击`inspect`,即可调试该页面。
注意:打开调试页面后,可以在地址输入栏打开自己的H5页面,这样就可以在自己的H5页面上调试Native的功能。
注意:在H5页面使用`console.log()`,在AS的logcat中可以查看。
### 4 iOS调试
首先,开启Safari开发菜单。在Mac的Safari偏好设置中,开启开发菜单。具体步骤为:Safari -> 偏好设置… -> 高级 -> 勾选在菜单栏显示“开发”菜单。
然后,开启iPhone的Web检查器。具体步骤为:设置 -> Safari -> 高级 -> Web 检查器。
然后,运行App。打开项目,Cmd + R 运行,打开想调试的Web页面
最后,调试页面:
- 首先,电脑连接手机。
- 然后,打开Safari -> 开发 -> 设备 -> URL。
## 配置Eslint
项目创建时,我们使用[Eslint](http://eslint.cn/)+[Prettier](https://prettier.io)来统一代码风格。
### 1 安装Eslint+Prettier
Eslint是检查代码风格/错误的小工具,用来统一整个团队的代码风格。**当前版本:7.10.0。**
```
npm i eslint -D
//Eslint的babel解析器
npm i babel-eslint -D
//用于校验React语法
npm i eslint-plugin-react -D
```
Prettier是一个javascript的格式化工具,可以完全统一整个团队的代码风格。**当前版本:2.1.2。**
安装prettier,并使用与eslint配套的插件。
```
npm i prettier -D
//用于在Eslint中调用prettier的规则校验
npm i eslint-plugin-prettier -D
```
### 2 配置Eslint规则
官方文档:http://eslint.cn/docs/rules/
一共有三种方式支持对Eslint进行配置:
1. 根目录创建`.eslintrc `文件,能够写入YML、JSON的配置格式,并且支持`.yaml/.yml/.json/.js`后缀
2. 根目录创建`.eslintrc.js `文件,并对外export一个对象;
3. 在`package.json`中新建`eslintConfig`属性。
下面我们使用`.eslintrc.js `的方式:
```
//是否生产环境。process为webpack编译时的全局变量。
const isPrd = process.env.NODE_ENV === 'production';
/**
* 定义Eslint的校验规则
* 官方文档:http://eslint.cn/docs/rules/
*/
module.exports = {
//是否从父级目录查找配置文件。为true,则不从父级目录查找。
root: true,
//指定你想启用的环境
env: {
browser: true, //浏览器环境:使用浏览器运行代码
node: true, //node环境:使用node.js运行代码
es6: true //ES6环境:允许使用Promise、Uint8Array等全局对象
},
//指定解析器。需要安装babel-eslint:一个对Babel解析器的包装,使其能够与ESLint兼容。
parser: 'babel-eslint',
//解析器配置
parserOptions: {
ecmaVersion: 2018, //支持ES2018语法
sourceType: 'module', //指定代码是ECMAScript模块。如果不指定,那么直接在js中import、export会报错。
//额外支持的语法
ecmaFeatures: {
jsx: true, //支持JS语法糖
legacyDecorators: true //用来支持修饰器(@)写在class上面
}
},
//使用Eslint默认的校验规则
extends: ['eslint:recommended'],
//使用eslint-plugin-react和eslint-plugin-prettier插件
plugins: ['react', 'prettier'],
/**
* 自定义的校验规则
* 0或off:关闭规则
* 1或warn:警告提示
* 2或error:错误提示
*/
rules: {
/*****************Eslint的基本检查*****************/
//no-console代表禁止打印日志。
'no-console': isPrd ? 2 : 0,
//禁止调试
'no-debugger': isPrd ? 2 : 0,
//禁止未使用的变量、函数
'no-unused-vars': 0,
//禁止使用不必要的转义符
'no-useless-escape': 0,
/*****************react的检查*****************/
//react的属性检查:建议关闭
'react/prop-types': 0,
//react的状态检查:建议关闭
'react/no-direct-mutation-state': 0,
//react中数组的key:建议关闭,因为很容易误判
'react/jsx-key': 0,
//react的组件名需要名字。非组件的render函数会误判
'react/display-name': 0,
//是否开启prettier校验。默认值:0。
'prettier/prettier': 2
}
};
```
### 3 配置Prettier规则
官方文档:https://prettier.io/docs/en/options.html
一共有三种方式支持对Prettier进行配置:
1. 根目录创建`.prettierrc `文件,能够写入YML、JSON的配置格式,并且支持`.yaml/.yml/.json/.js`后缀
2. 根目录创建`.prettierrc.js `文件,并对外export一个对象;
3. 在`package.json`中新建`prettier`属性。
下面我们使用`.prettierrc.js`的方式对prettier进行配置,同时讲解下各个配置的作用。
```
/**
* 定义Prettier的规则
* 官方文档:https://prettier.io/docs/en/options.html
*/
module.exports = {
printWidth: 120, //一行最多显示的字符数。默认值:80。
tabWidth: 2, //每个缩进级别的空格数。默认值:2。
useTabs: false, //使用制表符而不是空格缩进行。默认值:false。
semi: true, //在语句末尾打印分号。默认值:true。
singleQuote: true, //使用单引号而不是双引号。默认值:false。
trailingComma: 'none', //多行时尽可能打印尾随逗号。类型:none|es5|all。默认值:none。
bracketSpacing: false, //在对象文字中的括号之间打印空格。默认值:true。为true的示例:{ foo: bar }。
endOfLine: 'auto' //指定行尾的结束符。建议为'auto',否则可能报错:Delete `␍`eslint(prettier/prettier)
};
```
### 4 使用
使用npm命令:
```
"scripts": {
//配置命令:检查并修复src目录下的所有js文件
"lint": "eslint --fix --ext .js src"
},
npm run lint
```
**注意:**由于Eslint规则中开启了prettier校验,所以使用eslint的fix功能时,会自动调用prettier来格式化代码。
直接使用Eslint:
```
//检查目录下的所有js、scss文件:包括子目录
./node_modules/.bin/eslint --ext .js --ext .scss src
```
直接使用Prettier:
```
//格式化目录下的所有js、scss文件:包括一级子目录
./node_modules/prettier/bin-prettier.js --write src/{*,/**/*}{.js,.scss}
```
### 5 WebStorm配置
在WebStorm中配置Prettier:
1. 安装Prettier依赖
2. 打开Preferences->Languages & Frameworks-> JavaScript->Prettier,指定Prettier的路径即可。
WebStorm默认的格式化快捷键为 `Command+Alt+L` ,默认的Prettier快捷键为`Command+Shift+Alt+P`。
### 6 VSCode配置
在VSCode中配置Prettier:
首先,打开EXTENSIONS,安装:Prettier - Code formatter
然后,打开Code -> Preferences -> Settings -> Text Editor -> Files -> Edit in settings.json,设置默认的格式化方式:
```
"editor.defaultFormatter":"esbenp.prettier-vscode",
"[javascript]":{
"editor.defaultFormatter":"esbenp.prettier-vscode"
}
```
然后,使用格式化快捷键: `option+shift+F`
## 代码规范
### 1 代码结构
React页面的代码顺序如下:
- 构造函数
- 生命周期函数
- render函数,包含render开头的函数
- 初始化函数:以init开头
- 回调函数:以on开头
- 其他函数。
### 2 组件规范
组件是指可复用的公共模块,包含js和css。
组件分类,分为以下三种:
- 公共组件:所有项目通用的组件,位于`src/components`。对应一个目录,包含Demo.js、index.js、index.scss等。
- 项目组件:当前项目专用的组件,位于`src/pages/components`。对应一个目录,包含index.js、index.scss等。
- 模块组件:当前模块专用的组件,位于`src/pages/[model]`。对应一个目录,包含index.js、index.scss等。
组件入口:为公共组件设置一个统一的入口,方便引用。比如:`import {Menu, Menus, Icon} from '../components';`
组件命名:
- 目录名统一小写开头;文件名统一大写开头(index例外);
- 公共组件、项目组件命名由功能决定,比如:按钮为button。
- 模块组件命名由模块+功能决定,比如:订单的按钮为orderButton。
组件注释:
- 创建组件时,需要写注释。
- 如果是改造antd的组件,则只需注释改动的点,并给出antd组件的链接;
- 如果是全新的组件,则需要详细注释。
组件注释格式:
```
/**
* 选择器:对antd的Select的扩展,支持设置dataSource和默认的filterOption
* 参考:https://ant.design/components/select-cn/
* 新增属性:
* dataSource:数据源。类型:object[]。默认值:无。示例:[{label,value}]。
* label:标签。类型:string。
* value:值。类型:string。
*/
export class Selects extends React.Component {}
```
注意事项:
- 优先使用公共组件,而不是直接使用第三方组件。因为公共组件一般比第三方组件功能强大、稳定。
- 不要随意封装公共组件。因为封装的组件需要学习成本、扩展性一般不如第三方组件、文件加载速度不如第三方组件、不利于项目移植。
组件默认值:通过defaultProps静态属性设置默认值
```
export class Selects extends React.Component {
static defaultProps={
age:18,
"xxxx":'xxx'
}
}
```
注意:defaultProps设置的默认值是所有组件实例共享的。
### 3 图片规范
基础规范:
- 图标统一放在iconfont中,背景图统一放在assets的img目录中
- 图标使用`components/icon`:封装antd图标与自定义图标。自定义图标使用[iconfont.cn](https://www.iconfont.cn)来创建symbol引用的iconfont。
- 使用本地图片时,由于不支持图片适配,所以优先使用最大分辨率的图片;
- 在js中导入图片时,优先使用import方式;
图片命名。因为图标统一放在iconfont中,所以不需要加icon前缀
- 公共背景图:bg_[name]
- 公共图标:[name]
- 模块专用背景图:[模块名]_[name]
- 模块专用图标:[模块名]_[name]
### 4 注释规范
注释统一使用中文标点。
每个类和方法都需要有注释,类、方法、代码块使用段落注释:
```
/**
* 目录页面:包含工程的所有页面
*/
```
## 样式规范
### 1 基础样式
项目公共的基础样式包括颜色值、字体大小和公共样式:src/components/index.scss。
```
/**
*为了保证页面的整齐,需要统一以下公共样式:
*颜色,以color-开头,后面加颜色名;
*字体大小,以font-size开头,后面加字号;
*边距,一般不统一;
*
*项目中使用的颜色和字体大小,都要使用基础样式中定义的常量。
*/
$theme: #2286ff; // 主题色
/**************************颜色**************************/
$color-blue: $theme; // 蓝色
/**************************字体大小**************************/
$font-size: 14px; //默认大小
$font-size-s: 12px; //小号字体
$font-size-m: $font-size + 2px; //中号字体
$font-size-l: 20px; //大号字体
$font-size-ll: 24px; //加大号字体
/**************************常用样式**************************/
//超出部分省略号。注意:如果元素设置了title属性,当鼠标悬停时,会自动展示title。
.ellipsis {
display: block; //设置这个是为了兼容ie10
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
**一般情况下,项目的颜色值和字体大小是统一的,所以页面中用到的颜色值和字体大小统一从基础样式中获取,以便于后期维护。**
### 2 样式命名
样式结构与dom结构保持一致,即整体使用后代选择器结构。
- 顶层(一级)样式名要保证唯一性。
- 公共组件、项目组件一级样式名为`g-[功能名]`,比如列表为`g-list`;二级样式名为`[功能名]`,比如:订单的列表为`list`;三级以上样式名为`[上级样式名]-[功能名]`,比如:订单的列表为`list-item`;
- 模块组件一级样式名为`[模块名]-[功能名]`,比如订单的列表为`order-list`;二级样式名为`[功能名]`,比如:订单的列表为`list`;三级以上样式名为`[上级样式名]-[功能名]`,比如:订单的列表为`list-item`;
- 业务模块一级样式名为`[模块名]-[功能名]`,比如订单为`order-query`;二级样式名为`[功能名]`,比如:订单的列表为`list`;三级以上样式名为`[上级样式名]-[功能名]`,比如:订单的列表为`list-item`;
### 3 注意事项
- 重写antd样式时,优先在对应组件上重写,其次在业务模块上重写。
- 因为要兼容IE9,所以不能使用flex布局
## 常见问题
### 1 sass与less
#### 1.1 sass与less选择
sass分为[dart-sass](https://sass-lang.com/dart-sass)和[node-sass](https://github.com/sass/node-sass)。
- [Less](http://lesscss.cn/): 是一门 CSS 预处理语言,它扩展了 CSS 语言,增加了变量、Mixin、函数等特性,使 CSS 更易维护和扩展。
- [node-sass](https://github.com/sass/node-sass):比less功能强大、使用简单。
- [dart-sass](https://sass-lang.com/dart-sass):功能跟node-sass完全一样,安装比node-sass简单。
综上,推荐使用dart-sass。
#### 1.2 node-sass安装失败
失败现象:出现Cannot download 情况,是因为node-sass被墙了。
解决办法:
设置变量SASS_BINARY_SITE,指向淘宝镜像地址:
```
npm i node-sass -D --SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass
```
或者设置全局镜像源:
```
npm config set SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass
npm i node-sass -D
```
### 2 兼容IE9
大多数依赖库会兼容IE9等老式浏览器,少数依赖库不会支持。
兼容性问题分为两种:
- 不支持语法,比如:const语法不支持
- 不支持API,比如:btoa对象未定义
#### 2.1 不支持语法
不支持语法的问题,有两种解决方法:
方法一:直接使用babel-loader进行转码。比如:antd-dayjs-webpack-plugin使用了const语法,导致IE9报错,我们可以使用babel-loader直接转码。见dayjs部分。
方法二:将依赖库的文件,拷贝src目录下。比如:将axios库文件node_modules\axios\dist\axios.min.js,拷贝到src。
**注意:node_modules的库下面,一般包含dist(打包后的文件)和lib(源代码)。拷贝时,优先拷贝dist下的文件。**
#### 2.2 不支持API
对于不支持API的问题(比如:IE9报错btoa未定义),一般先找到出问题的依赖库,然后降低版本
第一步,在控制台找到报错提示,点击进入具体出错的地方,然后一直向上找,直到找到类似下面的标记:
```
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
```
第二步,知道报错的依赖库是style-loader后,就进行降级处理:
```
npm uni style-loader -D
npm i style-loader@1.0.0 -D
```
**注意:style-loader 使用1.0.0版本后,antd的样式会错乱,所以最终方案是,开发环境使用新版的 style-loader,测试、生产环境使用 mini-css-extract-plugin。**
### 3 识别@
#### 3.1 VS Code
VS Code 默认不识别 @,需要配置 jsconfig.json 才能识别:
```
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
#### 3.2 Webstorm
WebStorm 默认不识别 @,需要配置 webpack.config.js 才能识别:
```
const path = require('path');
/**
* 用于WebStorm识别@
*/
module.exports = {
context: path.resolve(__dirname, './'),
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};
```
### 4 VS Code修饰器报错解决
在VS Code中使用修饰器后,会提示语法错误。
解决办法:打开Files -> Preferences -> Settings,然后搜索experimentalDecorators,然后勾选即可。
### 5 禁用网络缓存
默认情况下,浏览器会自动缓存html中的js、css等资源,打开网页时,优先从缓存中获取js、css等资源。
有时候,我们为了验证网页首次加载速度,需要禁用网络缓存:打开开发者工具 -> NetWork -> Disable Cache。