webpack


2020-11-09 上次更新时间:4/29/2022, 9:34:08 AM 0

# 前言

了解一个工具/框架的最全面、最快速的就是看官网文档。当看到很多文章总结的高级用法和写法的时候,总以为别人很厉害,其实是你自己看的不够仔细。(最近两年才悟出来,实在惭愧。。)

# 简介

webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具,当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。

# 核心概念

# entry

入口文件,作为构建其内部依赖图的开始,默认是src/index.js

可配置单个入口和多个入口。多个入口会生成多个独立分离的依赖图,从而形成多页面应用程序。

module.exports = {
    // entry: 'src/main.js', // 单入口
    entry: {  // 多入口
        app: './src/app.js',
        search: './src/search.js'
    }
}
1
2
3
4
5
6
7

# output

出口文件,设置输出 bundles 的路径以及如何命名这些文件,默认./dist

可设置单个或多个输出。如在 entry 中创建了多个入口,则在这里需要使用[name].js命名文件,使得输出文件名不冲突。

module.exports = {
    // output: { // 输出单个文件
    //     filename: 'bundle.js',
    //     path: __dirname + '/dist'
    // },
    output: { // 输出多个
        filename: '[name].js', // 根据 entry 中的多入口文件名输出
        path: __dirname + '/dist'
    }
}
1
2
3
4
5
6
7
8
9
10

# loader

模块转换器,将各种文件转换为 webpack 能处理的有效文件(webpack 自身只理解 JavaScript)。

loader 支持链式传递,资源可以实现管道式传递,并且 loader 的解析顺序是从后往前的

module.exports = {
  module: {
    rules: [
        { 
            test: /\.css$/, 
            use: [ // 先执行 css-loader,处理完后传递给 style-loader
                'style-loader',
                'css-loader'
            ]
        }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# plugins

插件,功能强大,可以用来处理各式各样的任务,插件可以访问到 webpack 的整个编译生命周期。

const HtmlWebpackPlugin = require('html-webpack-plugin'); //通过 npm 安装
const webpack = require('webpack'); //访问内置的插件

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};
1
2
3
4
5
6
7
8
9

# mode

模式,通过设置不同的模式启动相应的 webpack 内置优化(如设置:mode: 'production' 会启用 UglifyJsPlugin 插件进行压缩)

可以通过设置不同的 mode 实现针对不同环境设置不同的打包配置。

module.exports = {
    mode: 'production'
};
1
2
3

# 构建流程

了解完核心概念后,需要了解一下大概的构建流程

  • 从入口文件开始,分析整个应用的依赖树
  • 将每个依赖模块包装起来,放到一个数组中等待调用
  • 实现模块加载的方法,并发它放到模块执行环境中,确保模块间可以互相调用
  • 把执行入口文件的逻辑放到一个函数表达式中,并立即执行这个函数

# loader

loader 用于对模块的源代码进行转换,如 less -> css。

loader 的解析顺序是从后到前的

# 自定义loader

项目搭建

# 初始化
npm init
# 安装必须插件
npm install webpack webpack-cli loader-utils -D
1
2
3
4

需转换文件:test.we(webpack不识别该文件类型,需要做转换)

a:1
1

自定义loader(仅演示,更复杂的逻辑需要根据业务调整)

// loader/we-loader.js
const loaderUtils = require("loader-utils");

module.exports = function(source){
    let loaderOptions = loaderUtils.getOptions(this);
    console.log('loaderOptions', loaderOptions); // {type: we}
    console.log('source', source); // a:1
    // 处理逻辑
    const value = source.split(':')[1]
    return value; // 一定要返回
}


// loader/notes-loader.js
module.exports = function(source){
    console.log('source', source); // 1
    // 处理逻辑
    const result = `console.log(${source * 100})`
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

webpack.config.js

module.exports = {
    entry:{
        app:"./test.we" // 方便查看,直接使用 .we 文件做入口
    },
    output:{
        filename:"[name].bundle.js"
    },
    module:{
        rules:[
            {
                test:/\.we$/,
                use: [
                    {
                        loader: "./loader/notes-loader" // 自定义loader,乘以 100
                    },
                    {
                        loader: "./loader/we-loader", // 自定义loader,转换.we文件
                        options: {
                            type: 'we'
                        }
                    }
                ]
            }
        ]
    }
}
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

package.json 添加打包命令

{
  "name": "webpackdemo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "webpack": "webpack", // 使用 webpack 打包
  }
}
1
2
3
4
5
6
7
8
9

打包

# 打包
npm run webpack

# 日志:可以看到是先执行了 we-loader,再执行 notes-loader
loaderOptions { type: 'we' }
source a:1
source 1
1
2
3
4
5
6
7

打包后代码已经转换为想要的内容

# plugin

webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。

plugin 是拓展功能的插件,用于完成 loader 无法实现的其他事情。

# Compiler 和 Compilation

在插件开发中最重要的两个资源就是 compiler 和 compilation 对象。

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

  • compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

# 自定义plugin

(本次例子基于自定义loader的项目)

plugin/hello-word-plugin.js

const { ConcatSource } = require("webpack-sources");

const wrapComment = str => {
	return `/*!\n * ${str
		.replace(/\*\//g, "* /")
		.split("\n")
		.join("\n * ")
		.replace(/\s+\n/g, "\n")
		.trimRight()}\n */`;
};

class HelloWorldPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
      const options = this.options;
      console.log('options', options);  // { customOpt: { name: '呵呵' } }

      compiler.hooks.compilation.tap("HelloWorldPlugin", compilation => {
        compilation.hooks.processAssets.tap(
          {
            name: "HelloWorldPlugin",
          },
          () => {
            for (const chunk of compilation.chunks) {
              for (const file of chunk.files) {
                console.log('file>>>', file) // app.bundle.js
                const comment = wrapComment('这是个版权信息');
                compilation.updateAsset(
                  file,
                  old => {
                    return new ConcatSource(comment, "\n", old)
                  }
                );
              }
            }
          }
        );
      });
    }
}

module.exports = HelloWorldPlugin;
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
35
36
37
38
39
40
41
42
43
44

webpack.config.js

const HelloWorldPlugin = require("./plugin/hello-world-plugin")

module.exports = {
    entry:{
        app:"./test.we"
    },
    output:{
        filename:"[name].bundle.js"
    },
    module:{
        rules:[
            {
                test:/\.we$/,
                use: [
                    {
                        loader: "./loader/notes-loader"
                    },
                    {
                        loader: "./loader/we-loader",
                        options: {
                            type: 'we'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HelloWorldPlugin({ // 自定义plugin
            customOpt: {
                name: '呵呵'
            }
        })
    ]
}
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
35

打包输出,可以看到多了注释和一个 license 说明文件

# 模块

webpack 中一切皆模块。任何具有独立功能的代码放到一个单独的文件中,就是一个模块,如 js 文件、vue 文件、图片等。

# 模块化的优点

  • 作用域封装:每个模块都可以封装属于自己的作用域,就可以避免出现冲突和被覆盖
  • 复用性:相同的模块可以复用
  • 解除耦合:复杂业务解耦,更容易维护和拓展

# 模块化机制

webpack 不强制使用某种模块化方案,而是通过兼容所有模块化方案让用户可以自由选择。

# webpack 支持的模块化方案

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD definerequire 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件中的图片链接(<img src="..."))

js 文件一样能 import 引入 css 文件

# 模块解析

既然可以通过不同方式去引入模块,那么必定涉及到一个路径解析问题。

# resolver

resolver 是一个库,用于帮助找到模块的绝对路径。resolver 帮助 webpack 找到 bundle 中需要引入的模块代码,这些代码在包含在每个 require/import 语句中。

# 解析规则

当打包模块时,webpack 中使用 enhanced-resolve 来解析文件路径,它可以解析3种路径:

  • 绝对路径: import "C:\\Users\\me\\file";
  • 相对路径: import "./file2";
  • 模块路径: import "vue";

# 自定义解析配置

webpack 已经提供一些系列默认的解析配置了,你依然可以针对具体的需求进行修改。

相关配置都在resolve字段。

module.exports = {
    resolve: {
        extensions: ['.js', '.vue', '.json'], // 引入这几种文件时不再需要写文件后缀
        alias: { // 设置别名
            '@': resolve('src')
        }
    }
}
1
2
3
4
5
6
7
8

# 解析loader

loader 遵循标准的模块解析。也可以通过 resolveLoader 配置选项来为 Loader 提供独立的解析规则。

module.exports = {
    resolveLoader: { // 仅用于解析 loader 包
        alias: { // 设置别名
            'a-loader': path.resolve(__dirname, 'loaders/a.js')
        }
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                // use: path.resolve(__dirname, 'loaders/a.js'),
                use: 'a-loader', // 直接使用别名
                exclude: /node_modules/
            }
        ]
    }
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# devServer

devServerwebpack-dev-server 有关的配置。

可以通过配置 devServer 改变其行为,方便开发。(仅用于开发环境)

webpack.dev.config.js

  module.exports = {
    devServer: {
        host: '0.0.0.0', // 指定使用host,默认是localhost
        port: 9000, // 配置端口
        open: true, // 自动打开浏览器
        hot: true, // 开启热更新
        contentBase: path.join(__dirname, "public"), // 资源路径
        publicPath: "/assets/", // 此路径下的打包文件可在浏览器中访问
        historyApiFallback: true, // 当使用 history 模式时,任意的 404 响应都被替代为 index.html
        proxy: { // 代理
            "/api": {
                target: "http://172.163.11.14:3000",
                pathRewrite: {"^/api" : ""}
            }
        }
        // ...
    }
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 热更新/热替换

《webpack实现热更新(HMR)原理分析》

# 性能优化

《webpack性能优化总结》

# 站内文章

上次更新时间: 4/29/2022, 9:34:08 AM