通常,一个中高级前端工程师,除了要完成业务功能开发目标外,还要对所开发项目的效率、性能、质量等工程化维度去制定和实施技术优化目标,其中以提升效率为目标的优化技术和工具就属于效率工程化的范畴。 对于公司而言,团队效率可以直接带来人工投入产出比的提升,因此效率提升通常会被作为技术层面的一个重点优化方向。而在面试中,对效率工程化的理解程度和实践中的优化产出情况,也是衡量前端工程师能力高低的常见标准。
当然上面的话都是官方话语,实际就是最近一段时间在搞个人的前端基建生态,包括一些项目模板,项目规范,组件库,工具函数库,以及脚手架等,所以就想用一系列文章来记录一下。好记性不如烂笔头,好好做笔记才是对将来的自己负责,我是深有感受的。
1.初始化项目
1.1.初始化package.json
在开始webpack配置之前,先手动初始化一个基本的React+TypeScript项目,新建项目文件夹 xyl-react-template , 在项目根目录下使用 npm init -y 生成package.json文件。
npm init -y
可以看到在项目根目录下,生成了一个package.json文件;接下来就需要确定项目的目录结构。
1.2.初始项目目录结构
├── config
| ├── webpack.base.js # 公共配置
| ├── webpack.dev.js # 开发环境配置
| └── webpack.prod.js # 打包环境配置
├── public
│ └── index.html # html模板
├── src
| ├── App.tsx
│ └── index.tsx # react应用入口页面
├── tsconfig.json # ts配置
└── package.json
- config:存放webpack编译配置文件
- src:源码目录
- public:静态文件托管目录
1.3.初始react配置
安装react,react-dom依赖
npm i react react-dom -S
安装 @types/react 和 @types/react-dom 依赖
npm i @types/react @types/react-dom -D
添加public/index.html内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xyl-react-template</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
添加tsconfig.json内容
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules/**"
]
}
添加src/App.tsx内容
import React from 'react'
function App() {
return <h2>xyl-react-template</h2>
}
export default App
添加src/index.tsx内容
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = document.getElementById('root');
if(root) {
createRoot(root).render(<App />)
}
接下来,我们需要使用 webpack 将我们的源码打包。
2.webpack配置
2.1.webpack基础配置
首先安装依赖 webpack 和 webpack-cli
npm install webpack webpack-cli -D
项目入口出口配置
在webpack.base.js文件中添加项目的入口出口等基本配置
// webpack.base.js
const path = require("path");
module.exports = {
mode: "production",
entry: path.resolve(__dirname, "../src/index.tsx"),
output: {
path: path.resolve(__dirname, "../dist"),
filename: "static/js/[name].[chunkhash:8].js",
publicPath: "/", // 打包后文件的公共前缀路径
},
};
js相关解析配置
由于webpack默认只能识别js文件,不能识别jsx语法,需要配置loader的预设,借助预设 @babel/preset-react 来识别jsx语法;但是由于我们还是用了ts语法,所以我们要先借助 @babel/preset-typescript 来将ts语法转换为 js 语法。
安装babel核心模块和babel预设
npm i babel-loader @babel/preset-react @babel/core @babel/preset-react @babel/preset-typescript core-js -D
在webpack.base.js添加module.rules配置
// webpack.base.js
module.exports = {
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/, // 匹配.ts, tsx文件
use: ["babel-loader"]
}
]
}
}
在项目根目录下创建文件babel.config.js,将babel解析相关配置添加到文件中,如下:
module.exports = {
// 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
presets: [
[
'@babel/preset-env',
{
// 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
// "targets": {
// "chrome": 35,
// "ie": 9
// },
useBuiltIns: 'usage', // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
corejs: 3 // 配置使用core-js使用的版本
}
],
'@babel/preset-react',
'@babel/preset-typescript'
],
};
- 配置
extensions(解析文件扩展名)
extensions是webpack的resolve解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为ts不支持引入以 .ts, .tsx为后缀的文件,所以要在extensions中配置,而第三方库里面很多引入js文件没有带后缀,所以也要配置下js。
修改webpack.base.js文件配置
// webpack.base.js
module.exports = {
// ...
resolve: {
extensions: ['.tsx', '.ts','.js', '.jsx']
}
}
html模板文件管理配置
通过html-webpack-plugin可以简化 html 文件的创建,以便为webpack提供服务。
安装依赖:
npm i html-webpack-plugin -D
这个插件会生成一个html文件,在body中使用script标签自动引入你所有webpack生成的bundle,只需要添加这个插件到webpack的plugins中,如下:
// webpack.base.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'), // 模板取定义root节点的模板
inject: true, // 自动注入静态资源
})
]
}
修改package.json文件配置:
"scripts": {
"build": "webpack -c config/webpack.base.js"
},
执行npm run build,会发现此时 dist 目录中就多了index.html文件,并且还自动帮我们引入了打包后的文件模块,打包后的dist文件可以在本地借助node服务器serve打开,全局安装serve
npm i serve -g
然后在项目根目录命令行执行serve -s dist,就可以启动打包后的项目了。
样式文件解析配置
对于样式文件的解析,webpack本身也是不支持的,但是提供了loader让我们去配置使用解析能力。
安装依赖:
npm install style-loader css-loader less-loader postcss-loader postcss autoprefixer --save-dev
- less-loader:将less编译为css
- css-loader:对@import和url()进行处理,就像js解析import/require()一样。
- style-loader:将css插入到DOM中
- postcss-loader,postcss和autoprefixer一起处理css浏览器兼容问题。
配置如下:
rules: [
//...
{
test: /.css$/,
include: [path.resolve(__dirname, "../src")],
use: ["style-loader", "css-loader","postcss-loader"],
},
{
test: /.less$/,
include: [path.resolve(__dirname, "../src")],
use: ["style-loader", "css-loader","postcss-loader", "less-loader"],
},
],
在项目根目录下,创建文件postcss.config.js,添加如下配置:
module.exports = {
plugins: [require("autoprefixer")],
};
在项目根目录下执行npx browserslist,这个,命令是查看默认条件下筛选出来的一些浏览器(默认条件就是:市场占有率大于百分之五,并且没有死掉,如果这个浏览器版本24个月没有更新,那么就相当于死掉)
在项目根目录下,创建配置文件.browserslistrc文件,这个文件是用来筛选需要兼容的浏览器的,我们自定义我们自己的配置
IE 9 # 兼容IE 9
chrome 35 # 兼容chrome 35
重新执行npx browserslist,可以看到跟默认的列表不一样了,这就表明我们的配置生效了。
现在在src目录下添加App.less文件,并添加以下代码:
#root {
h2 {
font-size: 20px;
transform: translateY(100px);
}
}
在App.tsx文件中引入样式文件,重新编译,查看浏览器。可以看到对于css3的新特性都自动加上了浏览器兼容前缀。
静态资源解析配置
静态资源解析配置的基本策略就是,当文件小于最大体积的时候,就采用bse64的方式将静态文件转成base64打包到代码中,否则就会直接被生成到输出的构建产物目录中。
webpack5自身已经支持静态资源解析,不需要额外的loader,配置如下:
- 处理图片文件
修改webpack.base.js,添加图片解析配置;
module.exports = {
module: {
rules: [
// ...
{
test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 20 * 1024, // 小于20kb转base64位
}
},
generator:{
filename:'static/images/[name][ext]', // 文件输出目录和命名
},
},
]
}
}
测试:准备一张小于20kb和大于20kb的图片,放在src/assets/imgs目录下, 修改App.tsx:
import React from "react";
import "./App.less";
import sml from './assets/imgs/sml.jpeg'
import big from './assets/imgs/big.png'
function App() {
return (
<>
<h2>xyl-react-template</h2>
<img src={sml} alt="小于20kb的图片" />
<img src={big} alt="大于于20kb的图片" />
</>
);
}
export default App;
这个时候在引入图片的地方会报错
这是因为ts语法不支持这些图片类型,我们需要添加一个图片的声明文件。
新增 src/images.d.ts 文件,添加内容;
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
declare module '*.less'
declare module '*.css'
添加图片声明文件后,就可以正常引入图片了, 然后执行npm run build打包,借助serve -s dist查看效果,可以看到可以正常解析图片了,并且小于 20kb 的图片被转成了 base64 位格式的。如下图:
- 处理字体和媒体文件
webpack.base.js文件:
// webpack.base.js
module.exports = {
module: {
rules: [
// ...
{
test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 20 * 1024, // 小于20kb转base64位
}
},
generator:{
filename:'static/fonts/[name][ext]', // 文件输出目录和命名
},
},
{
test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
type: "asset", // type选择asset
parser: {
dataUrlCondition: {
maxSize: 20 * 1024, // 小于20kb转base64位
}
},
generator:{
filename:'static/media/[name][ext]', // 文件输出目录和命名
},
},
]
}
}
配置环境变量
区分开发环境还是生产环境,可以用 process.env.NODE_ENV ,设置环境变量可以借助cross-env和webpack.DefinePlugin来设置。
- cross-env:兼容各系统的设置环境变量的包
- webpack.DefinePlugin:webpack 内置的插件,可以为业务代码注入环境变量
安装cross-env
npm i cross-env -D
修改package.json的scripts脚本字段,改为如下配置:
"scripts": {
"build": "cross-env NODE_ENV=production webpack -c config/webpack.base.js"
},
webpack 会自动根据设置的 mode字段来给process.env.NODE_ENV 环境变量注入对应的 development 和 production ,这里在命令中再次设置环境变量 NODE_ENV 是为了在 webpack 和 babel 的配置文件中访问到。
在webpack.base.js中打印一下设置的环境变量
// webpack.base.js
// ...
console.log('NODE_ENV', process.env.NODE_ENV)
执行 npm run build ,可以看到打印的信息
这里需要把 process.env.NODE_ENV 注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和其他数据,要借助webpack.DefinePlugin插件
修改 webpack.base.js
// webpack.base.js
// ...
const webpack = require('webpack')
module.export = {
// ...
plugins: [
// ...
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
]
}
配置后会把值注入到业务代码里面去
在 src/App.tsx 打印一下环境变量
// src/App.tsx
// ...
console.log('NODE_ENV', process.env.NODE_ENV)
2.2.webpack开发环境配置
webpack-dev-server配置
开发环境配置代码在webpack.dev.js中,通过webpack-dev-server的这些配置提供一个本地服务,还需要依赖webpack-merge来合并公共配置。
npm i webpack-dev-server webpack-merge -D
修改webpack.base.js,删除mode属性。
修改webpack.dev.js代码, 合并公共配置,并添加开发环境配置。
// webpack.dev.js
const path = require('path')
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
open: true,//告诉 dev-server 在服务器已经启动后打开浏览器。设置其为 `true` 以打开你的默认浏览器
port: 3000, // 服务端口号
compress: false, // gzip压缩,开发环境不开启,提升热更新速度
hot: true, // 开启热更新,后面会讲react模块热替换具体配置
proxy: {
'/api': 'http://localhost:8080', //服务代理配置
},
static: {
directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
publicPath: '/', //告诉服务器在哪个 URL 上提供static.directory的内容
}
}
})
修改package.json文件配置:
"scripts": {
"build": "cross-env NODE_ENV=production webpack -c config/webpack.base.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server -c config/webpack.dev.js"
},
执行npm run dev,就能看到项目已经启动起来了,访问 http://localhost:3000/ 。
模块热替换
热更新上面在 devServer 中配置hot为true就已经开启了。
现在开发模式下修改css和less文件,页面样式可以在不刷新浏览器的情况实时生效,因为此时样式都在style标签里面,因为style-loader做了替换样式的热替换功能。但是修改App.tsx,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。
可以借助@pmmmwh/react-refresh-webpack-plugin插件来实现,该插件又依赖于react-refresh, 安装依赖:
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
配置react热更新插件,修改webpack.dev.js
// webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = merge(baseConfig, {
// ...
plugins: [
new ReactRefreshWebpackPlugin(), // 添加热更新插件
]
})
为babel配置react-refesh刷新插件,修改babel.config.js文件
const isDEV = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
// ...
"plugins": [
isDEV && require.resolve('react-refresh/babel'), // 如果是开发模式,就启动react热更新插件
// ...
].filter(Boolean) // 过滤空值
}
source-map配置
Devtool 选项可以控制是否生成,以及如何生成 source map。
- 对于开发环境
以下选项非常适合开发环境:
eval - 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。
eval-source-map - 每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
eval-cheap-source-map - 类似 eval-source-map,每个模块使用 eval() 执行。这是 "cheap(低开销)" 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。
eval-cheap-module-source-map - 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。
我们通常将开发环境的devtool取值设为
eval-cheap-module-source-map,该配置能保留预处理前的原始代码信息,并且打包速度也不慢,是最佳选择。
- 对于生产环境
这些选项通常用于生产环境中,生产环境通常不使用source-map,因为source-map会有泄漏原始代码的风险。
(none)(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。
devtool的命名规则为 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
| 关键字 | 描述 |
|---|---|
| inline | 代码内通过 dataUrl 形式引入 SourceMap |
| hidden | 生成 SourceMap 文件,但不使用 |
| eval | eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap |
| nosources | 不生成 SourceMap |
| cheap | 只需要定位到行信息,不需要列信息 |
| module | 展示源代码中的错误位置 |
修改webpack.dev.js,添加devtool为 'eval-cheap-module-source-map'。
const path = require("path");
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.js");
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = merge(baseConfig, {
mode: "development",
devtool: "eval-cheap-module-source-map"
///...
});
2.3.webpack生产环境配置
修改package.json文件配置:
"scripts": {
"build": "cross-env NODE_ENV=production webpack -c config/webpack.prod.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server -c config/webpack.dev.js"
},
修改webpack.prod.js代码
// webpack.prod.js
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.js");
module.exports = merge(baseConfig, {
mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
})
执行npm run build,最终打包在dist文件中, 打包结果:
dist
├── static
| ├── js
| ├── main.js
├── index.html
清除打包文件
由于遗留了之前的构建产物,我们的 /dist 文件夹显得相当杂乱。webpack 将生成文件并放置在 /dist 文件夹中,但是它不会追踪哪些文件是实际在项目中用到的。
通常比较推荐的做法是,在每次构建前清理 /dist 文件夹,这样只会生成用到的文件。让我们使用 output.clean 配置项实现这个需求。
// webpack.base.js
output: {
///...
clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
},
复制public文件夹
一般public文件夹都会放一些静态资源,可以直接根据绝对路径引入,不需要webpack进行解析,只需要打包的时候把public下内容复制到构建目录中,可以借助copy-webpack-plugin插件。
安装依赖
npm i copy-webpack-plugin -D
开发环境已经在 devServer 中配置了 static 托管了 public 文件夹,在开发环境使用绝对路径可以访问到 public 下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件webpack.prod.js中新增 copy 插件配置。
// webpack.prod.js
// ..
const path = require('path')
const CopyPlugin = require('copy-webpack-plugin');
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.js");
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// 复制文件插件
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'), // 复制public下文件
to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
filter: source => {
return !source.includes('index.html') // 忽略index.html
}
},
],
}),
]
})
在上面的配置中,忽略了index.html,因为html-webpack-plugin会以 public 下的index.html为模板生成一个index.html到 dist 文件下,所以不需要再复制该文件了。
测试一下,在 public 中新增一个favicon.ico图标文件,在index.html中引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xyl-react-template</title>
</head>
<body>
<!-- 容器节点 -->
<div id="root"></div>
</body>
</html>
再执行npm run build打包,就可以看到 public 下的 favicon.ico 图标文件被复制到 dist 文件中了。
2.4.优化构建速度
构建耗时分析
当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而 speed-measure-webpack-plugin 插件可以帮我们做到,安装依赖:
npm i speed-measure-webpack-plugin -D
使用的时候为了不影响到正常的开发/打包模式,我们选择新建一个配置文件,新增 webpack 构建分析配置文件config/webpack.analy.js
const prodConfig = require('./webpack.prod.js') // 引入打包配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件
const { merge } = require('webpack-merge') // 引入合并webpack配置方法
// 使用smp.wrap方法,把生产环境配置传进去
module.exports = smp.wrap(merge(prodConfig, {
}))
修改package.json添加启动webpack打包分析脚本命令,在scripts新增:
{
// ...
"scripts": {
// ...
"analy": "cross-env NODE_ENV=production webpack -c config/webpack.analy.js"
}
// ...
}
执行npm run analy命令,可以看到各plugin和loader的耗时时间,现在因为项目内容比较少,所以耗时都比较少,在真正的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化。
开启持久化缓存
webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,通过配置 webpack 持久化缓存,来缓存生成的 webpack 模块和 chunk,改善下一次打包的构建速度,可提速 90% 左右,配置也简单。
修改webpack.base.js
// webpack.base.js
// ...
module.exports = {
// ...
cache: {
type: 'filesystem', // 使用文件缓存
},
}
当前文章代码的测试结果
| 模式 | 第一次耗时 | 第二次耗时 |
|---|---|---|
| 启动开发模式 | 2869毫秒 | 687毫秒 |
| 启动打包模式 | 5455毫秒 | 552毫秒 |
通过开启webpack5持久化存储缓存,再次打包的时间提升了90% 。
缓存的存储位置在node_modules/.cache/webpack,里面又区分了development和production缓存
开启多线程loader
webpack的loader默认在单线程执行,现代电脑一般都有多核cpu,可以借助多核cpu开启多线程loader解析,可以极大地提升loader解析的速度,thread-loader就是用来开启多进程解析loader的,安装依赖
npm i thread-loader -D
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
修改webpack.base.js
// webpack.base.js
module.exports = {
// ...
module: {
rules: [
{
test: /.(ts|tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
开启多线程也是需要启动时间,大约600ms左右,所以适合规模比较大的项目。
缩小loader作用范围
一般第三库都是已经处理好的,不需要再次使用loader去解析,可以按照实际情况合理配置loader的作用范围,来减少不必要的loader解析,节省时间,通过使用 include和exclude 两个配置项,可以实现这个功能,常见的例如:
- include:只解析该选项配置的模块
- exclude:不解该选项配置的模块,优先级更高
修改webpack.base.js
// webpack.base.js
const path = require('path')
module.exports = {
// ...
module: {
rules: [
{
include: [path.resolve(__dirname, '../src')], 只对项目src文件的ts,tsx进行loader解析
test: /.(ts|tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
其他loader也是相同的配置方式,如果除src文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入antd的css,可以把antd的文件目录路径添加解析css规则到include里面。
缩小模块搜索范围
node里面模块有三种
- node核心模块
- node_modules模块
- 自定义文件模块
使用require和import引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径,会优先查询node核心模块,如果没有找到会去当前目录下node_modules中寻找,如果没有找到会查从父级文件夹查找node_modules,一直查到系统node全局模块。
这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下node_modules或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。可以告诉webpack搜索目录范围,来规避这两个问题。
修改webpack.base.js
// webpack.base.js
const path = require('path')
module.exports = {
// ...
resolve: {
// ...
modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找
},
}
配置文件alias别名
webpack支持设置别名alias,设置别名可以让后续引用的地方减少路径的复杂度。
修改webpack.base.js
module.export = {
// ...
resolve: {
// ...
alias: {
'@': path.join(__dirname, '../src')
}
}
}
修改tsconfig.json,添加baseUrl和paths
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}
配置修改完成后,在项目中使用 @/xxx.xx,就会指向项目中src/xxx.xx, 在 js/ts文件和css文件中都可以用。
src/App.tsx可以修改为
import React from "react";
import "./App.less";
import sml from '@/assets/imgs/sml.jpeg'
import big from '@/assets/imgs/big.png'
console.log("ssss1",process.env.NODE_ENV)
function App() {
return (
<>
<h2>xyl-react-template</h2>
<img src={sml} alt="小于20kb的图片" />
<img src={big} alt="大于于20kb的图片" />
</>
);
}
export default App;
2.5.优化构建结果文件
webpack包分析工具
webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:
npm install webpack-bundle-analyzer -D
修改 webpack.analy.js
// webpack.analy.js
const prodConfig = require('./webpack.prod.js')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const { merge } = require('webpack-merge')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
module.exports = smp.wrap(merge(prodConfig, {
plugins: [
new BundleAnalyzerPlugin() // 配置分析打包结果插件
]
}))
配置好后,执行npm run analy命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。
抽取css样式文件
在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但生产环境时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖:
npm i mini-css-extract-plugin -D
修改webpack.base.js, 根据环境变量设置开发环境使用style-looader,打包模式抽离css。
// webpack.base.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.css$/, //匹配所有的 css 文件
include: [path.resolve(__dirname, '../src')],
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
'css-loader',
'postcss-loader'
]
},
{
test: /.less$/, //匹配所有的 less 文件
include: [path.resolve(__dirname, '../src')],
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css
'css-loader',
'postcss-loader',
'less-loader'
]
},
]
},
// ...
}
再修改webpack.prod.js, 打包时添加抽离css插件
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// ...
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].css' // 抽离css的输出目录和名称
}),
]
})
配置完成后,在开发模式css会嵌入到style标签里面,方便样式热替换,打包时会把css抽离成单独的css文件。
资源压缩
资源压缩主要是对js和css文件进行压缩,常用的方式有把整个文件或大段代码压缩成一行,把较长的变量名替换成较短的变量名,移除空格与空行等。
资源压缩的主要目的是减小文件体积,提升页面加载速度和降低带宽消耗等。资源压缩通常发生在生产环境打包的最后一个环节,本地开发环境不需要进行压缩处理。
压缩css文件
上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。
可以借助css-minimizer-webpack-plugin来压缩css,安装依赖
npm i css-minimizer-webpack-plugin -D
修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置
// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// ...
optimization: {
minimize:true,
minimizer: [
new CssMinimizerPlugin(), // 压缩css
],
},
}
再次执行打包就可以看到css已经被压缩了
压缩js文件
设置mode为production时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件。
修改webpack.prod.js文件
// ...
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
// ...
optimization: {
minimiz:true,
minimizer: [
// ...
new TerserPlugin({ // 压缩js
parallel: true, // 开启多线程压缩
terserOptions: {
compress: {
pure_funcs: ["console.log"] // 删除console.log
}
}
}),
],
},
}
注意:只有当optimization.minimize的值为true时,webpack才会使用
optimization.minimizer里配置的压缩器进行压缩。
配置完成后再打包,css和js就都可以被压缩了
gzip压缩
前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。
nginx可以配置gzip: on来开启压缩,但是只在nginx层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu。
webpack可以借助compression-webpack-plugin插件在打包时生成 gzip 文章,安装依赖
npm i compression-webpack-plugin -D
添加配置,修改webpack.prod.js
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
// ...
plugins: [
// ...
new CompressionPlugin({
test: /.(js|css)$/, // 只生成css,js压缩文件
filename: '[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式,默认是gzip
test: /.(js|css)$/, // 只生成css,js压缩文件
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8 // 压缩率,默认值是 0.8
})
]
}
配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件。
代码分割
代码分割是webpack优化中非常重要的一部分,webpack中主要有三种方法进行代码分割。
- 入口entry:配置entry入口文件,从而手动分割 代码。
- 动态加载:通过import等方法进行按需加载。
- 抽取公共代码:使用splitChunks等技术抽取公共代码。
我们一般使用的比较多的就是splitChunks抽取公共代码。
optimization.splitChunks
代码分割非常重要的一项技术就是optimization.splitChunks,splitChunks指的是webpack插件SplitChunksPlugin,在webpack的配置项optimization.splitChunks里直接配置即可,无须单独安装。
在webpack4之前,webpack是通过CommonsChunkPlugin插件来抽取公共代码,webpack4之后使用的是SplitChunksPlugin插件,在webpack5又对其进行了优化。
splitChunks的宗旨是通过一定的规则实现模块的自动提取。
一般第三方包的代码变化频率比较小,可以单独把node_modules中的代码单独打包, 当第三包代码没变化时,对应chunkhash值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积,配置如下:
修改webpack.prod.js
module.exports = {
// ...
optimization: {
// ...
splitChunks: { // 分隔代码
cacheGroups: {
vendors: { // 提取node_modules代码
test: /node_modules/, // 只匹配node_modules里面的模块
name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
commons: { // 提取页面公共代码
name: 'commons', // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
}
}
}
}
}
配置完成后执行打包,可以看到生成了3个js文件,node_modules 里面的模块被抽离到verdors.xxx.js中,其他公共代码被抽离到commons.xxx.js中,业务代码在main.xxx.js中。
摇树优化 Tree Shaking
Tree Shaking可以帮我们检测模块中没有用到的代码块,并且在webpack打包时将没有用到的代码块移除,减小打包后的资源体积。
当我们没有配置摇树机制时,对于一个文件中引入的模块但是没有使用到的,webpack依然会将其引入,这时候我们就需要配置Tree Shaking来移除这部分没有使用的代码
使用Tree Shaking需要两个步骤:
- 标注未使用的代码
- 对未使用的代码进行删除
对于开发环境我们只需要配置:
const path = require('path');
//...
const TerserPlugin = require('terser-webpack-plugin');
//...
optimization: {
usedExports: true,
minimize: true,
minimizer: [new TerserPlugin()]
}
- usedExports:用来标记未使用的代码。
- TerserPlugin:webpack5自带插件,用来清除未使用的代码。
通常我们在开发环境是不会使用Tree Shaking的,因为他会降低构建速度并且对于开发环境也没有太大的意义;而在生产环境,只要配置参数mode为production就会自动开启Tree Shaking。
开启了Tree Shaking后,webpack会在打包时候删除大部分没有使用到的代码,但是有一些代码没有被其他模块导入使用,如polyfill.js,它主要用来扩展全局变量,这类代码是有副作用的代码,我们需要告诉webpack在Tree Shaking时不能删除它们。
要告诉webpack在Tree Shaking时不能删除某些文件,可以在package.json文件里使用sideEffects配置,如:
{
"sideEffects":[
"./polyfill.js"
]
}
如果sideEffects的值为false就表示所有的代码都没有副作用可以安全的删除。
PurgeCSS清理未使用css
js中会有未使用到的代码,css中也会有未被页面使用到的样式。PurgeCSS 是一个用来删除未使用的 CSS 代码的工具。PurgeCSS 通过分析你的内容和 CSS 文件,首先它将 CSS 文件中使用的选择器与内容文件中的选择器进行匹配,然后它会从 CSS 中删除未使用的选择器,从而生成更小的 CSS 文件。
这个插件是和mini-css-extract-plugin插件配合使用的,此外还需要glob-all来选择要检测哪些文件里面的类名和 id 还有标签名称,mini-css-extract-plugin已经安装过了,不用重复安装了。
安装依赖:
npm i purgecss-webpack-plugin glob-all -D
修改webpack.prod.js
/ webpack.prod.js
// ...
const globAll = require('glob-all')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
// ...
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css'
}),
// 清理无用css
new PurgeCSSPlugin({
// 检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称
// 只打包这些文件中用到的样式
paths: globAll.sync([
`${path.join(__dirname, '../src')}/**/*.tsx`,
path.join(__dirname, '../public/index.html')
]),
}),
]
}
资源懒加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
像react等单页应用打包默认会打包到一个js文件中,虽然使用代码分割可以把node_modules模块和公共模块分离,但页面初始加载还是会把整个项目的代码下载下来,其实只需要公共资源和当前页面的资源就可以了,其他页面资源可以等使用到的时候再加载,(比如与用户交互的时候才需要加载的某些资源)可以有效提升首屏加载速度。
webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。
以懒加载组件和css为例,新建懒加载组件src/components/LazyDemo.tsx
import React from "react";
function LazyDemo() {
return <h3>我是懒加载组件组件</h3>
}
export default LazyDemo
修改App.tsx
import React, { lazy, Suspense, useState } from 'react'
const LazyDemo = lazy(() => import('@/components/LazyDemo')) // 使用import语法配合react的Lazy动态引入资源
function App() {
const [ show, setShow ] = useState(false)
// 点击事件中动态引入less, 设置show为true
const onClick = () => {
import('./App.less')
setShow(true)
}
return (
<>
<h2 onClick={onClick}>展示</h2>
{/* show为true时加载LazyDemo组件 */}
{ show && <Suspense fallback={null}><LazyDemo /></Suspense> }
</>
)
}
export default App
点击展示文字时,才会动态加载app.css和LazyDemo组件的资源。
资源预加载
上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助link标签的rel属性prefetch与preload,link标签除了加载css之外也可以加载js资源,设置rel属性可以规定link提前加载资源,但是加载资源后不执行,等用到了再执行。
rel的属性值
- preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
- prefetch是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源,会在空闲时加载。
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。
对于当前页面很有必要的资源使用 preload ,对于可能在将来的页面中使用的资源使用 prefetch。
webpack v4.6.0+ 增加了对预获取和预加载的支持,使用方式也比较简单,在import引入动态资源时使用webpack的魔法注释
// 单个目标
import(
/* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname
/* webpackPrefetch: true */ // 开启prefetch预获取
/* webpackPreload: true */ // 开启preload预获取
'./module'
);
懒加载、预加载、正常加载的区别
- 懒加载:当文件需要用到的时候才会去加载,不用的时候不加载。
- 正常加载:正常的时候是并发去加载,但是一般受到6个个数的限制。
- 预加载:等其他资源加载完毕后,浏览器空闲了,再加载资源,但是不会运行这个文件。
选择文件hash类型
项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack打包的hash分三种:
- hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值
- chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改,chunkhash值会变化
- contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash 值
hash是在输出文件时配置的,格式是filename: "[name].[chunkhash:8][ext]" , [xx] 格式是webpack提供的占位符, :8是生成hash的长度。
| 占位符 | 解释 |
|---|---|
| ext | 文件后缀名 |
| name | 文件名 |
| path | 文件相对路径 |
| folder | 文件所在文件夹 |
| hash | 每次构建生成的唯一 hash 值 |
| chunkhash | 根据 chunk 生成 hash 值 |
| contenthash | 根据文件内容生成hash 值 |
因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以js适合使用chunkhash。
css和图片资源媒体资源一般都是单独存在的,可以采用contenthash,只有文件本身变化后会生成新hash值。
修改webpack.base.js,把js输出的文件名称格式加上chunkhash,把css和图片媒体资源输出格式加上contenthash
// webpack.base.js
// ...
module.exports = {
// 打包文件出口
output: {
filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
// ...
},
module: {
rules: [
{
test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
// ...
generator:{
filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
},
},
{
test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
// ...
generator:{
filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
},
},
{
test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
// ...
generator:{
filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
},
},
]
},
// ...
}
再修改webpack.prod.js,修改抽离css文件名称格式
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
}),
// ...
],
// ...
})
再次打包就可以看到文件后面的hash了。
3.项目规范配置
3.1.配置eslint和prettier
第三方依赖
- eslint
- prettier
- eslint-config-prettier
- eslint-plugin-prettier
- eslint-plugin-react
- @typescript-eslint/eslint-plugin
- @typescript-eslint/parser
在vscode插件市场下载两个插件,ESLint和Prettier,下载安装好。
在项目根目录下执行npx eslint --init或者npm init @eslint/config,然后根据提示选择配置
装好依赖后,eslint就生效了,由于我们还需要使用prettier对代码格式进行美化,所以我们还需要安装prettier相关依赖。
npm install prettier eslint-config-prettier eslint-plugin-prettier
eslint-config-prettier 和eslint-plugin-prettier这两个依赖是用于解决prettier规则和eslint规则冲突的,缺一不可。
- eslint-config-prettier 的作用是关闭eslint中与prettier相互冲突的规则。
- eslint-plugin-prettier 的作用是赋予eslint用prettier格式化代码的能力
在项目根目录创建.prettierrc文件,配置如下(这个配置根据自己的团队实际需要配置):
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": true
}
在.eslintrc.js文件中修改配置,继承prettier的格式规则
//...
extends: [
//...
"plugin:prettier/recommended",
]
//...
在package.json中配置script脚本命令,
"script":{
"eslint":"eslint src/**/*.{js,jsx,ts,tsx} --fix",
//...
}
在终端执行npm run eslint 可以发现有问题的格式,有的已经被自动修复,需要手动修复的文件也已经提示出来了。我们按照文件逐个修复就OK,但是eslint只能够检测出语法的问题,是无法检测到ts的类型错误的,所以我们还需要通过另一个工具校验ts类型错误的。
3.2.配置typescript类型校验
在package.json文件的script脚本中配置:
"scripts": {
"ts-check": "tsc",
}
在终端执行脚本npm run ts-check,如果存在ts类型错误就会被抛出来
虽然目前可以校验语法错误,但是实际上我们还是可以把错误的语法格式提交到远程仓库,所以我们还需要在提交代码的时候去校验语法是否符合通过,如果不通过则不允许提交。
3.3.配置husky和lint-staged
安装lint-staged
npm i lint-staged -D
修改package.json中eslint的配置
"eslint": "tsc&&eslint"
修package.json中添加lint-staged配置
"lint-staged": {
"src/**/*.{ts,tsx,js,jsx}": [
"npm run eslint"
]
},
因为要检测git暂存区代码,所以需要执行git init初始化一下git, git init
然后将有语法问题的代码提交到暂存区,如App.tsx,然后执行npx lint-staged
然后安装husky,在git钩子函数触发时校验语法
npm install husky -D
配置pre-commit钩子
生成 .husky配置文件夹
npx husky install
会在项目根目录生成 .husky文件夹,生成文件成功后,需要让husky支持监听pre-commit钩子,监听到后执行上面定义的npm run eslint语法检测。
npx husky add .husky/pre-commit 'npm run eslint'
然后提交代码进行测试
然后发现被拦截了
修正语法,重新提交,成功commit
使用husky和commitlint规范commit备注信息, commitlint文档
npm install --save-dev @commitlint/config-conventional @commitlint/cli
在根目录创建commitlint.config.js文件,继承@commitlint/config-conventional的规则,并且自定义comit备注规则,配置如下:
module.exports = {
// 继承的规则
extends: ['@commitlint/config-conventional'],
// 定义规则类型
rules: {
// type 类型定义,表示 git 提交的 type 必须在以下类型范围内
'type-enum': [
2,
'always',
[
'feat', // 新功能 feature
'fix', // 修复 bug
'docs', // 文档注释
'style', // 代码格式(不影响代码运行的变动)
'refactor', // 重构(既不增加新功能,也不是修复bug)
'perf', // 性能优化
'test', // 增加测试
'chore', // 构建过程或辅助工具的变动
'revert', // 回退
'build' // 打包
]
],
// subject 大小写不做校验
'subject-case': [0]
}
}
添加hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
在.husky目录下查看,新生成了commit-msg文件
测试提交备注信息
git commit -m "eeee"
根据commitlint规则,修正提交信息,重新commit,commit成功。到这里基本的规范就完成了,还有stylelint,没有使用,如果需要可以参考stylelint官网自行配置。
项目模板源码可参考xyl-react-template。
如果有笔误,或者对于某些知识理解不到位的地方,也希望大家能不吝赐教!