前言
记录在项目中使用到的一些优化。
性能优化
1.Gzip压缩
借助CompressionWebpackPlugin插件来提前对文件进行Gzip压缩,服务器查找到有与源文件同名的.gz文件就会直接读取,不会主动压缩,降低cpu负载,优化了服务器性能。 安装
npm install --save-dev compression-webpack-plugin@1.1.12
webpackconfig.plugin中增加如下配置:
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
threshold: 10240,
minRatio: 0.8, //压缩比例
deleteOriginalAssets: false //是否删除源文件
})
nginx中增加gzip相关配置:
注:gzip_types中不要配置image相关
gzip on;
gzip_static on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
开启Gzip后:
文件体积减小至原来的1/4,测试平均整体加载时间减少20%。
文件体积越大效果越明显。
2.Code Split
- 第三库一般比较稳定,不会轻易改变,单独提取出来,做长期缓存。
- 可以拆分代码块,避免代码块体积过大,加载时间长
在webpack config文件的optimization中增加配置
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默 认会选择所有的模块。可以传递的值类型:RegExp、String和 Function;
chunks: 'initial', //同步(initial)、异步(async)、所有模块有效(all)
minSize: 30000, //最小尺寸
name: 'vendors', // 打包的chunks的名字
priority: -10, //优先级,表示抽取权重,数字越大表示优先级越高
enforce: true,
reuseExistingChunk: true // 可设置是否重用已用chunk 不再创建新的chunk
},
styles: {
name: 'styles',
test: /\.(css|less)$/,
chunks: 'all',
enforce: true,
}
}
}
3.React.lazy、React.Suspense按需动态加载
React.lazy 可以让我们像渲染常规组件一样处理动态引入的组件,结合 Suspense 可以更优雅地展现组件懒加载的过渡动画以及处理加载异常的场景。
import React, { lazy } from 'react' const SmsManager = lazy(() => import(/* webpackChunkName: "manager" */ 'manager'))
动态加载遇到的问题:
- 因为网络问题导致加载失败,报错Loading chunk failed
- 用户使用过程中,如果重新发布,因为浏览器缓存为旧的文件,和服务器文件名不一致,会导致文件下载失败报错Loading chunk failed
解决办法: 针对网络问题,可以通过webpack-retry-chunk-load-plugin重新加载
let {RetryChunkLoadPlugin} = require('webpack-retry-chunk-load-plugin')
//plugins中新增配置
plugins: [
new RetryChunkLoadPlugin({
cacheBust: `function() {
return Date.now();
}`,
maxRetries: 2,//加载失败最多尝试次数
})
]
针对使用过程中发布导致下载失败的问题,可以通过重新加载的方式解决(这种方式不算特别好,如果有更好的方法我再补充)
import React from 'react'
import { observer, inject } from 'mobx-react'
import Component from 'edt-components/Component'
import { sessionStorage } from 'utils/storage'
import FullLoading from 'edt-components/FullLoading'
import _ from 'lodash'
@observer
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.data = {
hasError: false,
errorType: null
}
this.clearLoadingChunk()
}
componentDidCatch(error, errorInfo) {
this.handleError(error, errorInfo)
}
clearLoadingChunk = () => {
window.clearTimeout(this.clearTimer)
this.clearTimer = window.setTimeout(() => {
sessionStorage.removeItem('loadingChunk')
}, 60000)
}
handleError = (error, errorInfo) => {
this.setData({hasError: true})
this.handleLoadingChunkError(error)
}
handleLoadingChunkError = (error, errorInfo) => {
//页面浏览期间重新发布,会出现浏览器缓存文件名和服务器文件名不一致,导致下载js失败白屏的问题
const loadFaild = new RegExp(/Loading chunk (\d)+ failed/g)
if (error && error.message && error.message.match(loadFaild)) {
this.setData({errorType: ERROR_ENUM.RE_LOADING})
let loadingChunk = sessionStorage.getItem('loadingChunk')
loadingChunk = _.isNumber(loadingChunk) ? loadingChunk + 1 : 1
if (loadingChunk > 2) {
this.setData({errorType: ERROR_ENUM.LOAD_FAIL})
//当前流程结束,重置loadingChunk
sessionStorage.setItem('loadingChunk', 0)
} else {
sessionStorage.setItem('loadingChunk', loadingChunk)
window.location.reload()
}
}
}
renderErrorContent = () => {
const {errorType} = this.data
if (errorType === ERROR_ENUM.LOAD_FAIL) {
return (
<h3>页面加载失败, 请刷新页面重新加载...</h3>
)
}
if (errorType === ERROR_ENUM.RE_LOADING) {
return (
<FullLoading/>
)
}
}
render() {
return this.data.hasError ? this.renderErrorContent() : this.props.children
}
}
//页面错误类型枚举
const ERROR_ENUM = {
LOAD_FAIL: 'LOAD_FAIL',
RE_LOADING: 'RE_LOADING'
}
export default ErrorBoundary
4.Js压缩UglifyJsPlugin
optimization: _.assignIn({}, commonConfig.optimization, {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true // set to true if you want JS source maps
})
],
})
5.Css压缩 MiniCssExtractPlugin
new MiniCssExtractPlugin({
filename: '[id]_' + config.version + '_[hash].css',
chunkFilename: '[id]_' + config.version + '_[hash].css'
}),
6.HTML压缩
plugins: [
new HtmlWebpackPlugin({
title: config.title,
template: config.template,
inject: true
})
]
7.长列表虚拟加载
使用react-virtualized 源代码结合列表代码实现虚拟加载
代码优化
1.Css自动化重构
项目中原来没有依据配色规范等,有一个公共的样式文件来对项目样式文件进行管理,如果需要支持主题更换,则需要重新提取整合项目中的css样式。 可以使用Css自动化重构工具lemonj,自动提取整合,不需要人工收集和整理,开发人员只需要修改变量名就行了。
安装
npm install lemonj -g
分析项目css代码,components为需要分析的文件名
lemonj analysis components
得到整体分析结果(坏味道代码),和两个文件mappings.less和result.json 坏味道代码:
Code Smell: {
colors: 24,
importants: 4,
issues: 8,
mediaQueries: 1,
absolute: 0,
oddWidth: 1
}
mappings.less:
@color_8: rgba(0, 0, 0, 0.1);
// components\TransferPicker\style.less:4
@color_9: #fff;
// components\ATable\style.less:171
// components\ATable\style.less:177
@color_10: #e6f7ff;
// components\ATable\style.less:284
// components\ATable\style.less:295
@color_11: #fbfdff;
// components\NavigationBar\style.less:30
@color_12: #929598;
// components\Card\style.less:45
@color_13: #F8F9FA;
修改mappings.less中的变量名,如color_8改为primary_color,保存,执行命令:
lemonj refactor componnets
项目中的所有对应样式则会改成mappings.less中对应的样式变量名
2. 复杂列表逻辑分层
原来的复杂列表代码,所有的数据查询,渲染,事件全部放在一个js中,随着功能扩展,代码量越来越大, 不易维护。 因此将列表使用高阶组件设计分层,分为数据层,事件层,渲染层。
import Data from './Data'
import Event from './Event'
import Render from './Render'
...
@Data
@Event
@Render
class List {
...
}
项目工程化
1.组件库
由于刚开始只有一个系统,后续拓展新增项目后,时间紧急就直接把组件代码复制到新项目中,但是多个系统维护一套组件很麻烦,时间长了导致组件不统一,而且迭代功能有时候需要多个系统重复修改。 因此抽离公共组件库,多个系统共用一套组件库。
2.组件库展示网站
抽取组件库之后,遇到了以下问题
- 多个系统共用一个组件库,策划方的组件UI设计没有对应示例可以查看,无法与组件库的样式和交互保持统一
- 组件库目前难以管理,组件的改动会同时影响多个系统中组件的展示和交互,开发人员对组件改动带来的影响 难以把控
- 项目中组件库使用频繁,但是查阅组件API繁琐,需要翻阅组件代码
- 无法直观全面地了解每个组件具备的功能,对新人学习成本较高,组件新增功能也无法快速让组内其他人了解
- 组件测试
因此参考bisheng ,打造一个组件库文档网站,以解决上述问题。
使用框架:bisheng
参考链接:文档工具bisheng的使用
3.单元测试
抽离组件库后,必须增加单元测试。
jest
是facebook推出的一个前端测框架,搭配enzyme
可以轻松实现对React应用的单元测试
4.前端监控
使用Sentry进行前端监控
参考链接:Sentry部署实践
5.自定义CLI工具
- 多个项目分别管理自己的webpack配置,webpack进行bug修复或升级更迭时需要多个项目分别修改,重复劳动,难以维护,易导致项目构建版本和配置差异。创建webpack-cli统一构建流程及构建产物,减少项目更新迭代的重复劳动。
- 每次新建项目,都需要复制粘贴,删除多余文件,修改项目个性化配置及通用交互等,操作重复麻烦,易遗漏部分相关修改。创建create-cli自定义脚手架支持项目初始化使用命令行自动生成项目模板
参考链接:NodeCli工具实践
6.css模块化插件
平时写代码的过程中偶尔会遇到样式被其他模块样式覆盖的问题,react方面目前市面上一直没有比较好的解决办法。参考vue对css scoped的处理方法,创建了css作用域插件scoped-css-plugin。支持对class进行转换,在样式名后增加hash值来达到css作用域的目的。
- 转换规则
- 以组件文件夹名称__dirname为准,检查文件夹内index.js和style.less
- 根据文件夹路径生成相应的hash
- index.js中发现匹配__dirname的className,则在该className后新增className-css-hash-[hash]的类名
- style.less中发现匹配__dirname的className,则将该className转换为className-css-hash-[hash]
进行转换之后,js中保留了原className供外部定制css,组件内样式将依据hash划定到作用域内(没有增加css选择器优先级)
待优化
- 公共组件优化(表单配置化,表单拓展,部分组件功能拓展)