Webpack5.x入门知识大放送

247 阅读12分钟

什么是webpack,webpack解决什么问题

webpack是一个静态模块打包工具,他能将前端模块的静态资源(js,css,image,tff等)进行打包成一个或多个bundles,用于在浏览器展示你的资源;在前端项目中更高效地管理和维护项目中的每一个资源

解决问题:。如:

  1. 能够将散落的模块打包到一起;如:文件合并,减少http请求等
  2. 能够编译代码中的新特性;如:es6->es5;
  3. 能够支持不同种类的前端资源模块;scss、less -> css等。image -> base64

如何使用webpack实现模块化打包

快速上手

  1. 创建项目npm init -y

    1. 安装webpack与webpackcli npm install webpack webpackcli --save-dev
  2. 项目目录结构

    // demo 结构
    |__src
        |__ index.js // 项目入口文件
        |__ moduleA.js // 模块A
    |__index.html // 外部访问入口
    |__webpack.config.js  // webpack显式配置文件
    |__package.json // 项目配置与依赖
    
    
    // moduleA.js
    export default function() {
      console.log('moduleA')
    }
    
    // index.js
    import moduleA from './moduleA.js'
    moduleA() // 'moduleA'
    

    index.js 是webpack默认的入口文件,在此处引入moduleA

    // index.html
    <script src="./dist/bundle.js"></script>
    

    在应用入口引入webpack构建产物(后面可通过配置来控制)

    // webpack.config.js 导出的是一个对象
    //  import { Configuration } from 'webpack'; // 导入webpack的声明配置,会有自动补全的功能,但是在运行时需要去掉。
    
    /**
     * 加上此注释与引入的Configuration,在编写配置时有代码提示功能(vscode支持)
     * @type { Configuration }
     */
    const config = {
        entry: './src/index.js', // 项目的入口文件,默认是./src/index.js
        output: {
            filename: 'bundle.js', // 输出文件名,默认是main.js
            path: path.resolve(__dirname, 'dist'), // 输出文件夹,默认是./dist
        },
        mode: 'none', // 模式: production(默认,会开启优化等处理), development(为模块和 chunk 启用有效的名), none(不使用任何默认优化选项)
    };
    module.exports = config;
    

    webpack.config.js是webpack显式配置文件,如果不新建这个文件也行,webpack会以默认的配置来打包。

    此处有个开发时的小技巧,通过引入webpack的类型声明文件Configuration.js,在config对象加上对应的注释,在开发时vscode会提供代码提示。

    除了这些配置,webpack还有许多配置,后面会记录起来。

    // package.json
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack"
     }
    

    package.json定义脚本,之后可以在命令行直接运行npm run build来执行webpack命令。当然也可以用npx webpack来执行。npx

  3. 根据以上配置后,命令行执行 npm run build,待执行完,会生成构建产物./dist/bundle.js, 浏览器打开index.html,启动控制台,输出moduleA。一个最基础的webpack构建成功。

webpack产物分析

我们先看一下webpack(5.69.1,在mode=node的配置下)的产物,几个重要的配置

__webpack_require__ 函数

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
  // Check if module is in cache
  var cachedModule = __webpack_module_cache__[moduleId];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  // Create a new module (and put it into the cache)
  var module = (__webpack_module_cache__[moduleId] = {
    // no module.id needed
    // no module.loaded needed
    exports: {},
  });

  // Execute the module function
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // Return the exports of the module
  return module.exports;
}

__webpack_require__是实现模块化的基础,用来加载并执行模块,如果有export还会导出模块。

  1. 首先判断该模块是否已经加载过了,如果已经加载过了,则直接使用缓存。
  2. 新加载的模块新建一个对象{exports: {}}(用于存储模块export内容),同时赋值给__webpack_module_cache__(缓存起来)与module
  3. 执行__webpack_modules__对应模块的业务代码,__webpack_modules__是一个数组,存储模块的代码
var __webpack_modules__ = [
    ,
    (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__,
      });
      // moduleA.js
      function __WEBPACK_DEFAULT_EXPORT__() {
        console.log("moduleA");
      }
    },
 ]
  1. 数组内容是一个函数,接收三个参数__unused_webpack_module, __webpack_exports__(用于收集模块中export的变量),__webpack_require__(给模块提供__webpack_require__方法,比如,模块A引入了模块B,在A中可以通过该对象引入模块)。

    __webpack_require__.r给模块定义一个命名空间。__webpack_require__.d收集export变量依赖并赋值给__webpack_exports__。依赖模块使用通过闭包的方式访问被模块的变量。

  2. 执行完对应模块后,回到__webpack_require__中,此时module.exports已经储存了模块export的变量了,可供其他模块访问。

(() => {
  var _moduleA_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
 (0,_moduleB__WEBPACK_IMPORTED_MODULE_1__["default"])();
})();
  1. 可以在产物中最后看到以上代码,此处代码即是index.js的代码,也是项目代码的入口。

    1. index.js中声明了import moduleA from './moduleA',对应代码第一行代码,通过__webpack_require__导入一个模块。
    2. 然后执行这个模块moduleA() 对应第二行代码,输出moduleA

总结:webpack产物通过自定函数__webpack_require__来加载模块(具有缓存机制)。通过__webpack_modules__模块集来管理不同模块中的代码(依赖与导出)。最后通过一个IIFE来执行index.js,运行应用。

loader

作用:处理项目中的任意资源,使资源能够以模块化的方式使用。

webpack只能理解 JavaScript 和 JSON 文件,如果要加载其他的资源文件,则需要用Loader去处理这些资源文件,并将它们转换为有效 模块,供webpack处理使用,以及被添加到依赖图中。

loader的基本配置

// webpack.config.js
const config = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
    },
    mode: "none",
    module: {
        rules: [
            { test: /\.css$/, use: ["style-loader", "css-loader"] },
            { test: /\.txt$/, use: ['raw-loader'] }
        ],
    },
};
module.exports = config;

我们主要看module对象,module的rules属性是一个数组,数组每一个元素处理一种资源。元素配置如下

  1. test 属性,识别出哪些文件会被转换,以正则表达式匹配文件名,如以上表示匹配.css结尾的文件,已use中的loader去处理这些文件。
  2. use 属性,定义出在进行转换时,应该使用哪个 loader,顺序从右往左,【从下往上】(即先将css文件交给css-loader处理,处理完再交给style-loader处理,最后再交给webpack处理,生成到bundle.js以模块的形式导出使用)

除了在webpack.config.js配置,还可以通过

  1. 通过命令行参数方式,

    webpack --module-bind 'css=style-loader!css-loader'
    
  2. 内联方式

    import from 'style-loader!css-loader?module!./style.txt'
    

loader 工作过程

以css资源加载来理解loader的工作过程

执行过程:style.css -> css-loader -> style-loader -> JavaScript 代码(有效模块) -> webpack -> bundle.js

目录结构

|__src
   |__ index.js // 项目入口文件
   |__ style.css // 样式文件
|__index.html // 外部访问入口
|__webpack.config.js  // webpack显式配置文件
|__package.json // 项目配置与依赖

index.js

import './style.css'

style.css

* {
    margin: 0;
    padding: 0;
}

html {
    min-height: 100vh;
}

body {
    height: 100%;
    background: blue;
}

webpack.config.js

// import { Configuration } from "webpack";
/**
 * @type { Configuration }
 */
const config = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
    },
    mode: "none",
    module: {
        rules: [
            { test: /\.css$/, use: ["style-loader", "css-loader"] },
        ],
    },
};

module.exports = config;

node_modules/css-loader/dist/index.js中最后两行中打印,看css-loader处理style.css输出结果:

// node_modules/css-loader/dist/index.js
console.log(`${importCode}${moduleCode}${exportCode}`);
callback(null, `${importCode}${moduleCode}${exportCode}`);

运行 npm run build

控制台打印:

// ... 省略
// Module
___CSS_LOADER_EXPORT___.push([module.id, "* {\n    margin: 0;\n    padding: 0;\n}\n\nhtml {\n    min-height: 100vh;\n}\n\nbody {\n    height: 100%;\n    background: blue;\n}", ""]);
// Exports
export default ___CSS_LOADER_EXPORT___;

可以看到,css-loader将css文件中的代码转成了字符串,并放到一个数组中,再以模块的形式导出。(loader的返回结果为字符串形式的代码,也就是代码需要转成字符串,供下一个loader/webpack使用)

再分析构建产物bundle.js

___CSS_LOADER_EXPORT___.push([module.id, "* {\n    margin: 0;\n    padding: 0;\n}\n\nhtml {\n    min-height: 100vh;\n}\n\nbody {\n    height: 100%;\n    background: blue;\n}", ""]);
// Exports
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);

bundle.js可以找到以上代码,webpackcss-loader处理过后的代码以模块导出,但是此时并不会执行,因为css-loader只将css代码转成webpack能识别的模块。若想将css代码生效,还需要style-loader的处理。style-loader负责将css-loader转化后的代码以style标签的形式插入到html中。

var _node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(8) // id = 8 是css-loader处理过后的模块
var update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_style_css__WEBPACK_IMPORTED_MODULE_6__["default"], options); // 执行这个函数即会将css代码插入html中,具体逻辑可以调试

style-loader还有个pitch函数,具体请看loader的pitch函数

开发一个loader

了解完loader的工作工程,我们来自定义一个txt-loader,将文本内容转化为字符串。

前置知识:loader需要导出一个函数,函数参数为源文件,返回webpack可以识别的模块

  • 在src目录新建test.txt,随便写上的内容

  • 在index.js引入模块

    import txtStr from "./test.txt";
    document.body.innerHTML = txtStr;
    
  • 在根新建txt-loader.js,并编写以下代码

    module.exports = (resouce) => {
        const result = JSON.stringify(resouce);
        return `export default ${result}`;
    };
    

    resouce参数是文件的文件内容,以字符串形式传入,我们只需要将其转成 esmodule 模块导出即可。

  • webpack.config.js中添加配置

    module: {
            rules: [
                { test: /\.txt/, use: ["./txt-loader.js"] },
            ],
    },
    
  • npm run build,看下构建产物

    const __WEBPACK_DEFAULT_EXPORT__ = ("test\ntest\ntest\ntest\ntest"); // 文件内容
    
  • 大功告成

总结

loader是webpack中处理不同资源的核心机制,目的是将资源文件转化成webpack能理解的js代码。在配置loader的过程我们需要注意,use数组是从从右往左,从下往上顺序处理文件。

开发中常用的loader

样式:style-loader、css-loader、less-loader、sass-loader等

文件:raw-loader、file-loader 、url-loader等

编译:babel-loader、coffee-loader 、ts-loader等

校验测试:mocha-loader、jshint-loader 、eslint-loader等

Plugin

Plugin(插件) 是 webpack 生态的的一个关键部分。它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理

插件常见的应用场景:

  • 实现自动在打包前清除dist目录
  • 自动生成应用所需要的html文件
  • 拷贝目录
  • 压缩代码
  • ...

Plugin的基本配置

使用clean-webpack-plugin清除打包之前的dist下的文件,使用html-webpack-plugin插件自动生成index.html并自动导入bundle.js,使用copy-webpack-plugin插件将没被打包的目录复制到dist目录下

目录结构

|__dist
    |__test.icon // 测试用文件,看插件是否生效
|__src
    |__ index.js // 项目入口文件
|__index.html // 外部访问入口
|__static
    |__ index.ico
|__webpack.config.js  // webpack显式配置文件
|__package.json // 项目配置与依赖

首先安装 插件npm i clean-webpack-plugin html-webpack-plugin --save-dev

// import { Configuration } from "webpack";
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");

/**
 * @type { Configuration }
 */
const config = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
    mode: "none",
    module: {
        rules: [
            { test: /\.css$/, use: ["style-loader", "css-loader"] },
            { test: /\.md/, use: ["./md-loader.js"] },
            { test: /\.txt/, use: ["./txt-loader.js"] },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: "webpack-test",
        }),
        new CopyWebpackPlugin({
            patterns: [{
                from: path.resolve(__dirname, "static"),
            }, ],
        }),
    ],
};

module.exports = config;

plugins是一个数组,只需要将配置的插件导入并创建即可。

npm run build 运行,查看dist目录,看到dist目录只有一个bundle.js,test.icon已经被删除,并将static目录下的index.ico复制到dist目录下,同时还生成了index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>webpack-test</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="bundle.js"></script></head>
  <body>
  </body>
</html>

plugin 工作工程

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

——「深入浅出 Webpack」

webpack处理plugin采用钩子机制,webpack在编译的过程会触发一系列的Tapable钩子事件,plugin所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

plugin过程详细介绍plugin

开发一个plugin

开发一个输出文件名称的fileList文件

思路:

  • 显然这个操作需要在文件生成到dist目录之前进行,所以我们要注册的是Compiler上的emit钩子。
  • emit 是一个异步串行钩子,我们用 tapAsync 来注册。
  • emit 的回调函数里我们可以拿到 compilation 对象,所有待生成的文件都在它的 assets 属性上。
  • 通过 compilation.assets 获取我们需要的文件信息,并将其整理为新的文件内容准备输出。
  • 然后往 compilation.assets 添加这个新的文件。
// file-list-webpack-plugin.js
module.exports = class FileListWebpackPlugin {
    constructor(options = {}) {
        this.filename = options.filename || "fileList.txt";
    }
    apply(compiler) {
        compiler.hooks.emit.tapAsync("FileListWebpackPlugin", (compilation, cb) => {
            let content = "";
            for (let filename in compilation.assets) {
                content += `- ${filename}\n`;
            }
            compilation.assets[this.filename] = {
                // 写入新文件的内容
                source: function() {
                    return content;
                },
                // 新文件大小(给 webapck 输出展示用)
                size: function() {
                    return content.length;
                },
            };
            cb();
        });
    }
};
// ./webpack.config.js
// import { Configuration } from "webpack";
const FileListWebPlugin = require("./file-list-webpack-plugin");
/**
 * @type { Configuration }
 */
const config = {
    plugins: [
        new FileListWebPlugin(),
    ],
};
module.exports = config;

总结

webpack为每一个工作环节都预留的合适的钩子,扩展时只需要找到合适的时间去做合适的事情。

工作原理剖析

  1. webpack cli 启动打包流程,获取命令行参数与webpack.config.js配置整合到一起,形成完成的配置对象
  2. 加载Webpack 核心模块,创建Compiler对象
  3. 使用Compiler对象开始编译整个项目
    1. 触发run 钩子。 开始构建整个应用,主要创建了compilation对象(一次编译的上下文,存放构建过程中的全部资源与配置)
    2. 触发make钩子。根据entry配置找到入口模块,开始一次递归出所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同的loader处理
      1. SingleEntryPlugin中调用了Compilation对象的addEntry方法,开始解析入口
      2. addEntry方法中又调用了_addModuleChain方法,将入口模块添加到模块列表中
      3. 再通过Compilation对象的buildModule方法进行模块构建
      4. buildModule方法中执行具体的loader,处理特殊资源加载
      5. build完成后,通过acorn库生成模块代码的AST语法树
      6. 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环build每个依赖
      7. 所有依赖解析完成,build阶段结束。
      8. 最后合并生成需要输出的bundle.js写入dist目录
  4. 从入口文件开始,解析模块依赖,形成依赖关系树
  5. 递归依赖树,将每个模块交给对应的loader处理
  6. 合并Loader处理完的结果,将打包结果输出到dist目录

Dev Server

webpack-dev-server是一个使用了express的Http服务器,它的作用主要是为了监听资源文件的改变,该http服务器和client使用了websocket通信协议,只要资源文件发生改变,webpack-dev-server就会实时的进行编译。

npm i webpack-dev-sever --save-dev

// package.json
"script": {
  "dev": "webpack serve"
}
// import { Configuration } from "webpack";
const path = require("path");

/**
 * @type { Configuration }
 */
const config = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
    devServer: {
        static: {
            directory: path.join(__dirname, "dist"),
        },
        compress: true,
        port: 8081,
        hot: true,
    },
    mode: "none",
};

module.exports = config;

SourceMap

Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。

有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。

这个文件是给浏览器的开发者工具使用的,开发者工具读取这个文件,便能找到对应源代码的位置信息等。

阮一峰Sourcemap

配置

// webpack.config.js
// ./webpack.config.js
// import { Configuration } from "webpack";
const path = require("path");

/**
 * @type { Configuration }
 */
const config = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
    devtool: "source-map"
};	

module.exports = config;

打包运行可以看到dist目录生成了两个文件

|__dist
	 |__bundle.js
	 |__bundle.js.map

在bundle.js最后一行可以看到如下代码,此行代码指示浏览器对应的sourcemap文件是bundle.js.map

//# sourceMappingURL=bundle.js.map

webpack其他的一些sourcemap模式

devtool取值初次构建重新构建适合生产环境品质
none最快最快
eval最快最快转换后代码
cheap-eval-source-map更快转换后代码(只有行信息)
cheap-modue-eval-source-map更快源代码(只有行信息)
eval-source-map最慢完整源代码
cheap-source-map转换后代码(只有 行信息)
cheap-module-source-map更慢源代码(只有行信息)
inline-cheap-source-map转换后代码(只有行信息)
inline-cheap-module-source-map更慢源代码(只有行信息)
hidden-source-map不会自动引入到打包代码后,单独生成一个文件
nosources-source-map只能看到错误位置,点击进去看不到源代码,保护生产环境源代码不暴露

HMR

模块热更新,修改代码后,只替换对应模块,不对整个应用替换。

配置

  • 运行webpack-dev-server命令时,通过—hot参数去开启这个特性

  • 配置文件方式:

    const webpack = require('webpack')
    devServer: {
      hot: true,
    },
    plugin: [
      new webpack.HotModuleReplacementPlugin()
    ]
    

js模块手动热更新

第一个参数就是editor模块的路径

第二个参数则需要我们传入一个函数

module.hot.accept('./editor', ()=> {
  console.log('editor 更新了')
})

Tree shaking

Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)

只有模块是通过 static 方式引用时,Tree Shaking 才会起作用,如es6的import,export

在 ES6 模块规范之前,我们使用require()语法的 CommonJS 模块规范。这些模块是 dynamic 动态加载的,这意味着无法应用 Tree Shaking,因为在实际运行代码之前无法确定需要哪些模块。

Tree-shaking实现的前提是ES Modules

在production模 式下,webpack已经自动开启了这个功能。我们也来了解一下具体的配置过程

  1. 开启tree shaking

    // ./webpack.config.js
    module.exports = {
      optimization: {
        // 模块只导出被使用的成员,未引用的不会被导出
        usedExports: true// 压缩输出结果,未引用代码将被去除。
        minimize: true,
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true
      }
    }
    

sideEffects

webpack允许我们通过配置的方式,去标识我们的代码是否有副作用,从而为Tree-shaking提供更大的压缩空间。 这里的副作用指的是模块执行时除了导出成员之外所做的事情。 sideEffects一般用于npm包标记是否有副作用。

sideEffects

Code Splitting

代码分割,按需加载对应的资源,降低启动成本,提高响应速度

Webpack实现分包的方式主要有两种:

  • 根据业务不同配置多个打包入口,输出多个打包结果
  • 结合ES Module 的动态导入(Dynamic Imports)特性,按需加载模块

多入口打包

// ./webpack.config.js
// import { Configuration } from "webpack";
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

/**
 * @type { Configuration }
 */
const config = {
    // 多入口写成对象的形式
    entry: {
        index: "./src/index/index.js",
        about: "./src/about/index.js",
    },
    output: {
        //[name]是占位符,其值是entry的key
        filename: "[name].bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
    module: {
        rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }],
    },
    plugins: [
        new CleanWebpackPlugin(),
        // 一个入口一个HtmlWebpackPlugin
        new HtmlWebpackPlugin({
            title: "webpack-index",
            filename: "index.html",
            // chunks,指定使用 index.bundle.js,注意,这里的值要与entry的key保持一致
            chunks: ["index"],
        }),
        new HtmlWebpackPlugin({
            title: "webpack-about",
            filename: "about.html",
            // chunks,指定使用 about.bundle.js
            chunks: ["about"],
        }),
    ],
    optimization: {
        splitChunks: {
            // 自动提取所有公共模块到单独的bundle中
            chunks: "all",
        },
    },
};

module.exports = config;