webpack学习 - codesplit优化代码运行性能

210 阅读8分钟

背景

打包代码时会将所有js文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的js文件,其他文件不应该加载

所以我们需要将打包生成的文件进行代码分割,生成多个js文件,渲染哪个页面就只加载某个js文件, 这样加载的资源就少,速度就更快。

是什么

code split做了两件事: 1、分割文件:将打包生成的文件进行分割,生成多个js文件 2、按需加载:需要哪个文件就加载哪个文件

怎么用

代码分割实现方式有不同的方式,为了更加方便体现它们之间的差异,分别创建新的文件来演示

一、多入口

1、文件目录

image.png

2、下载包

npm install webpack webpack-cli html-webpack-plugin --save-dev

3、新建文件

内容不用关注,主要看打包后的输出结果

main.js

console.log("hello main")

app.js

console.log("hello app")

4、配置

webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    //一个入口文件,单入口可以写成字符串
    // entry: './src/main.js'

    //多个入口文件,多入口
    entry: {
        main: './src/main.js',
        app: './src/app.js'
    },

    output: {
        path: path.resolve(__dirname, 'dist'),
        //[name]webpack命名方式,使用chunk的name那么作为输出的文件名
        //什么是chunk,打包的资源是chunk,输出的资源是bundle
        //比如说entry中xxx: './sc/yyy.js', yyy就是chunk的文件名
        //为什么这么命名?如果写死名称main.js,那么打包生成两个js文件都叫main.js,会发生覆盖(实际会报conflict错误)
        filename: '[name].js'
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        })
    ],
    mode: 'production',
}

5、运行指令

npx webpack

此时在dist目录中能看到输出了两个js文件

image.png

总结:配置几个入口,至少输出几个js文件

二、提取重复代码

如果多个文件都引用同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。 我们需要提取多入口的重复代码,只打包生成一个js文件,其他文件引用它就好

1、修改文件

main.js

import { sum } from './math.js'

console.log("hello main")
console.log(sum(3, 4, 5))

app.js

import { sum } from './math.js'

console.log("hello app")
console.log(sum(1, 2, 3))

添加文件math.js

export function sum(...args) {
    return args.reduce((a, b) => {
        return a + b;
    }, 0)
}

2、修改配置文件

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    //一个入口文件,单入口可以写成字符串
    // entry: './src/main.js'

    //多个入口文件,多入口
    entry: {
        main: './src/main.js',
        app: './src/app.js'
    },

    output: {
        path: path.resolve(__dirname, 'dist'),
        //[name]webpack命名方式,使用chunk的name那么作为输出的文件名
        //什么是chunk,打包的资源是chunk,输出的资源是bundle
        //比如说entry中xxx: './sc/yyy.js', yyy就是chunk的文件名
        //为什么这么命名?如果写死名称main.js,那么打包生成两个js文件都叫main.js,会发生覆盖(实际会报conflict错误)
        filename: '[name].js'
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        })
    ],
    mode: 'production',
    optimization: {
        // 代码分割配置
        splitChunks: {
            chunks: "all", // 对所有模块都进行分割
            // 以下是默认值
            // minSize: 20000, // 分割代码最小的大小
            // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
            // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
            // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
            // maxInitialRequests: 30, // 入口js文件最大并行请求数量
            // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
            // cacheGroups: { // 组,哪些模块要打包到一个组
            //   defaultVendors: { // 组名
            //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
            //     priority: -10, // 权重(越大越高)
            //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
            //   },
            //   default: { // 其他没有写的配置会使用上面的默认值
            //     minChunks: 2, // 这里的minChunks权重更大
            //     priority: -20,
            //     reuseExistingChunk: true,
            //   },
            // },
            // 修改配置
            cacheGroups: {
                // 组,哪些模块要打包到一个组
                // defaultVendors: { // 组名
                //   test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
                //   priority: -10, // 权重(越大越高)
                //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
                // },
                default: {
                    // 其他没有写的配置会使用上面的默认值
                    minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    },
}

3、运行命令

npx webpack

此时会发现生成3个js文件,有一个是提取的公共模块

image.png

三、按需加载,动态导入

想要实现按需加载,动态导入模块。还需要额外配置:

1、修改文件

增加count.js

export default function count(x, y) {
    return x - y
}

main.js

import count from './count.js'
import { sum } from './math.js'
// import count from './count.js'
console.log("hello main")
console.log(sum(3, 4, 5))
let btn = document.getElementById("btn")
btn.onclick = () => {
    // import 动态导入:会将动态导入的文件代码分割(拆分成单独模块),在需要使用的时候自动加载
    import('./count.js')
        .then((res) => {
            console.log('模块加载成功', res.default(7, 4))
        })
        .catch((err) => {
            console.log('模块加载失败', err)
        })
}

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>Code Split</title>
</head>

<body>
    <h1>hello webpack</h1>
    <button id="btn">按钮</button>
</body>

</html>

2、运行命令

npx webpack

image.png

我们可以发现,一旦通过 import 动态导入语法导入模块,模块就被代码分割,同时也能按需加载了(点击按钮之后才会加载655.js也就打包之前的count.js文件)

四、单入口

开发时一般是单页面应用(SPA),只有一个入口(单入口)。那么需要这样配置:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 单入口
  entry: "./src/main.js",
  // 多入口
  // entry: {
  //   main: "./src/main.js",
  //   app: "./src/app.js",
  // },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
    // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
    filename: "js/[name].js",
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  mode: "production",
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // minSize: 20000, // 分割代码最小的大小
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\/]node_modules[\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
  },
};

五、更新配置

最终我们会使用单入口+代码分割+动态导入方式来进行配置。更新之前的配置文件。

webpack.prod.js

const path = require('path');
const os = require('os');
const ESLintPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

console.log('hahahahha', __dirname)

const threads = os.cpus().length; //cpu核数

function getStyleLoader(pre) {
    return [
        MiniCssExtractPlugin.loader,
        'css-loader',
        {
            loader: 'postcss-loader',
            options: {
                postcssOptions: {
                    plugins: [
                        [
                            "postcss-preset-env",
                        ],
                    ],
                },
            },
        },
        pre
    ].filter(Boolean);
}


module.exports = {
    //入口
    entry: './src/main.js',
    //输出
    output: {
        //所有文件的输出目录
        //__dirname代表当前文件文件夹目录
        path: path.resolve(__dirname, '../dist'),
        //入口文件打包输出文件名
        filename: 'static/js/main33.js',
        //在打包文件前,清除之前的打包文件
        clean: true,
        //输出图片名称
        // assetModuleFilename: 'images/[hash][ext][query]'
    },
    //加载器
    module: {
        //loader的配置
        rules: [
            {
                oneOf: [
                    {
                        test: /\.css$/i,
                        use: getStyleLoader()
                    },
                    {
                        test: /\.less$/i,
                        use: getStyleLoader('less-loader')
                    },
                    {
                        test: /\.s[ac]ss$/i,
                        use: getStyleLoader('sass-loader')
                    },
                    {
                        test: /\.styl$/i,
                        use: getStyleLoader('stylus-loader')
                    },
                    {
                        test: /\.(png|jpe?g|gif|webp|svg)/,
                        type: "asset",
                        parser: {
                            dataUrlCondition: {
                                // 小于10kb的图片转base64
                                // 优点:减少请求数量  缺点:体积会更大
                                maxSize: 10 * 1024
                            }
                        },
                        generator: {
                            filename: 'static/images/[name].[hash:10][ext][query]'
                        }
                    },
                    {
                        test: /\.(ttf|woff2?|mp3|mp4|avi)/,
                        type: "asset/resource",
                        generator: {
                            filename: 'static/media/[name].[hash:10][ext][query]'
                        }
                    },
                    {
                        test: /\.js$/,
                        // exclude: /(node_modules|bower_components)/,  //排除node_modules中的js文件,其他文件都处理
                        include: path.resolve(__dirname, "../src"),   //只处理src中的js文件
                        // loader: 'babel-loader',
                        // options: {
                        //     cacheDirectory: true,  //开启bable缓存
                        //     cacheCompression: false, //关闭缓存文件压缩
                        // }
                        use: [
                            {
                                loader: 'thread-loader', // 开启多进程
                                options: {
                                    works: threads // 进程数量
                                }
                            },
                            {
                                loader: 'babel-loader',
                                options: {
                                    cacheDirectory: true,  //开启bable缓存
                                    cacheCompression: false, //关闭缓存文件压缩
                                    plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                                },
                            }
                        ],
                    },
                ]
            }
        ]
    },
    //插件
    plugins: [
        new ESLintPlugin({
            // 检测哪些文件
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules",
            // cache: true,   //默认为true
            // cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslintcache")
            threads // 开启多进程和设置进程数量
        }),
        new HtmlWebpackPlugin({
            //模板: 以public/index.html文件创建新的html文件
            // 新的文件特点:1、结构和原来一致  2、自动引入打包的输出的资源
            template: path.resolve(__dirname, "../public/index.html")
        }),
        new MiniCssExtractPlugin({
            filename: 'static/css/main.css'
        }),
        // new CssMinimizerPlugin(),
        // new TerserPlugin({
        //     parallel: threads // 开启多进程和设置进程数量
        // })
    ],
    optimization: {
        //压缩的操作
        minimizer: [
            // 压缩css
            new CssMinimizerPlugin(),
            // 压缩js
            new TerserPlugin({
                parallel: threads // 开启多进程和设置进程数量
            })
        ],

        //代码分割
        splitChunks: {
            chunks: 'all',
            // 其他的都用默认值即可
            // 如果用到node_modules中的文件,就会将node_modules文件单独打包成一个文件
            // 
        }
    },
    //模式
    mode: "production",
    devtool: "source-map"
}

六、给动态导入文件取名

1、修改文件

main.js

import count from './js/count.js'
import sum from './js/sum.js'
// import { add } from './js/math.js'
//要想webpack打包资源,必须引入资源
import './css/index.css'
import './less/index.less'
import './sass/index.scss'
import './sass/index.sass'
import './stylus/index.styl'

import './css/iconfont.css'

console.log(count(2, 2))
console.log(sum(1, 2, 3, 4))
// console.log(add(3, 4))

document.getElementById('btn').onclick = () => {
    //eslint不是识别动态导入语法,需要额外增加配置
    //webpackChunkName: 'xxx'是webpack特殊命名规则,也叫魔法命名
    import(/* webpackChunkName: 'math'*/'./js/math.js')
        .then(res => {
            console.log("模块加载成功", res.mulit(1, 2))
        })
}


if (module.hot) {
    //判断是否支持热模块替换功能
    module.hot.accept('./js/count', () => {
        console.log('count文件被更新了')
    })
    module.hot.accept('./js/sum', () => {
        console.log('sum文件被更新了')
    })
}

2、修改eslint配置

下载插件依赖包

npm install eslint-plugin-import --save-dev

修改配置文件.eslintrc.js


module.exports = {
    extends: [
        'eslint:recommended',
    ],
    env: {
        node: true,   //启用node中全局变量
        browser: true    //启用浏览器中全局变量
    },
    parserOptions: {
        ecmaVersion: 11,  //新版本eslint-plugin-import插件需要使用更高版本ecmaVersion配置:2020 || 11,不然还是会报错
        sourceType: 'module'
    },
    rules: {
        "no-var": 2 //不能使用var定义变量
    },
    plugins: ['import'] //解决动态导入语法报错
}

七、统一命名配置

js代码多入口、动态导入; 图片、字体统一命名

 //入口文件打包输出文件名
    filename: 'static/js/[name].js',
    //给打包输出的其他文件命名
    chunkFilename: 'static/js/[name].chunk.js',

    //图片、字体等通过type:asset处理的资源
    assetModuleFilename: 'static/media/[name].[hash:10][ext][query]',

css多入口,动态导入

    new MiniCssExtractPlugin({
        filename: 'static/css/[name].css',
        chunkFilename: 'static/css/[name].chunk.css'
    }),

完整配置

const path = require('path');
const os = require('os');
const ESLintPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

console.log('hahahahha', __dirname)

const threads = os.cpus().length; //cpu核数

function getStyleLoader(pre) {
    return [
        MiniCssExtractPlugin.loader,
        'css-loader',
        {
            loader: 'postcss-loader',
            options: {
                postcssOptions: {
                    plugins: [
                        [
                            "postcss-preset-env",
                        ],
                    ],
                },
            },
        },
        pre
    ].filter(Boolean);
}


module.exports = {
    //入口
    entry: './src/main.js',
    //输出
    output: {
        //所有文件的输出目录
        //__dirname代表当前文件文件夹目录
        path: path.resolve(__dirname, '../dist'),
        //入口文件打包输出文件名
        filename: 'static/js/[name].js',
        //给打包输出的其他文件命名
        chunkFilename: 'static/js/[name].chunk.js',

        //图片、字体等通过type:asset处理的资源
        assetModuleFilename: 'static/media/[name].[hash:10][ext][query]',
        //在打包文件前,清除之前的打包文件
        clean: true,
        //输出图片名称
        // assetModuleFilename: 'images/[hash][ext][query]'
    },
    //加载器
    module: {
        //loader的配置
        rules: [
            {
                oneOf: [
                    {
                        test: /\.css$/i,
                        use: getStyleLoader()
                    },
                    {
                        test: /\.less$/i,
                        use: getStyleLoader('less-loader')
                    },
                    {
                        test: /\.s[ac]ss$/i,
                        use: getStyleLoader('sass-loader')
                    },
                    {
                        test: /\.styl$/i,
                        use: getStyleLoader('stylus-loader')
                    },
                    {
                        test: /\.(png|jpe?g|gif|webp|svg)/,
                        type: "asset",
                        parser: {
                            dataUrlCondition: {
                                // 小于10kb的图片转base64
                                // 优点:减少请求数量  缺点:体积会更大
                                maxSize: 10 * 1024
                            }
                        },
                        // generator: {
                        //     //输出的图片名称
                        //     // [hash:10]hash值取前10位
                        //     filename: 'static/images/[name].[hash:10][ext][query]'
                        // }
                    },
                    {
                        test: /\.(ttf|woff2?|mp3|mp4|avi)/,
                        type: "asset/resource",
                        // generator: {
                        //     filename: 'static/media/[name].[hash:10][ext][query]'
                        // }
                    },
                    {
                        test: /\.js$/,
                        // exclude: /(node_modules|bower_components)/,  //排除node_modules中的js文件,其他文件都处理
                        include: path.resolve(__dirname, "../src"),   //只处理src中的js文件
                        // loader: 'babel-loader',
                        // options: {
                        //     cacheDirectory: true,  //开启bable缓存
                        //     cacheCompression: false, //关闭缓存文件压缩
                        // }
                        use: [
                            {
                                loader: 'thread-loader', // 开启多进程
                                options: {
                                    works: threads // 进程数量
                                }
                            },
                            {
                                loader: 'babel-loader',
                                options: {
                                    cacheDirectory: true,  //开启bable缓存
                                    cacheCompression: false, //关闭缓存文件压缩
                                    plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                                },
                            }
                        ],
                    },
                ]
            }
        ]
    },
    //插件
    plugins: [
        new ESLintPlugin({
            // 检测哪些文件
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules",
            // cache: true,   //默认为true
            // cacheLocation: path.resolve(__dirname, "../node_modules/.cache/eslintcache")
            threads // 开启多进程和设置进程数量
        }),
        new HtmlWebpackPlugin({
            //模板: 以public/index.html文件创建新的html文件
            // 新的文件特点:1、结构和原来一致  2、自动引入打包的输出的资源
            template: path.resolve(__dirname, "../public/index.html")
        }),
        new MiniCssExtractPlugin({
            filename: 'static/css/[name].css',
            chunkFilename: 'static/css/[name].chunk.css'
        }),
        // new CssMinimizerPlugin(),
        // new TerserPlugin({
        //     parallel: threads // 开启多进程和设置进程数量
        // })
    ],
    optimization: {
        //压缩的操作
        minimizer: [
            // 压缩css
            new CssMinimizerPlugin(),
            // 压缩js
            new TerserPlugin({
                parallel: threads // 开启多进程和设置进程数量
            })
        ],

        //代码分割
        splitChunks: {
            chunks: 'all',
            // 其他的都用默认值即可
            // 如果用到node_modules中的文件,就会将node_modules文件单独打包成一个文件
            // 
        }
    },
    //模式
    mode: "production",
    devtool: "source-map"
}

执行命令打包效果:

image.png