Webpack 概念
Webpack 是基于模块化的打包工具,他把一切文件都视为模块。 Webpack 通过开发时态的入口文件开始,通过模块化导入分析出所有依赖模块,经过一系列的处理过程,最终将开发时态的打包为运行时态的代码。 特点:
- 为前端工程化而生:解决工程化开发的问题,让开发者注重开发业务代码,而工程化的问题交给 Webpack 处理就行
- 简单易用:支持零配置,可以不用写一行配置代码,就能用 webpack。webpack 默认打包的入口文件是
src/index.js - 强大的生态:Webpack 非常灵活、可扩展,其本身的功能并不能,但其强大的扩展(插件)机制,使我们可以用第三方库来增强 Webpack 的能力
- 基于 Nodejs:Webpack 打包需要读写文件
- 基于模块化:Webpack 在打包过程通过模块化导入语句来分析依赖关系的,它支持各种模块化规范,包含 Commonjs、ES6 Module、UMD等
Webpack 使用需要强制安装两个包:
- webpack:核心包,包含 webpack 构建过程需要的所有 API
- webpack-cli:webpack 的 cli 工具,通过调用 webpack 核心包的 API,来完成构建过程
安装:全局安装、本地安装(推荐)
简单的打包流程

Webpack 多个模块化混合使用
Webpack 是同时支持多个模块化同时使用的,常用的是两个:Commonjs、ES6 Module。不管是哪个模块化,最终 模块导出的是一个对象,模块导入的也是当前模块导出的对象。
相同模块化


模块化混用


Webpack 打包结果分析
- 先通过主模块分析出所有依赖模块,封装成 key 为模块路径,value 为模块代码的对象,传到 IIFE modules 中
- 定义模块缓存对象,所以相同的模块只会执行一次
- 定义
__webpack_require__函数,用于运行返回和缓存模块导出内容 - 从执行入口模块开始,执行所有运行时代码
(function (modules) { // 2.modules 所有模块的集合
// 3.缓存模块
var installedModules = {};
// ===> 运行 & 缓存模块,最后返回模块的导出结果
function __webpack_require__(moduleId) {
// 5.判断是否有缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 6.创建模块对象,并缓存对象的引用
var module = installedModules[moduleId] = {
exports: {}
};
// 7.执行模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 8.返回模块导出结果 module.exports
return module.exports;
}
// 4.执行入口模块
return __webpack_require__("./src/index.js");
})(
// 1.所有模块的集合
{
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
console.log('sync', _sync__WEBPACK_IMPORTED_MODULE_0__["default"]);
}),
"./src/sync.js": (function (module, __webpack_exports__, __webpack_require__) {
const data = 'sync'
// __webpack_exports__ === module.exports
__webpack_exports__["default"] = (data);
})
}
);
Webpack 编译过程(粗粒度)
初始化
通过 cli、配置文件、默认配置生成 webpack 最终的配置
编译
创建 chunk
根据 webpack 配置的入口文件创建一个 chunk(多个入口会创建多个 chunk)
chunk 有两个属性: name:chunk 的名称,默认为 main id:开发环境是 name,生产环境是数字
构建依赖

通过入口开始分析依赖(AST 分析),并生成对应的模块记录,模块记录包含模块 id 和转换后的代码
生成 chunk 资源列表
通过配置的 chunk 生成最终的 chunk 文件名和文件内容,并创建文件内容的 hash(chunkhash)。

chunk 合并
将所有 chunk 资源合并为一个总的 assets 资源,并生成一个 hash 值

输出
通过 node 的 fs 模块,将总的 assets 资源列表生成相应的文件到 output 目录下。
Webpack 基本配置
devtool
汉译为开发工具,就是方便我们开发调试需要的工具。在 Webpack 指的是 source-map(源码地图)。
我们运行的代码是打包后的代码,打包后的代码不是人看的,代码出错了,并不能定位到源代码哪个位置出错了,所以需要一个工具来帮助我们在运行时定位到源码(开发时代码)的位置,这个工具就是 Source map。Source map 实际上就是一个配置文件,给浏览器看的,它包含我们的原始代码,并记录了运行时代码和源代码之间的对应关系

最佳实践:
- Source Map 最好在开发环境使用,可友好的调试代码

- Source Map 尽量不要再生产环境使用,因为他会暴露我们的源代码,并且体积也很大,还会带来额外的请求。如果要是用的话,需要去规避这些问题
webpack 中使用:
module.exports = {
mode: "development",
devtool: "eval",
};
几种常见 devtool:
- eval:映射的是 loader 转换后的源代码

- eval-source-map 对生成的 source-map 文件 base64 加密,并且添加到 eval 中,这种方式可以看到源代码和运行时代码的真是映射


- cheap-eval-source-map 类似 eval-source-map ,低开销的 source-map,显示转义后的代码,并且只能定位行,不能定位到列
entry & output
用于配置 入口文件和输出文件
const path = require("path");
module.exports = {
mode: "development",
entry: { // 多个入口会生成多个 chunk
main: "./src/index.js",
// 将多个入口打包到一个 chunk 中,入口加载时从前往后执行
test: ["./src/a.js", "./src/b.js"],
},
output: {
// chunkhash:5 原始 hash 很长,我们只需要截取前几位就行,5 就是截取前 5 位
filename: "[name].[chunkhash:5].js",
path: path.resolve(__dirname, "build"),
},
};
filename 占位符规则:
- name:chunkname,entry 中配置的 key
- id: 使用 chunkid 当 name,不推荐,生产环境 chunkid 是数字
- hash:总资源的 hash,多个 chunk 资源文件的 hash 是一样的,如果一个文件内容改变,所有 chunk 资源文件的 hash 都会变

- chunkhash:使用 chunk hash,每个 chunk 资源文件的 hash 不同,修改的文件和其他 chunk 没有依赖关系,就不会改变 chunk 资源的 hash

loader
一个函数,参数是代码字符串,返回值是一个新的(修改后的)代码字符串。在文件读取后,AST 处理之前执行

使用场景:webpack ast 语法分析只支持 js 语法,所以需要 loader 来将不是 js 语法的代码文件,转为 js 语法的代码。比如处理 css、图片、字体图标等文件,在进行语法分析前都需要 loader 来处理;还可用来压缩源代码,反正基于源文件内容的修改都可用 loader 来实现。
loader 的使用
自定义一个 loader:替换匹配的字符串为 Hello,匹配项可自定义
loader-utils 可以处理配置项 options 的参数
const loaderUtils = require("loader-utils");
// 替换 replaceText 属性值为 Hello
module.exports = function (sourceCode) {
const options = loaderUtils.getOptions(this);
return sourceCode.replace(new RegExp(options.replaceText, "g"), "Hello");
};
module.exports = {
mode: "development",
module: { // 针对模块处理的配置
rules: [// 定义模块匹配规则,模块匹配指的是模块 ID(文件的相对路径) 的匹配
{
test: /index\.js$/, // 正则匹配模块 ID
use: [// 匹配的文件使用 loader 处理
{
loader: "./lib/loaders/custom-loader.js",
options: {
replaceText: "你好",
},
},
],
},
{
test: /\.js$/,
// 等效 上面 loader 的写法
use: ["./lib/loaders/custom-loader.js?replaceText=你好"],
},
],
},
};
loader 的处理流程
当前模块匹配的 loader 会形成一个数组,loader 依次自右向左执行 loader 的配置上的处理顺序:从上到下匹配,从下往上、从右到左依次执行

loader 小练习
loader 一个重要的作用是将模块转为 js 语法,所以可以尝试自己实现一下
处理 CSS 文件
方法:将 css 代码用 js 动态插入到 head 标签内
module.exports = function (sourceCode) {
return `
var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.getElementsByTagName("head")[0].appendChild(style);
modules.exports = \`${sourceCode}\`;
`;
};

处理图片
方法:必须拿到源文件的 buffer (二进制文件),然后小文件用 base64 ,大文件生成到打包目录下
const loaderUtils = require("loader-utils");
function imageLoader(buffer) {
const { limit = 1000, name = "[contenthash:5].[ext]" } =
loaderUtils.getOptions(this);
let content = "";
if (buffer.byteLength >= limit) {
content = getFilePath.call(this, buffer, name);
} else {
content = getBase64(buffer);
}
return `module.exports = \`${content}\``;
}
// 将 buffer 转为 base64 格式
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
// 将图片输出到总资源 assets,并返回文件路径
function getFilePath(buffer, name) {
// 按照规则生成文件名字
const filename = loaderUtils.interpolateName(this, name, {
content: buffer,
});
// 可以将文件输出的打包目录下
this.emitFile(filename, buffer);
return filename;
}
imageLoader.raw = true; // 处理原始 buffer
module.exports = imageLoader;

plugin
loader 主要负责的是代码转换,在文件被读取后执行。plugin 负责整个编译过程,loader 发生在文件打包之前,所以 plugin 使用面更广。 plugin 的本质是一个带有 apply 方法的对象
const plugin = {
apply: function(compiler){}
};
但通常会写成 类的形式,更方便增强插件的功能
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
//事件处理函数
})
}
}
插件的 apply 放在是在初始化 Complier 后执行的,整个编译完成打包工作是 compiler 内部创建的 compilation 完成的,每一次编译都会重新生成一个 compilattion,而 compiler 只会在初始化时创建,插件的 apply 方法也是只执行一次。 apply 执行的目的是,将我们写的事件函数注册到 webpack 的钩子上

一个简单地 plugin
功能:生成打包清单
将文件生成到打包目录下,只需要在总资源列表添加一个文件对象,必须包含属性 source 和 size 方法
module.exports = class FileListPlugin {
// 初始化生成的文件名
constructor(filename) {
this.filename = filename;
}
apply(compiler) {
// 生成资源到 ouput 目录之前
compiler.hooks.emit.tap("FileListPlugin", (compilation) => {
const assets = compilation.assets; // 所有生成的资源文件的信息
let fileContent = [];
Object.keys(assets).forEach((key) => {
fileContent.push(
`【文件】${key}\n【大小】${assets[key].size() / 1000} KB`
);
});
const str = fileContent.join("\n\n");
// 往最终生成的资源中,添加一个文件
// 格式也必须是这样
compilation.assets[this.filename || "filelist.txt"] = {
source() { // 文件内容
return str;
},
size() { // 文件大小
return str.length;
},
};
});
}
};
注册
const FileListPlugin = require("./lib/plugins/FileListPlugin");
module.exports = {
mode: "development",
plugins: [new FileListPlugin("filelist.md")],
};

env
我们需要区分开发环境和生产环境的 webpack 配置,这时可以通过命令行参数 env 来传 env 的定义方式不同,对应输出的 结果也会不同,通常用于判断环境可以用 第一种方式,若需要定义其他变量可以使用第二种
webpack --env development:{ development: true }webpack --env development=true:{ development: "true" }webpack --env.development``webpack --env.development=truewebpack 5 不支持这两种写法
webpack 的配置也需要该问函数的形式,参数是 env,返回值是 webpack 的配置对象
const baseConfig = require("./webpack.base"); // 公共配置
const devConfig = require("./webpack.dev"); // 开发环境配置
const proConfig = require("./webpack.pro"); // 生产环境的配置
module.exports = function (env) {
if (env.prod) {
return {
...baseConfig,
...proConfig
}
} else {
return {
...baseConfig,
...devConfig
}
}
}
publicPath
是一个字符串,拼接在打包后文件路径的前面publicPath + 文件路径,其实 publicPath 并没有什么实际意义,用不用的到是 plugin 和 loader 自己控制的。
console.log(__webpack_public_path__);
// 打包后
console.log(__webpack_require__.p);
设置可以在,output 中直接设置,但如果有的扩展用到 publicPath,可能会出现文件找不到的情况,所以需要注意下