什么是webpack,webpack解决什么问题
webpack是一个静态模块打包工具,他能将前端模块的静态资源(js,css,image,tff等)进行打包成一个或多个bundles,用于在浏览器展示你的资源;在前端项目中更高效地管理和维护项目中的每一个资源
解决问题:。如:
- 能够将散落的模块打包到一起;如:文件合并,减少http请求等
- 能够编译代码中的新特性;如:es6->es5;
- 能够支持不同种类的前端资源模块;scss、less -> css等。image -> base64
如何使用webpack实现模块化打包
快速上手
-
创建项目
npm init -y- 安装webpack与webpackcli
npm install webpack webpackcli --save-dev
- 安装webpack与webpackcli
-
项目目录结构
// 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 -
根据以上配置后,命令行执行 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还会导出模块。
- 首先判断该模块是否已经加载过了,如果已经加载过了,则直接使用缓存。
- 新加载的模块新建一个对象
{exports: {}}(用于存储模块export内容),同时赋值给__webpack_module_cache__(缓存起来)与module - 执行
__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");
}
},
]
-
数组内容是一个函数,接收三个参数
__unused_webpack_module,__webpack_exports__(用于收集模块中export的变量),__webpack_require__(给模块提供__webpack_require__方法,比如,模块A引入了模块B,在A中可以通过该对象引入模块)。__webpack_require__.r给模块定义一个命名空间。__webpack_require__.d收集export变量依赖并赋值给__webpack_exports__。依赖模块使用通过闭包的方式访问被模块的变量。 -
执行完对应模块后,回到
__webpack_require__中,此时module.exports已经储存了模块export的变量了,可供其他模块访问。
(() => {
var _moduleA_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
(0,_moduleB__WEBPACK_IMPORTED_MODULE_1__["default"])();
})();
-
可以在产物中最后看到以上代码,此处代码即是
index.js的代码,也是项目代码的入口。- 在
index.js中声明了import moduleA from './moduleA',对应代码第一行代码,通过__webpack_require__导入一个模块。 - 然后执行这个模块
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属性是一个数组,数组每一个元素处理一种资源。元素配置如下
test属性,识别出哪些文件会被转换,以正则表达式匹配文件名,如以上表示匹配.css结尾的文件,已use中的loader去处理这些文件。use属性,定义出在进行转换时,应该使用哪个 loader,顺序从右往左,【从下往上】(即先将css文件交给css-loader处理,处理完再交给style-loader处理,最后再交给webpack处理,生成到bundle.js以模块的形式导出使用)
除了在webpack.config.js配置,还可以通过
-
通过命令行参数方式,
webpack --module-bind 'css=style-loader!css-loader' -
内联方式
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可以找到以上代码,webpack将css-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为每一个工作环节都预留的合适的钩子,扩展时只需要找到合适的时间去做合适的事情。
工作原理剖析
- webpack cli 启动打包流程,获取命令行参数与webpack.config.js配置整合到一起,形成完成的配置对象
- 加载Webpack 核心模块,创建Compiler对象
- 使用Compiler对象开始编译整个项目
- 触发run 钩子。 开始构建整个应用,主要创建了compilation对象(一次编译的上下文,存放构建过程中的全部资源与配置)
- 触发make钩子。根据entry配置找到入口模块,开始一次递归出所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同的loader处理
- SingleEntryPlugin中调用了Compilation对象的addEntry方法,开始解析入口
- addEntry方法中又调用了_addModuleChain方法,将入口模块添加到模块列表中
- 再通过Compilation对象的buildModule方法进行模块构建
- buildModule方法中执行具体的loader,处理特殊资源加载
- build完成后,通过acorn库生成模块代码的AST语法树
- 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环build每个依赖
- 所有依赖解析完成,build阶段结束。
- 最后合并生成需要输出的bundle.js写入dist目录
- 从入口文件开始,解析模块依赖,形成依赖关系树
- 递归依赖树,将每个模块交给对应的loader处理
- 合并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就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
这个文件是给浏览器的开发者工具使用的,开发者工具读取这个文件,便能找到对应源代码的位置信息等。
配置
// 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已经自动开启了这个功能。我们也来了解一下具体的配置过程
-
开启tree shaking
// ./webpack.config.js module.exports = { optimization: { // 模块只导出被使用的成员,未引用的不会被导出 usedExports: true, // 压缩输出结果,未引用代码将被去除。 minimize: true, // 尽可能合并每一个模块到一个函数中 concatenateModules: true } }
sideEffects
webpack允许我们通过配置的方式,去标识我们的代码是否有副作用,从而为Tree-shaking提供更大的压缩空间。 这里的副作用指的是模块执行时除了导出成员之外所做的事情。 sideEffects一般用于npm包标记是否有副作用。
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;