# webpack

webpack是个模块打包机,它是通过loader模块(Module) 进行处理,通过plugins给webpack赋能,最后打包成浏览器能识别的js等文件。

原理:

  • 识别“入口模块”
  • 分析模块依赖(Tree Shaking
  • 解析模块(通过不同Loader
  • 编译模块,生成抽象语法树AST
  • 循环遍历AST
  • 打包成bundle.js

# Module、Chunk、Bundle

首先,webpack是个模块打包机

# Module

指“模块”。

我们编写的任何文件,对webpack来说都是一个个模块

通过配置module.rules,指定哪些文件交给哪些loader去处理:

module: {
    rules: [
        {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }
    ]
}
1
2
3
4
5
6
7
8

# Chunk

指“代码块”,一个 Chunk 由多个模块组成。用于代码合并与分割。

入口文件(即入口模块) 为例,webpack会通过 入口模块和其它模块之间形成引用关系,去逐个打包模块,那这些“有关联的Module”就形成了一个Chunk

webpack源码中对Chunk的解释:

A Chunk is a unit of encapsulation for Modules.

Chunks are "rendered" into bundles that get emitted when the build completes.

产生chunk的三种途径(详见 代码分割):不同的入口模块(entry)、SplitChunksPlugin、动态导入

# Bundle

综上,Chunk是一些“有关联的模块(module)”的封装单元,并且它们会在 构建之后 变成一个个 Bundle

大多数情况下,一个Chunk只会产生一个Bundle

# bundle.js

bundle.js实际上是一个立即执行的匿名函数

  • 这个函数接受一个数组
    • 它由一个个模块(Module) (模块function的形式) 组成
    • 模块(Module)按照require的顺序排列
  • 每个模块(Module)都有唯一的id(从0递增)

查看bundle.js

# Loader、Plugins

  • Loader 模块转换器。对模块进行解析处理
  • Plugins 扩展插件。在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果。

# Loader

# 常用的loader

sass-loader、less-loader、css-loader、style-loader、babel-loader、vue-loader、file-loader

TIP

file-loader:在执行 import MyImage from './my-image.png 时,这张图片会经过处理、被添加到 output 指定的目录,然后 MyImage 变量将表示 这张图片在处理后 的最终url。

url-loader:和file-loader功能类似,处理图片、字体图标等文件。额外提供了options.limit,指定转为base64的上限值

  • 底层依赖于file-loader(不安装file-loader会报错)

css-loader:处理 .css 里的 url。css内的url('./my-image.png') 会被处理,然后添加到 output 指定的目录,然后url()里面会被替换成 这张图片在处理后 的最终url。(类似file-loader处理)

未经过css-loader处理:

经过css-loader处理:

因为 url-loader底层依赖于file-loader,通常webpack里只需配置url-loader即可

{
    test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)$/i,
    use: [
        {
            loader: require.resolve('url-loader'),
            options: {
                limit: 10000, // 小于10kb的图片会转成base64
                name: 'static/media/[name].[hash:8].[ext]'
            }
        },
        'image-webpack-loader' /*对图片进行压缩*/
    ]
},
1
2
3
4
5
6
7
8
9
10
11
12
13

# Plugins

# 常用的plugins

  • html-webpack-plugin:用于生成一个HTML文件,并将webpack最终生成的JS、CSS以及一些静态资源以scriptlink的形式插入到其中。
  • happypack:把任务分解给多个子进程去并发执行。

# 特点

1、plugins需要在webpack.plugins里实例化并注册;

2、webpack会在调用时,执行plugin对象apply方法,传入 compiler对象

3、compiler下的hooks对象挂载了相应的webpack事件钩子;

4、webpack会在 整个构建过程中 调用这些事件钩子。

compiler对象里拥有 所有和webpack主环境相关 的信息。

TIP

常见的事件钩子:

(按触发顺序)

  • afterPlugins
    • 初始化插件之后
  • compile
    • 在创建新的compilation之前
  • afterCompile
    • 创建完新的compilation
  • emit
    • 在 生成资源 之前
  • afterEmit
    • 在 生成资源 之后
  • done
    • 完成本次编译

# 示例插件:保存时clear日志

class CleanTerminalPlugin {
    constructor(options = {}) {
        this.time = 0;
    }

    apply(compiler) {
        this.useCompilerHooks(compiler);
    }

    useCompilerHooks(compiler) {
        // 在创建完新的compilation后,清空控制台
        compiler.hooks.afterCompile.tap('CleanTerminalPlugin', () => this.clearConsole());
    }

    clearConsole() {
        if (this.time > 2) {
            console.clear();
        } else {
            this.time++;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 代码分割

代码分割最初的目的是 是把代码分离到不同的 bundle 中。这样做的好处是可以 按需加载并行加载 这些 bundle。

代码分割后,bundle 的体积会更小、控制加载优先级。如果使用合理,可以优化加载时间。

因为 Chunk是对一些模块进行封装的基本单位 ,所以代码分割方式和“生成不同Chunk”是一样的:

  • 不同的入口模块(entry)
  • 防止重复
    • SplitChunksPlugin(需指定output.chunkFilename
  • 动态导入

# 通过Entry划分

直接通过 新增入口 来生成不同 Chunk:

// 假设`index.js`、`another.js`中都引入了`lodash`:
module.exports = {
    entry: {
        index: './src/index.js',
        another: './src/another.js', // 新增的入口
    },
    output: {
        filename: '[name].bundle.js', // 注意:多入口时,filename不能写死,要带上[name]
        path: path.resolve(__dirname, 'dist')
    }
}
1
2
3
4
5
6
7
8
9
10
11

npm run build效果:

            Asset     Size   Chunks             Chunk Names
another.bundle.js  550 KiB  another  [emitted]  another
  index.bundle.js  550 KiB    index  [emitted]  index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
1
2
3
4
5

可见,不同入口Chunk之间包含的一些重复模块,会被重复引入到各个Bundle中

需要进一步通过防止重复来移除重复模块。

# 通过SplitChunkPlugin

SplitChunksPlugin可以将 不同入口Chunk之间包含的一些重复模块 提取到一个新生成的 chunk

// 假设`index.js`、`another.js`中都引入了`lodash`:
module.exports = {
    entry: {
        index: './src/index.js',
        another: './src/another.js',
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    // 新增以下:(如不指定,webpack会默认只会对async的第三方包进行分割,见下方“默认配置”)
    optimization: {
        splitChunks: {
            chunks: 'all' // 默认async
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

npm run build效果:

                          Asset      Size                 Chunks             Chunk Names
              another.bundle.js  5.95 KiB                another  [emitted]  another
                index.bundle.js  5.89 KiB                  index  [emitted]  index
vendors~another~index.bundle.js   547 KiB  vendors~another~index  [emitted]  vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
1
2
3
4
5
6

可见,不同入口Chunk之间包含的一些重复模块 已经被提取到了vendors这个Chunk里

vendors~another~index,表示:缓存组~提取的Chunk1~提取的Chunk2...

其中,vendors是默认配置下的vendors缓存组

也可以通过声明cacheGroups.vendors.name来指定这个 提取好的chunk 名字

# 默认配置

optimization: {
    splitChunks: {
        // chunks:表示
        chunks: 'async', // <-- 默认只对“异步加载模块”进行分割
        // minSize:表示引入的包或模块>30kb才会加入“切割范畴”
        minSize: 30000,
        minChunks: 1,
        maxAsyncRequests: 5,
        maxInitialRequests: 3,
        automaticNameDelimiter: '~',
        name: true,
        cacheGroups: {
            // 默认有一个vendors缓存组
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10
            },
        default: {
                minChunks: 2,
                priority: -20,
                reuseExistingChunk: true
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# mini-css-extract-plugin

通过mini-css-extract-plugin将CSS从主应用程序中分离。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// ...
module: {
    rule: [
        {
            test: /\.(css|less)$/,
            // 以下为简化代码
            use: [
                'style-loader',
                MiniCssExtractPlugin.loader, // <-- 使用
                'css-loader',
                'postcss-loader',
                'less-loader'
            ],
            sideEffects: true
        }
    ]
},
plugins: {
    new MiniCssExtractPlugin({
        filename: 'static/css/[name].[contenthash:8].css',
        chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
    })
},
optimization: {
    minimizer: [
        // mode: 'production'会开启tree-shaking和js代码压缩,但配置optimization. minimizer会使默认的压缩功能失效。
        // 所以,指定css压缩插件的同时,务必指定js的压缩插件。
        new TerserPlugin({}),
        // This is only used in production mode
        new OptimizeCSSAssetsPlugin({})
    ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

配合optimization.minimizeroptimization.splitChunks.cacheGroup使用:

  • optimization.minimizer:对css进行压缩(也需指定js压缩,因为会影响webpack对js的压缩)
  • optimization.splitChunks.cacheGroup:将可复用的css代码块提取到单独的 chunk 文件
    • 这一点不是必须,因为复用的css文件比较少,且对于“全局复用样式”可以通过style-resources-loader

# 通过动态导入

两种方式:1、import();2、require.ensure(较少)

import()内部通过 Promise 来实现动态导入。

module.exports = {
    entry: {
        index: './src/index.js' // 只有一个index入口
    },
    output: {
        filename: '[name].bundle.js',
        chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', // 指定chunk的名字生成规则
        path: path.resolve(__dirname, 'dist')
    }
}
1
2
3
4
5
6
7
8
9
10

是在import()时,进行 魔术注释 指定 非入口chunk 的名称

import(/* webpackChunkName: "pc-home" */ '@/view/pc/home')
1

此处实验过,入口chunk也会应用chunkFilename的规则?

npm run build效果:

                   Asset      Size          Chunks             Chunk Names
         index.bundle.js  7.88 KiB           index  [emitted]  index
vendors~lodash.bundle.js   547 KiB  vendors~lodash  [emitted]  vendors~lodash
Entrypoint index = index.bundle.js
1
2
3
4

vendors~lodash,是因为webpack默认会对async包进行SplitChunk配置下的vendors缓存组。

# 热编译的提速方案

# Happypack

在webpack里,loaders都是单个解析、编译,不能同时处理多个任务。利用Happypack可以让它交给多个子进程去并发执行。执行完后,子进程再将结果发送给主进程。

// 共享进程池:多个Happypack实例都使用同一个共享进程池的子进程去处理任务,防止资源占用过多。
const Happypack = require('happypack');
const happyThreadPool = Happypack.ThreadPool({ size: 5 });

modules.exports = {
    plugins: [
        new HappyPack({
            id: 'babel',
            loaders: ['babel-loader'],
            threadPool: happyThreadPool
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# DllPlugin

将复用性较高的第三方模块,打包到动态链接库里。

很多产品都用到螺丝,但并不是每次生产产品都需要把生产螺丝的过程重新执行。而是螺丝单独生产,侧面也加快了产品的生产速度。

使用Dll,分为DLL构建、主项目构建。

  • 在DLL构建文件里指定要打包的第三方库、接入DllPlugin
    • 执行DLL构建打包后,会生成:x.dllx.manifest.json
    • x.dll:类似bundle.js,用数组保存模块,索引值作为id
    • x.manifest.json:描述对应dll文件里的模块信息
  • 在主项目构建文件引入动态库文件
    • webpack.DllReferencePlugin
  • 在入口文件引入dll

和CDN区别:

  • CDN需要配置externals、业务层去掉import
  • DLL放在本地,比较稳定、业务层引用不变

# HardSourceWebpackPlugin

高速缓存在第二次启动 及以后,直接从缓存获取文件,以提高开发编译速度。

搭配Happypack、DLL、CDM使用更佳

// webpack.dev.js
    new HardSourceWebpackPlugin({
        // 缓存存放位置
        cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
        // hash生成规则
        configHash: function(webpackConfig) {
            return require('node-object-hash')({ sort: false }).hash(webpackConfig);
        },
        // Either false, a string, an object, or a project hashing function.
        environmentHash: {
            root: process.cwd(),
            directories: [],
            files: ['package-lock.json', 'yarn.lock']
        },
        // 控制台输出格式
        info: {
            mode: 'test',
            level: 'debug' // 'debug', 'log', 'info', 'warn', or 'error'
        },
        // 旧缓存清空机制
        cachePrune: {
            // 缓存有效期大于2days时
            maxAge: 2 * 24 * 60 * 60 * 1000,
            // 所有缓存体积大于500MB时
            sizeThreshold: 500 * 1024 * 1024
        }
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 总结

建议:在 开发打包 不同环境采用不同策略,以尽最大优化:

方式 DLL CDN webpack打包 高速缓存
开发 react、react-router-dom、react-dom lodash、moment antd
打包 react、react-router-dom、react-dom、antd lodash、moment /

Q:为什么 antd 在开发时放DLL、打包时放webpack?

  • 开发时,通过DLL会完整构建,提高编译速度
  • 打包时,可以tree shaking,减少打包体积

Q:具体放置策略?

  • 全盘使用的包放入DLL(如:react);
  • 部分使用的包通过webpack打包(如:antd);
  • 体积较小通过CDN(如:lodash)

# gulp与webpack的区别

gulp强调的是前端开发流程

用法: 定义一系列的task,再定义它处理的事物、顺序,最后让gulp执行task,从而构建前端项目。

4个常用的方法:

  • src():获取流
  • dest():写文件
  • task():定义任务
  • watch():用来监听事件

IE8下最好用gulp,IE9用webpack

# 实践笔记

webpack使用笔记

# 链接

更新时间: 6/29/2020, 7:57:54 PM