命中注定拯救前端的,应该是 webpack(三)

1,872 阅读6分钟

接上篇,继续完善项目的构建。

开发速度优化

避免缓存干扰,加入 hash

目前我们生成的上篇文章中,css 文件输出 filename:'[name].[hash:8].css',

但 js 固定输出为 bundle.js。

项目部署到服务器上以后,为了提高加载速度,通常会使用一种叫做 缓存 的技术。如果使用资源输出的名字没有更改,浏览器加载资源时,可能会认为文件并没有更新,从而读取缓存中的文件。

根据 webpack 官方文档,我们采用 filename: '[name].[chunkhash].js'。css 也做相应的改变。

运行 npm run build 打包,得到如下结果:

7.png

我们再打包一次,得到如下结果:

9.png

此时我们发现,已经生成了新的 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 // 重新安装新的版本运行

此时得到文件的打包时间:

10.png

设定构建匹配文件范围

在 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, 看下打包时间是否有变化

11.png

可以看到,速度有了显著的提升。

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')]
},

打包两次,结果如下:

12.png

13.png

可以看到,它将结果缓存起来了,而且打包的速度得到了显著的提升。

部署大小优化

我们加快了构建速度,只是提高了我们的开发效率,但是对于真正的用户而言,你开发效率有没有提高并不影响他访问页面的速度。那访问页面的速度跟什么有关系呢?

我们知道,当浏览器向服务端发起请求时,浏览器会下载静态的资源文件,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

可以得到如下结果:

14.png

可以看到,整体的资源占比。鼠标移上去,能够显示现在文件的大小。接下来,我们通过配置,减少文件。

dist 会生成 stats.json 文件,展示各个文件的大小。

15.png

减少文件大小,逐个突破

我们现在到 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"
  },

此时配置文件的阅读变得相对简单,运行对应的命令,就能使用各自的配置打包或者在本地服务器启动。

配置路由,完善项目

尽管还有诸多配置不够完善,但是!完成大于完美。接下来,我们给项目添加路由,让它的完成度更高一些。

修改项目结构如下:

Snipaste_2022-04-19_14-03-54.png

可以看到,我添加了 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>
    }
}

这个时候运行,我们就得到了如下界面:

Snipaste_2022-04-19_14-37-09.png

我们点击不同的链接,就会发现,page-content 发生了改变,顶部的路由也发生了变化。

到了这一步,如果我们想要让路由的写法更加简洁,我们可以单独配置一个路由文件导出,然后通过 map 统一渲染,这样,配置路由的时候,我们直接去路由文件中配置即可。

结语

写到这里,相信的大家对一些基本的用法已经比较了解了。

根据官网和文中提到的参考文档,还有很多知识点我没有提及。比如,source-map 的使用。

设置多个入口文件、代码按需分割等。

我平时写管理系统比较多,对功能的使用场景还不太有概念,等我进一步研究,下篇再见!

想要文章中代码可以点击这里~