序
接上篇,继续完善项目的构建。
开发速度优化
避免缓存干扰,加入 hash
目前我们生成的上篇文章中,css 文件输出 filename:'[name].[hash:8].css',
但 js 固定输出为 bundle.js。
项目部署到服务器上以后,为了提高加载速度,通常会使用一种叫做 缓存 的技术。如果使用资源输出的名字没有更改,浏览器加载资源时,可能会认为文件并没有更新,从而读取缓存中的文件。
根据 webpack 官方文档,我们采用 filename: '[name].[chunkhash].js'。css 也做相应的改变。
运行 npm run build 打包,得到如下结果:
我们再打包一次,得到如下结果:
此时我们发现,已经生成了新的 css 和 js。但是新的问题也出现了,由于名字变更,目录下上次的打包结果并没有被覆盖,导致了冗余文件的出现。能不能在打包新的文件之前,清空现有文件内容呢?
自动清空打包目录
npm install clean-webpack-plugin -D
在 webpack.config.js 中写入:
...
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
...
plugins:[ // 配置插件
new CleanWebpackPlugin() ,// 清空打包目录
...
],
这样,我们每次都只会得到最新的打包内容。
优化构建速度,提高开发效率
现在项目的文件比较少,打包时间短。如果项目变得庞大,那么我们必须通过配置,减少构建时间,提高开发效率,不然的话,很容因为打包时间过长,忘了本来要干啥。
我们怎么确认我们的配置有没有成功的减少配置时间呢?
安装打包速度监控插件
npm i speed-measure-webpack-plugin -D
在 webpack 中写入。
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");、
const smp = new SpeedMeasurePlugin();
...
module.exports = smp.wrap({...})// 将现有的所以配置包裹起来。
此时运行,会提示我 You forgot to add 'mini-css-extract-plugin' plugin 忘记添加插件了,参考 🔥【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系 - 掘金 (juejin.cn) 得到解决方案,降低插件的版本。
npm uninstall mini-css-extract-plugin // 卸载
npm install mini-css-extract-plugin@1.3.6 -D // 重新安装新的版本运行
此时得到文件的打包时间:
设定构建匹配文件范围
在 webpack.config.js 的 module 配置下面,指定构建的目标文件位置,缩小检索范围
{
test: /\.(jsx|js)$/,
use:
[{
loader:'babel-loader',
}],
include:[path.resolve(__dirname, 'src')]
},
{
...
include:[path.resolve(__dirname, 'src')]
},
{
...
include:[path.resolve(__dirname, 'src/assets')]
}
还有一些其他的优化设置,由于目前的项目没有这个需求,暂时不写。
多进程 Loader 转换
npm i thread-loader -D
在 webpack.config.js - module - rules 添加如下配置:
{
test: /\.(jsx|js)$/,
use:
[
{
loader:'babel-loader',
},
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}],// 在每个用到 loader 的规则后都加上这一段。
include:[path.resolve(__dirname, 'src')]
},
验证优化
此时运行 npm run build, 看下打包时间是否有变化
可以看到,速度有了显著的提升。
cache-loader 减少重复计算
它能够将上次的计算结果缓存起来,避免每次构建的时候都重新计算。
我暂时先在 babel-loader 前添加:
npm i cache-loader -D
{
test: /\.(jsx|js)$/,
use:
['cache-loader',
'babel-loader',
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}],
include:[path.resolve(__dirname, 'src')]
},
打包两次,结果如下:
可以看到,它将结果缓存起来了,而且打包的速度得到了显著的提升。
部署大小优化
我们加快了构建速度,只是提高了我们的开发效率,但是对于真正的用户而言,你开发效率有没有提高并不影响他访问页面的速度。那访问页面的速度跟什么有关系呢?
我们知道,当浏览器向服务端发起请求时,浏览器会下载静态的资源文件,js、css、 图片等。
如果减少它们的体积,下载的速度是不是会加快呢?还有哪些地方能够优化呢?
使用分析工具,指点江山
下载
npm i webpack-bundle-analyzer -D
在 webpack.config.js 中添加
const { BundleAnalyzerPlugin }= require('webpack-bundle-analyzer');
...
plugins:[
...,
new BundleAnalyzerPlugin({generateStatsFile: true,})// 配置生成 json 文件
]
在 package.json script中添加
"aly": "webpack --progress --mode production"
运行 npm run aly
可以得到如下结果:
可以看到,整体的资源占比。鼠标移上去,能够显示现在文件的大小。接下来,我们通过配置,减少文件。
dist 会生成 stats.json 文件,展示各个文件的大小。
减少文件大小,逐个突破
我们现在到 dist 文件夹下,可以看到 css、js 的展示非常有条理,易于阅读。但是浏览器引擎解析的时候,并不需要读取空格,所以我们可以去掉空格,减少文件大小。
压缩 css
npm install optimize-css-assets-webpack-plugin -D
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
...
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
]
},
...
这时,我们再打包,发现 css 的 size 变成了 195。
压缩 JS
...
const TerserPlugin = require('terser-webpack-plugin');
...
const config = {
...
optimization: {
minimize: true, // 开启最小化
minimizer: [
// ...
new TerserPlugin({})
]
},
...
}
Tree-shaking 抖落废码
首先我们明确一个点,什么是无用代码?
我们在 src 下新建一个 文件 fun.js
写入如下代码:
export const FirstFun = () =>{
alert('FirstFun');
}
export const SecFun = () =>{
alert('SecFun');
}
修改 index.js 文件如下:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app.jsx';
import { FirstFun } from './fun.js';
import "./index.less"
FirstFun();
ReactDOM.render(<App />,document.querySelector("#root"))
可以看到,我们并没有引入 SecFun。
但执行 npm run build 后,我们在 dist 下的 js 文件中查找,依然能找到 SecFun 相关代码。那么此时,SecFun 就是无用代码。
那么,我们怎么让没有引用到的代码在打包的时候删除 掉呢?
在 package.json 中配置
{
"name": "your-project",
"sideEffects": ["*.less"],
...
}
这里要注意,tree-shaking 默认生效于 import 和 export 引入的语法,所以我们要将 less 排除在外,否则打包时,会将 less 文件内容也去掉。
再次打包。
此时,再去 dist 文件夹下的 js 文件中查找 SecFun 。已经找不到了。
所以,清楚没有 import 的内容,成功!
整体体验优化
方便开发者阅读和维护 - 配置分离,各司其职
配置到这里,我们发现,有些配置是为了方便我们开发,有些配置是为了让我们输出的文件更少,有些配置,方便书写的同时,打包也必须读取对应规则才能外城打包,因此有些配置是通用的。
随着配置项的增多,如果依然配置在同一个文件中,会造成阅读和维护上的困难。
我们能不能按照它的功能,抽取通用配置,然后和开发、部署各自合并,打包时,只读取对应的配置呢?
新建三个文件。
webpack.common.js,webpack.dev.js,webpack.prod.js。
目前为止,我的 webpack.config.js 配置如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin }= require('webpack-bundle-analyzer');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
mode:'development',
// entry: ["@babel/polyfill",path.resolve(__dirname,'./src/index.js')],
entry:'./src/index.js',
output:{
filename: '[name].[chunkhash].js',
path:path.resolve(__dirname,'./dist'),
},
// devtool: 'inline-source-map',
resolve:{
alias:{
'@component': path.resolve(__dirname, 'src/component'),
'@assets': path.resolve(__dirname, 'src/assets'),
},
extensions:['.jsx','.js'],
},
plugins:[ // 配置插件
new CleanWebpackPlugin() ,
new MiniCssExtractPlugin({ // 添加插件
filename: '[name].[chunkhash].css'
}),
new HtmlWebpackPlugin({
template: './index.html',
scriptLoading: 'blocking',
}),
new webpack.HotModuleReplacementPlugin(),
new BundleAnalyzerPlugin({generateStatsFile: true,analyzerMode: 'disabled',})
],
module: {
rules: [
{
test: /\.(jsx|js)$/,
use:
['cache-loader',
'babel-loader',
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}],
include:[path.resolve(__dirname, 'src')]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,//将处理好的 css 通过 style 标签的形式添加到页面上
{
loader:'css-loader',
options: {
modules: true
}
},//将 CSS 转化成 webpack 能够识别的数据
'postcss-loader',//自动添加 CSS3 部分属性的浏览器前缀
'less-loader',// 将 less 文件转化为 css
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}
],
include:[path.resolve(__dirname, 'src')]
},
{
test: /\.(jpe?g|png|gif)$/i, // 匹配图片文件
use:[
{
loader:'file-loader',
options: {
limit: 10240,
}// 使用 file-loader
},
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}
],
include:[path.resolve(__dirname, 'src/assets')]
}
]
},
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
new TerserPlugin()
]
},
devServer:{
static: {
directory: path.join(__dirname, './dist'),
},
compress: true,
port: 9000,
hot: true,
// open:true,
watchFiles:'./index.html'
}
})
我们知道,浏览器是无法直接阅读 jsx、less 格式的代码的,所以我们都必须转化成 css、js 文件。
因此,根据目前的知识,我个人认为 构建、入口、html模板 这一块应该是通用内容。
热更新、代码分析、本地服务器、多进程打包 只会在本地开发的时候用到。
css、js 的压缩、废代码的删除则主要用于生产环境。
因此,webpack.common.js 配置如下。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode:'development',
// entry: ["@babel/polyfill",path.resolve(__dirname,'./src/index.js')],
entry:'./src/index.js',
output:{
filename: '[name].[chunkhash].js',
path:path.resolve(__dirname,'./dist'),
},
resolve:{
alias:{
'@component': path.resolve(__dirname, 'src/component'),
'@assets': path.resolve(__dirname, 'src/assets'),
},
extensions:['.jsx','.js'],
},
plugins:[ // 配置插件
new CleanWebpackPlugin() ,
new MiniCssExtractPlugin({ // 添加插件
filename: '[name].[chunkhash].css'
}),
new HtmlWebpackPlugin({
template: './index.html',
scriptLoading: 'blocking',
}),
],
module: {
rules: [
{
test: /\.(jsx|js)$/,
use:
['cache-loader',
'babel-loader',
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}],
include:[path.resolve(__dirname, 'src')]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,//将处理好的 css 通过 style 标签的形式添加到页面上
{
loader:'css-loader',
options: {
modules: true
}
},//将 CSS 转化成 webpack 能够识别的数据
'postcss-loader',//自动添加 CSS3 部分属性的浏览器前缀
'less-loader',// 将 less 文件转化为 css
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}
],
include:[path.resolve(__dirname, 'src')]
},
{
test: /\.(jpe?g|png|gif)$/i, // 匹配图片文件
use:[
{
loader:'file-loader',
options: {
limit: 10240,
}// 使用 file-loader
},
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
}
],
include:[path.resolve(__dirname, 'src/assets')]
}
]
},
}
我们下载插件,便于配置文件的合并。
npm i webpack-merge -D
在 webpack.pro.js 中写入:
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin }= require('webpack-bundle-analyzer');
module.exports = merge(common, {
plugins:[
new BundleAnalyzerPlugin({generateStatsFile: true,analyzerMode: 'disabled',})
],
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
new TerserPlugin()
]
},
});
在 webpack.dev.js 中写入:
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const webpack = require('webpack');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(merge(common, {
plugins:[
new webpack.HotModuleReplacementPlugin(),
],
devServer:{
static: {
directory: path.join(__dirname, './dist'),
},
compress: true,
port: 9000,
hot: true,
// open:true,
watchFiles:'./index.html'
}
}));
然后修改 package.json 中的 script 文件如下:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.pro.js",// 打包
"start": "webpack-dev-server --open --config webpack.dev.js",// 启动
"aly": "webpack --progress --mode production --config webpack.pro.js"
},
此时配置文件的阅读变得相对简单,运行对应的命令,就能使用各自的配置打包或者在本地服务器启动。
配置路由,完善项目
尽管还有诸多配置不够完善,但是!完成大于完美。接下来,我们给项目添加路由,让它的完成度更高一些。
修改项目结构如下:
可以看到,我添加了 src/page 文件夹,用于放置页面文件,然后在 page 文件夹下新建了三个文件夹,代表三个页面内容。
代码如下,三个界面只是展示内容有差别:
import React from 'react';
export default class Index extends React.Component{
render(){
return <div>HOME</div>
// return <div>FIRST-PAGE</div>
// return <div>SECOND-PAGE</div>
}
}
下载插件
npm i react-router-dom -D
修改 Index.jsx 代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app.jsx';
ReactDOM.render(<App />,document.querySelector("#root"))
此时,修改 app.jsx 代码如下:
import React from 'react';
import { BrowserRouter,Route,Routes,Link,} from 'react-router-dom';
import Home from './page/home';
import FirstPage from './page/firstPage';
import SecondPage from './page/secondPage';
export default class App extends React.Component{
render(){
return <BrowserRouter>
<h1>APP Page</h1>
<div><Link to='/home'>to Home</Link></div>
<div><Link to='/firstPage'>to firstPage</Link></div>
<div><Link to='/secondPage'>to secondPage</Link></div>
<div>
<h1>Page-Content</h1>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/home" element={<Home />} />
<Route exact path="/firstPage" element={<FirstPage />} />
<Route exact path="/secondPage" element={<SecondPage/>} />
</Routes>
</div>
</BrowserRouter>
}
}
这个时候运行,我们就得到了如下界面:
我们点击不同的链接,就会发现,page-content 发生了改变,顶部的路由也发生了变化。
到了这一步,如果我们想要让路由的写法更加简洁,我们可以单独配置一个路由文件导出,然后通过 map 统一渲染,这样,配置路由的时候,我们直接去路由文件中配置即可。
结语
写到这里,相信的大家对一些基本的用法已经比较了解了。
根据官网和文中提到的参考文档,还有很多知识点我没有提及。比如,source-map 的使用。
设置多个入口文件、代码按需分割等。
我平时写管理系统比较多,对功能的使用场景还不太有概念,等我进一步研究,下篇再见!