# 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 ( {this.renderMenuList(routerMenu)} ); } /** * 菜单列表 */ 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。