# webpack_init
**Repository Path**: wang_xi_long/webpack_init
## Basic Information
- **Project Name**: webpack_init
- **Description**: 学习webpack相关记录
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-07-31
- **Last Updated**: 2022-08-07
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
[toc]
## 一、webpack4 基础知识
**webpack4 与 webpack5 相差较大, 遇到配置不一样的可以查官方文档**
### 1. 安装 webpack
- 安装本地的 webpack
- webpack webpack -D
- yarn init -y
- yarn add webpack webpack-cli -D
### 2. webpack 可以进行 0 配置
- 打包工具 -> 输出后的结果(js 模块)
- 打包(支持 js 模块)
- module.exports 导出
- require 导入
- 新建 src/index.js
- npx webpack -> 会打包出 dist/main.js
### 3. 手动配置 webpack
- 默认配置文件的名字 webpack.config.js
- 打包的文件解析流程: 将所有解析的模块变成一个对象, 通过一个唯一入口, 加载模块, 依次实现递归依赖关系, 通过入口来运行所有的文件
1. 修改配置文件的名字
- 修改文件名 `webpack.config.my.js`
- ①: `npx webpack --config webpack.config.my.js`
- ②: 在 package.json 文件中修改文件名 `scripts.build: "webpack --config webpack.config.my.js"`
- 通过 npm run build 执行
- npm run build -- --config webpack.config.my.js 添加--可以传参
```json
{
"scripts": {
"build": "webpack --config webpack.config.my.js"
}
}
```
2. 使用 html 插件(使用开发服务器插件)
- 安装: yarn add webpack-dev-server -D
- ①:npx webpack-dev-server
②:在 package.json 中添加 `script.dev: "webpack-dev-server"`
- webpack.config.js 中添加 devServer: { port: 3000, progress: true }
- npm run dev 执行
- 配置 devServer 参数
```js
module.exports = {
// 开发服务器的配置
devServer: {
port: 3000, // 端口号, 默认8080
// progress: true, // 进度条
// contentBase: "./build", // 代理静态资源路径
client: {
progress: true, // 进度条
},
static: {
publicPath: "./build", // 代理静态资源路径
},
compress: true, // 开启gzip压缩
},
};
```
```json
{
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack-dev-server"
}
}
```
### 4. 动态生成 html 入口文件
1. 安装: yarn add html-webpack-plugin
- 引入:
- plugins: 使用
```js
// 1. 引入
let HtmlWebpackPlugin = require("html-webpack-plugin");
// 2. 使用
module.exports = {
// 数组 放着所有的webpack插件
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.hmtl", // 模板
filename: "index.html", // 文件名
}),
],
};
```
2. 新建 src/index.html 模板
```html
webpack
```
3. 修改 webpack.config.js
```js
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 开发服务器的配置
devServer: {
port: 3000, // 端口号, 默认8080
// progress: true, // 进度条
// contentBase: "./build", // 代理静态资源路径
client: {
progress: true, // 进度条
},
static: {
publicPath: "./build", // 代理静态资源路径
},
compress: true, // 开启gzip压缩(production)
},
mode: "production", // 模式 默认两种 production development
entry: "./src/index.js", // 入口
output: {
filename: "bundle.js", // 打包后的文件名
path: path.resolve(__dirname, "build"), // 路径必须是一个绝对路径
},
// 数组 放着所有的webpack插件
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", // 模板
filename: "index.html", // 文件名
minify: {
removeAttributeQuotes: true, // 删除index.html中的双引号
collapseWhitespace: true, // 压缩成一行
},
}),
],
};
```
4. 执行`npm run build`, 会生成对应的压缩的`build/bundle.js`和`build/index.html`
### 5. 加载 CSS 样式
- webpack 默认只能加载 js
**css**
1. 新建 src/index.css
```css
body {
width: 100vw;
height: 100vh;
background: red;
}
```
2. 引入 index.js
```js
require("./index.css");
```
3. 修改 webpack.config.js
```js
module.exports = {
// 模块
module: {
// 规则
rules: [
{
test: /\.css$/,
use: [
{
loader: "style-loader",
options: {
// insertAt: 'top', // webpack4
insert: "top", // webpack5
},
},
"css-loader",
],
},
],
},
};
```
- `css-loader` 是处理@import 这种语法的
- `style-loader` 是把 css 插入到 head 标签中
- loader 功能单一
- loader 的用法
- 一个 loader 用字符串
- 多个 loader 用字符串数组
- 多个 loader 带参数用对象数组
**less**
1. 安装 less 及 less-loader
```
yarn add less less-loader -D
```
2. 写入 less 编译规则
```js
module.exports = {
// 模块
module: {
// 规则
rules: [
{
test: /\.css$/,
use: [
{
loader: "style-loader",
options: {
// insertAt: 'top', // webpack4
insert: function insertAtTop(element) { // webpack5
var parent = document.querySelector("head");
// eslint-disable-next-line no-underscore-dangle
var lastInsertedElement =
window._lastElementInsertedByStyleLoader;
if (!lastInsertedElement) {
parent.insertBefore(element, parent.firstChild);
} else if (lastInsertedElement.nextSibling) {
parent.insertBefore(element, lastInsertedElement.nextSibling);
} else {
parent.appendChild(element);
}
// eslint-disable-next-line no-underscore-dangle
window._lastElementInsertedByStyleLoader = element;
},,
}
},
"css-loader",
],
},
{
// 可以出来less文件 sass stylus node-sass sass-loader stylus stylus-loader
test: /\.less$/,
use: [
{
loader: "style-loader",
options: {
// insert: () => "top", // webpack4
insert: function insertAtTop(element) { // webpack5
var parent = document.querySelector("head");
// eslint-disable-next-line no-underscore-dangle
var lastInsertedElement =
window._lastElementInsertedByStyleLoader;
if (!lastInsertedElement) {
parent.insertBefore(element, parent.firstChild);
} else if (lastInsertedElement.nextSibling) {
parent.insertBefore(element, lastInsertedElement.nextSibling);
} else {
parent.appendChild(element);
}
// eslint-disable-next-line no-underscore-dangle
window._lastElementInsertedByStyleLoader = element;
},
},
},
"css-loader",
"less-loader",
],
},
],
},
};
```
3. 新建 src/index.less, 写 less 样式
```less
body {
div {
width: 100px;
height: 100px;
background: #000;
color: #fff;
border: 1px solid #f600ff;
}
}
```
4. src/index.js 引入 less
```js
require("./index.less");
```
5. 重新 `npm run dev`
**将 css 打包成统一的文件**
1. 安装插件 `mini-css-extract-plugin`
```
yarn add mini-css-extract-plugin -D
```
2. webpack.config.js 引入插件
```js
let MiniCssExtractPlugin = require("mini-css-extract-plugin");
```
3. 添加到插件里
```js
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "main.css",
}),
],
};
```
4. 使用
```js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
```
**注意:** 使用`mini-css-extract-plugin` webpack5 默认将 style 样式插入到下面了, index 页面写的重复样式会不生效
**样式添加游览器内核**
1. 添加 index.less 测试样式
```css
body {
div {
width: 100px;
height: 100px;
background: #000;
color: #fff;
border: 1px solid #f600ff;
transform: rotate(45deg);
}
}
```
2. 安装 postcss-loader autoprefixer
```
yarn add postcss-loader autoprefixer -D
```
3. 新建 postcss.config.js
```js
module.exports = {
plugins: [require("autoprefixer")],
};
```
4. 使用 "postcss-loader"
```js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
},
],
},
};
```
**压缩 css 样式**
1. 安装 optimize-css-assets-webpack-plugin
```
yarn add optimize-css-assets-webpack-plugin -D
```
2. webpack.config.js 引入及使用
```js
// 1. 引入
let OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
// 2. 使用
module.exports = {
optimization: {
minimizer: [new OptimizeCssAssetsWebpackPlugin()],
},
};
```
3. 打包 `npm run build`, css 已经压缩, 但 js 却出了问题
**压缩 js**
1. 安装 terser-webpack-plugin 或 uglifyjs-webpack-plugin
```
yarn add terser-webpack-plugin -D
```
2. webpack.config.js 引入及使用
```js
// 1. 引入
let OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
let TerserJSPlugin = require("terser-webpack-plugin");
// 2. 使用
module.exports = {
optimization: {
minimizer: [new OptimizeCssAssetsWebpackPlugin(), new TerserJSPlugin()],],
},
};
```
或
```
yarn add uglifyjs-webpack-plugin -D
```
```js
// 1. 引入
let OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
let UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
// 2. 使用
module.exports = {
optimization: {
minimizer: [new OptimizeCssAssetsWebpackPlugin(), new UglifyjsWebpackPlugin({
cache: true, // 开启缓存
parallel: true, // 并发打包,一次打多包
sourceMap: true, // es6转es5源码调试映射
})],],
},
};
```
### 6. 转化 ES6 语法
**ES6 转 ES5**
1. 安装 babel 插件
```
yarn add babel-loader @babel/core @babel/preset-env -D
```
2. webpack.config.js 使用
```js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
};
```
3. 如果不支持 class 语法(webpack5 已经内置)
```
yarn add @babel/plugin-proposal-class-properties -D
```
```js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],,
plugins: [
'@babel/plugin-proposal-class-properties'
]
},
},
},
],
},
};
```
**装饰器**
1. index.js 添加装饰器
```js
@log
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const p = new Person("前端小溪", 26);
console.log(p.name + "------------" + p.age);
function log(target) {
console.log(target, "123");
}
```
2. 安装支持插件
```
yarn add @babel/plugin-proposal-decorators -D
```
3. 使用 html
```js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],,
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
],
},
},
},
],
},
};
```
### 处理 js 语法及校验
**处理打包方法不共用及内置 api 无法转化问题**
1. 安装
```
yarn add @babel/plugin-transform-runtime
```
2. 加入插件中
```js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],,
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
"@babel/plugin-transform-runtime",
],
},
},
include: path.resolve(__dirname, "src"), // 解析src下js文件
exclude: /node_modules/, // 排除node_modules的js文件
},
],
},
};
```
3. npm run build
**转化 includes 等 api**
1. 安装
```
yarn add @babel/polyfill
```
2. 在需要的文件中添加 index.j
```js
require("@babel/polyfill");
let inA = "前端小溪".includes("小溪");
console.log(inA);
```
**代码校验**
1. 安装
```
yarn add eslint eslint-loader -D
```
2. 定制 eslint 规范, 并下载对应.eslintrc.json 文件,加入到项目里
https://eslint.org/play/
3. 添加到 webpack.config.js 解析规则中
```js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "eslint-loader",
options: {
enforce: "pre", // 放到前面
},
},
},
],
},
};
```
### 7. 全局变量引入
**使用 jquery**
1. 安装
```
yarn add jquery -D
```
2. 引入
```
import $ from "jquery";
console.log($);
```
**全局使用 window.$ expose-loader**
1. 安装 expose-loader 插件,暴露全局的 loader
```
yarn add expose-loader -D
```
2. 使用方式一 index.js
```js
import $ from "expose-loader?exposes=$,jQuery!jquery";
```
3. 使用方式二
```js
// index.js
import $ from "jquery";
```
```js
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: require.resolve("jquery"),
loader: "expose-loader",
options: {
exposes: ["$", "jQuery"],
},
},
],
},
};
```
- 相当于按需加载, 但一处加载后, window.$ 就可以访问
**全局使用 $ webpack**
1. webpack.config.js 引入使用
```js
let webpack = require("webpack");
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
}),
],
};
```
- 全局使用 $ 可以取到 jquery 实例
- 但不能使用 window.$ 获取 jquery 实例
**CDN 方式**
1. 在 template 里 index.html 引入 jquery
```html
```
2. 配置打包忽略依赖的 jquery
```js
// webpack.config.js
new webpack.ProvidePlugin({
$: "jquery",
}),
```
**全局变量引入**
1. expose-loader 暴露到 window 上
2. providePlugin 给每一个模块提供一个 $
3. cdn 等引入, 忽略打包
**loader 类型**
1. pre 前面执行的 loader
2. normal 普通的 loader
3. 内联的 loader
4. postloader 后置
### 8. 图片处理
**在 js 中创建拖来引入**
1. index.js
```js
let image = new Image();
image.src = "./xiaoxi.jpg"; // 图片不显示
document.body.appendChild(image);
```
2. 使用 `file-loader`
```
yarn add file-loader -D
```
3. 替换
```js
// 把生成的图片地址返回
import xiaoxi from "./xiaoxi.jpg";
let image = new Image();
image.src = xiaoxi; // 图片不显示
document.body.appendChild(image);
```
- file-loader 默认会在内部生成一张图片, 到 build 目录下
4. 添加编译规则 webpack.config.js
```js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: "file-loader",
},
],
},
};
```
**在 css 引入, background('url')**
1. 引入
```css
background: url("./xiaoxi.png");
```
2. css-loader 会进行解析
**
**
1. 因为 url 是绝对路径, url 只是一个字符串
```
yarn add html-withimg-loader -D
```
2. 添加规则
```js
module.exports = {
module: {
rules: [
{
test: /\.html$/,
use: "html-withimg-loader",
},
],
},
};
```
**html-withimg-loader 失效**
**使用 url-loader**
1. 安装
```
yarn add url-loader -D
```
2.
```js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: "url-loader",
options: {
limit: 200 * 1024, // 200kb以下打包成base64, 以上产生原文件
},
},
},
],
},
};
```
### 9. 打包文件分类
**css 分类**
1. plugins 中设置路径
```js
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "css/main.css",
}),
],
};
```
**图片分类**
```js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: "url-loader",
options: {
limit: 200 * 1024, // 200kb以下打包成base64, 以上产生原文件
outputPath: "img/", // 打包分类
publicPath: "http://www.xiaoxi.work", // 追加前缀
esModule: false, // 不使用esmodel
},
},
},
],
},
};
```
## 二、webpack4 基础知识
### 1. 打包多页应用
```js
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
home: "./src/home.js",
about: "./src/about.js",
},
output: {
filename: "[name].js", // [name] home,about
path: path.resolve(__dirname, "build"),
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", // 模板
filename: "home.html", // 文件名
chunks: ["home"], // home.html里只加载home.js
}),
new HtmlWebpackPlugin({
template: "./src/index.html", // 模板
filename: "about.html", // 文件名
chunks: ["about"], // about.html里只加载about.js
}),
],
};
```
### 2. 配置 source-map
- 当错误语法, 通过打包 ES6 转 ES5 后(如生产环境), 报错需要溯源,
1. 方式一: source-map
源码映射, 会单独生成一个 sourcemap 文件, 出错了会标记 当前报错的列和行,大而全, 方便帮我们调试源代码,
```js
module.exports = {
devtool: "source-map",
};
```
2. 方式二: eval-source-map
不会产生单独的文件, 但是可以显示行和列
```js
module.exports = {
devtool: "eval-source-map",
};
```
3. 方式三: cheap-module-source-map
不会产生列, 但是是一个单独的映射文件, 产生后可以保留起来
```js
module.exports = {
devtool: "cheap-module-source-map",
};
```
4. 方式四: eval-cheap-module-source-map
不会产生文件, 集成在打包后的文件中,不会产生列
```js
module.exports = {
devtool: "eval-cheap-module-source-map",
};
```
### 3. watch 监听文件
1. 配置 watch
```js
module.exports = {
watch: true, // 开启热更新
watchOptions: {
// 监控的选项
poll: 1000, // 每秒
aggregateTimeout: 500, // 防抖 持续输入不更新
ignored: /node_modules/, // 不需要监控的文件
},
};
```
### 4. webpack 小插件应用
**清除文件**
为了在每次打包发布时自动清理掉 dist 目录中的旧文件
1. 安装 CleanWebpackPlugin
```
yarn add clean-webpack-plugin -D
```
2. webpack5 引入及使用
```js
let { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [new CleanWebpackPlugin()],
};
```
**复制文件**
1. 安装 CopyWebpackPlugin
```js
yarn add copy-webpack-plugin -D
```
2. 引入及使用
```js
let path = require("path");
let CopyWebpackPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
// "README.md" // 将README.md拷贝到dist下
// { from:"README.md", to: './' }, // 将README.md拷贝到dist下
// path.resolve(__dirname, "src", "file.ext"), // 将src/file.ext拷贝到dist下
{ from: "source", to: "dest" }, // 将source目录下文件拷贝到dist/dest目录下
],
}),
],
};
```
**插入版本信息**
1. bannerPlugin 是 webpack 内置插件
```js
let webpack = require("webpack");
```
2. 使用
```js
module.exports = {
plugins: [
new webpack.BannerPlugin(
"Copyright 2002-2022 the original author or authors.Licensed under the Apache License, Version 2.0 (the 'License');"
),
],
};
```
### 5. webpack 跨域问题
1. 编写一个服务器 server.js
```js
let express = require("express");
let app = express();
app.get("/use", (req, res) => {
res.json({ name: "前端小溪" });
});
app.listen(3000);
```
2. 请求接口 index.js
```js
let xhr = new XMLHttpRequest();
xhr.open("GET", "/user", true);
xhr.onload = function () {
console.log(xhr.response);
};
xhr.send();
```
3. 此时出现了跨域, 设置跨域
```js
module.exports = {
devServer: {
// 重写的方式, 把请求代理到express服务器上
proxy: {
// '/api': 'http://localhost:3001', // 配置一个代理
"/api": {
target: "http://localhost:3001",
pathRewrite: { "/api": "" }, // 将/api去掉
},
},
},
};
```
- 此时已经在开发环境已经解决了跨域
4. 如果前端只想单纯来模拟数据
```js
module.exports = {
devServer: {
// 提供的钩子函数
onBeforeSetupMiddleware({ app }) {
app.get("/user", (req, res) => {
res.json({ name: "前端小溪 -- 模拟数据" });
});
},
},
};
```
5. 使用中间件将打包的页面挂载到服务器上(在服务端启动 webpack)
安装
```
yarn add webpack-dev-middleware -D
```
```js
let express = require("express");
let app = express();
let webpack = require("webpack");
//中间件
let middle = require("webpack-dev-middleware");
let config = require("./webpack.config.js");
console.log(config);
let compiler = webpack(config);
app.use(middle(compiler));
app.get("/user", (req, res) => {
res.json({ name: "前端小溪" });
});
app.listen(3001, () => {
console.log("3000已启动");
});
```
- 这样在 3000 端口就可以看到页面了, 也能正常使用接口
### 6. resolve 属性的配置
```js
module.exports = {
// 解析, 第三方包 common
resolve: {
modules: [path.resolve("node_modules")],
// 别名 vue -> vue.runtime
alias: {
bootstrap: "bootstrap/dist/css/bootstrap.css",
},
extensions: [".js", ".css", ".json", ".vue"], // 不写后缀, 匹配规则
mainFields: ["style", "main"], // 先去style,再去main文件
mainFiles: [], // 入口文件的名字 index.js
},
};
```
### 7. 定义环境变量
1. webpack 内置插件
```js
let webpack = require("webpack");
module.exports = {
plugins: [
new webpack.DefinePlugin({
// ENV: "'development'", // 需要双层
ENV: JSON.stringify("development"),
FLAG: "true",
ADD: "1+1",
}),
],
};
```
2. index.js 使用
```js
console.log(ENV, ENV === "development"); // development true
console.log(typeof FLAG); // boolean
console.log(ADD); // 2
```
### 8. 区分不同环境
1. 基础环境 webpack.base.js
```js
let path = require("path");
module.exports = {
entry: "./src/index.js", // 入口
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"), // 路径必须是一个绝对路径
},
};
```
2. 开发环境 webpack.dev.js
```js
let { merge } = require("webpack-merge");
let base = require("./webpack.base.js");
module.exports = merge(base, {
mode: "development",
devServer: {},
devtool: "source-map",
});
```
3. 生产环境 webpack.prod.js
```js
let { merge } = require("webpack-merge");
let base = require("./webpack.base.js");
let OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
let TerserJSPlugin = require("terser-webpack-plugin");
module.exports = merge(base, {
mode: "production",
optimization: {
minimizer: [new OptimizeCssAssetsWebpackPlugin(), new TerserJSPlugin()],
},
});
```
## 三、webpack4 基础知识(优化)
### 1. noParse 不需要解析的依赖库
1. 安装依赖
```
yarn add @babel/preset-react
```
2. 引入 jQuery
```js
import jquery from "jquery";
```
3. 设置 noParse
```js
let HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
module: {
noParse: /jquery/, // 不去解析jquery的依赖库
rules: [
test: /\.js$/,
exclude: /node_modules/, // 不解析的文件
include: path.resolve('src'), // 只解析
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
]
}
}
]
}
}
```
### 2. IgnorePlugin 忽略第三方包指定目录不被打包
1. 安装 moment
```
yarn add moment
```
2. 使用 moment
```js
import moment from "moment";
// 手动引入所需要的语言
import "moment/locale/zh-cn";
// 设置语言
moment.locale("zh-cn");
let r = moment().endOf("day").fromNow();
console.log(r);
```
3. 忽略文件
```js
module.exports = {
plugins: [
// 忽略 moment 的locale
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
],
};
```
### 3. dllPlugin 动态链接库
> 大型项目用,不建议生产环境用
通过 DllPlugin 打包出的 dll 文件
通过 DllRefrencePlugin 使用 dll 文件
1. 安装 react
```
yarn add react react-dom @babel/preset-react -D
```
2. 引入 React
```js
import React from "react";
import { render } from "react-dom";
const element = Hello, 前端小溪
;
render(element, document.getElementById("root"));
```
3. 使用 preset-react
```js
module.exports = {
// 模块
module: {
// 规则
rules: [
{
test: /\.js$/, // normal 普通的loader
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
],
},
};
```
4. 使用 webpack.config.react.js
```js
let path = require("path");
let webpack = require("webpack");
module.exports = {
mode: "development",
entry: {
react: ["react", "react-dom"],
},
output: {
filename: "_dll_[name].js", // 产生的文件名
path: path.resolve(__dirname, "dist"),
library: "_dll_[name]", // _dll_[name]
// libraryTarget: "commonjs", // umd/ commonjs
},
plugins: [
new webpack.DllPlugin({
name: "_dll_[name]",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
};
```
5. webpack.config.js 使用
```js
const DllReferencePlugin = require("webpack/lib/DllReferencePlugin");
module.exports = {
mode: "development", // 模式 默认两种 production development
entry: "./src/index.js", // 入口
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"), // 路径必须是一个绝对路径
},
// 数组 放着所有的webpack插件
plugins: [
new DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
// 模块
module: {
// 规则
rules: [
{
test: /\.js$/, // normal 普通的loader
use: {
// babel-loader 将ES6转ES5
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
],
},
};
```
6. manifest.json 是记录包的依赖映射关系,主要提供给 index.html 使用。
7. 先编译一次 webpack.config.react.js 在编译 webpack.config.js 即可实现 dll 优化
### 4. happypack 多线程打包
> 本身会浪费一些性能, 只有大型项目用
1. 安装 happypack
```
yarn add happypack -D
```
2. 引入并使用
```js
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
// 1. 引入
// 模块 happypack 可以实现多线程来打包
let Happypack = require("happypack");
let webpack = require("webpack");
module.exports = {
mode: "development", // 模式 默认两种 production development
entry: "./src/index.js", // 入口
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"), // 路径必须是一个绝对路径
},
plugins: [
// 3. 设置对应的loader
new Happypack({
id: "js",
loaders: [
{
loader: "babel-loader",
query: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
}),
// 3. 设置对应的loader
new Happypack({
id: "style",
threads: 4, // 开启的线程数
loaders: ["css-loader", "postcss-loader"],
}),
new HtmlWebpackPlugin({
template: "./src/index.html", // 模板
filename: "index.html", // 文件名
}),
],
// 模块
module: {
// 规则
rules: [
{
test: /\.js$/, // normal 普通的loader
// 2. 匹配
use: "Happypack/loader?id=js",
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
},
{
test: /\.css$/,
// 2. 匹配
use: "Happypack/loader?id=style",
},
],
},
};
```
### 5. webpack 自带优化
> tree-shaking、scope hosting (生产模式)
**tree-shaking**
1. 编写 test.js
```js
let add = (a, b) => a + b + "add";
let minus = (a, b) => a - b + "minus";
export default {
add,
minus,
};
```
2. 使用 require 引入
```js
let calc = require("./test.js");
console.log(calc.add(1, 2));
```
打包结果展示
```js
(() => {
var e,
r = {
454: (e, r, t) => {
"use strict";
t.r(r), t.d(r, { default: () => o });
const o = {
add: function (e, r) {
return e + r + "add";
},
minus: function (e, r) {
return e - r + "minus";
},
};
},
},
t = {};
function o(e) {
var n = t[e];
if (void 0 !== n) return n.exports;
var d = (t[e] = { exports: {} });
return r[e](d, d.exports, o), d.exports;
}
(o.d = (e, r) => {
for (var t in r)
o.o(r, t) &&
!o.o(e, t) &&
Object.defineProperty(e, t, { enumerable: !0, get: r[t] });
}),
(o.o = (e, r) => Object.prototype.hasOwnProperty.call(e, r)),
(o.r = (e) => {
"undefined" != typeof Symbol &&
Symbol.toStringTag &&
Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }),
Object.defineProperty(e, "__esModule", { value: !0 });
}),
(e = o(454)),
console.log(e.add(1, 2));
})();
```
3. 使用 import 引入
```js
import calc from "./test";
console.log(calc.add(1, 2));
```
打包结果展示
```js
(() => {
"use strict";
console.log(1 + 2 + "add");
})();
```
4. 需要的配置
```js
let webpack = require("webpack");
module.exports = {
mode: "production", // 模式 默认两种 production development
entry: "./src/index.js", // 入口
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"), // 路径必须是一个绝对路径
},
plugins: [
new webpack.LoaderOptionsPlugin({
options: {
sideEffects: false,
},
}),
],
rules: [
{
test: /\.js$/, // normal 普通的loader
// 2. 匹配
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
},
],
};
```
- sideEffects 有三种情况
- sideEffects:true 所有文件都有副作用,全都不可 tree-shaking
- sideEffects:false 有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
- sideEffects:[] 部分 tree-shaking , 除了数组外都 tree-shaking
总结:
1. ES6(ESModule)的 export default、export 与 import
2. Node(commonjs)module.exports、exports 与 require
3. 如果是 export default 导出, require 导入的会多一层 default
4. import 在生产环境下,配置 sideEffects 会自动去掉没用到的代码, 而 require 则不会去掉多余代码
5. tree-shaking 配合 import 把没用到的代码 自动删除掉 (树木摇晃: 把多余的去掉)
**scropt hosting 作用域提升**
1. 演示
```js
let a = 1;
let b = 2;
let c = 3;
let d = a + b + c;
console.log(d);
```
2. 结果
```
console.log(6);
```
总结
1. 在生产环境下, 无需配置 webpack 会自动省略多余的变量, 和可以简化的代码
### 6. 抽离公共代码
1. 编写测试文件
```js
// a.js
console.log("a ---------------------------");
```
```js
// b.js
console.log("b ---------------------------");
```
```js
// test.js
import "./a";
import "./b";
console.log("test.js");
```
```js
// index.js
import "./a";
import "./b";
console.log("index.js");
```
2. 修改配置文件
```js
module.exports = {
mode: "production", // 模式 默认两种 production development
entry: {
index: "./src/index.js",
test: "./src/test.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"), // 路径必须是一个绝对路径
},
};
```
- 此时打包会发现 index 与 test 文件都打包了 a.js 和 b.js, 想把 a.js 和 b.js 打包成公共的 js 文件
3. 配置
```js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
common: {
chunks: "initial",
minSize: 0,
minChunks: 2,
},
},
},
},
};
```
4. 此时已经将 a.js 和 b.js 打包成共用的文件了, 但此时还有个 react 也需要打包
```js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
common: {
chunks: "initial",
minSize: 0,
minChunks: 2,
},
vendor: {
test: /node_modules/,
chunks: "initial",
minSize: 0,
minChunks: 2,
},
},
},
},
};
```
5. 此时将 a.js 和 b.js 单独打包了, 也将 react 打包到了共用的文件中, 但想将 react 单独打包
```js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
common: {
chunks: "initial",
minSize: 0,
minChunks: 2,
},
vendor: {
priority: 1, // 先将抽离第三方包, 在抽离公共的代码
test: /node_modules/,
chunks: "initial",
minSize: 0,
minChunks: 2,
},
},
},
},
};
```
总结
1. 此时已经将需要单独打包的文件完成了
### 7. 懒加载
1. 编写测试文件 source.js
```js
export default "前端小溪";
```
2. 测试 index.js
```js
let button = document.createElement("button");
button.innerHTML = "加载资源";
button.addEventListener("click", function () {
// 动态加载文件
import("./source").then((data) => {
console.log(data.default);
});
});
document.body.appendChild(button);
```
3. 如果没支持懒加载 import, 可以使用`@babel/plugins-syntax-dynamic-import`
```js
module.exports = {
rules: [
{
test: /\.js$/, // normal 普通的loader
// 2. 匹配
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
plugins: ["@babel/plugins-syntax-dynamic-import"],
},
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
},
],
};
```
### 8. 热更新
1. 启用热更新
```js
module.exports = {
devServer: {
hot: true, // 启用热更新
},
};
```
2. 监听热更新处理某事
```js
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin(), // 热更新插件
],
};
```
## 四、webpack 之 Tapable
- Tapable 是 webpack 核心
- Webpack 本质上是一种事件流的机制, 它的工作流程就是将各个插件串联起来, 而实现这一切的核心就是 Tapable, Tapable 有点类似 nodejs 的 events 库, 核心原理也是依赖于发布订阅模式
- 注册方法分为: tap、tapAsync、tapPromise

### 01-同步钩子 SyncHook
- 注册在该钩子下面的插件的执行顺序是顺序执行
- 只能使用 tap 注册,不能使用 toPromise 和 tapAsync
1. 使用
```js
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习react
```
2. 实现
```js
// 同步钩子 -- 并行
class SyncHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach((task) => task(...args));
}
}
let hook = new SyncHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习react
```
### 02-同步钩子 SyncBailHook
SyncBailHook:类似于 SyncHook,执行过程中注册的回调返回非 undefined 时就停止不在执行。
1. 使用
```js
const { SyncBailHook } = require("tapable");
const hook = new SyncBailHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
return "想停止学习";
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
```
2. 实现
```js
// 同步钩子 -- 串行(沙漏)
class SyncBailHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
let ret; // 当前函数
let index = 0; // 当前要先执行第一个
do {
ret = this.tasks[index++](...args);
} while (ret === undefined && index < this.tasks.length);
}
}
let hook = new SyncBailHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
return "我不想学了";
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
```
### 03-同步钩子 SyncWaterfallHook
SyncWaterfallHook:接受至少一个参数,上一个注册的回调返回值会作为下一个注册的回调的参数。如 1,2,3 函数, 函数 2 没有返回值, 函数 3 将接受的是函数 1 的返回值
1. 使用
```js
const { SyncWaterfallHook } = require("tapable");
const hook = new SyncWaterfallHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
return "我不想学了";
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.tap("webpack", function (name) {
console.log("前端小溪(" + name + ") --- 学习webpack");
});
hook.tap("vue", function (name) {
console.log("前端小溪(" + name + ") --- 学习vue");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(我不想学了) --- 学习react
// 前端小溪(我不想学了) --- 学习webpack
// 前端小溪(我不想学了) --- 学习vue
```
2. 实现
```js
// 同步钩子 -- 参数传递
class SyncWaterfallHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
let [first, ...others] = this.tasks;
let ret = first(...args);
others.reduce((pre, cur) => {
return cur(pre) || pre;
}, ret);
}
}
let hook = new SyncWaterfallHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
return "我不想学了";
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.tap("webpack", function (name) {
console.log("前端小溪(" + name + ") --- 学习webpack");
});
hook.tap("vue", function (name) {
console.log("前端小溪(" + name + ") --- 学习vue");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(我不想学了) --- 学习react
// 前端小溪(我不想学了) --- 学习webpack
// 前端小溪(我不想学了) --- 学习vue
```
### 04-同步钩子 SyncLoopHook
SyncLoopHook:有点类似 SyncBailHook,但是是在执行过程中回调返回非 undefined 时继续再次执行当前的回调。
1. 使用
```js
let { SyncLoopHook } = require("tapable");
class Lesson {
constructor() {
this.index = 0;
this.hooks = {
arch: new SyncLoopHook(["name"]),
};
}
tap() {
this.hooks.arch.tap("node", (data) => {
console.log("前端小溪(" + data + ") --- 学习node");
return ++this.index === 3 ? undefined : "继续学";
});
this.hooks.arch.tap("react", (data) => {
console.log("前端小溪(" + data + ") --- 学习react");
});
this.hooks.arch.tap("webpack", (data) => {
console.log("前端小溪(" + data + ") --- 学习webpack");
});
this.hooks.arch.tap("vue", (data) => {
console.log("前端小溪(" + data + ") --- 学习vue");
});
}
start() {
this.hooks.arch.call("xiaoxi");
}
}
let l = new Lesson();
l.tap(); // 注册两个事件
l.start();
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习react
// 前端小溪(xiaoxi) --- 学习webpack
// 前端小溪(xiaoxi) --- 学习vue
```
2. 实现
```js
// 同步钩子 -- 重复执行
class SyncLoopHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach((task) => {
let ret;
do {
ret = task(...args);
} while (ret !== undefined);
});
}
}
let hook = new SyncLoopHook(["name"]);
let index = 0;
hook.tap("node", (data) => {
console.log("前端小溪(" + data + ") --- 学习node");
return ++index === 3 ? undefined : "继续学";
});
hook.tap("react", (data) => {
console.log("前端小溪(" + data + ") --- 学习react");
});
hook.tap("webpack", (data) => {
console.log("前端小溪(" + data + ") --- 学习webpack");
});
hook.tap("vue", (data) => {
console.log("前端小溪(" + data + ") --- 学习vue");
});
hook.call("xiaoxi");
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习react
// 前端小溪(xiaoxi) --- 学习webpack
// 前端小溪(xiaoxi) --- 学习vue
```
### 05-异步钩子 AsyncParallelHook
当注册的所有异步回调都并行执行完毕之后再执行 callAsync 或者 promise 中的函数
AsyncParallelBailHook:执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数(由于并行执行的原因,注册的其他回调依然会执行)。
1. 使用
```js
let { AsyncParallelHook } = require("tapable");
// 同时发送多个请求
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(["name"]),
};
}
tap() {
this.hooks.arch.tapAsync("node", (data, callback) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习node");
callback();
}, 2000);
});
this.hooks.arch.tapAsync("react", (data, callback) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习react");
callback();
}, 1000);
});
}
start() {
this.hooks.arch.callAsync("xiaoxi", () => {
console.log("结束");
});
}
}
let l = new Lesson();
l.tap(); // 注册两个事件
l.start();
// 前端小溪(xiaoxi) --- 学习react
// 前端小溪(xiaoxi) --- 学习node
// 结束
```
2. 实现
```js
// 异步钩子 -- 并发
class AsyncParallelHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop(); // 拿出最终的函数
let index = 0;
let done = () => {
index++;
if (index === this.tasks.length) {
finalCallback();
}
};
this.tasks.forEach((task) => {
task(...args, done);
});
}
}
let hook = new AsyncParallelHook(["name"]);
hook.tapAsync("node", (data, callback) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习node");
callback();
}, 2000);
});
hook.tapAsync("react", (data, callback) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习react");
callback();
}, 1000);
});
hook.callAsync("xiaoxi", () => {
console.log("结束");
});
// 前端小溪(xiaoxi) --- 学习react
// 前端小溪(xiaoxi) --- 学习node
// 结束
```
3. 测试 promise
```js
let { AsyncParallelHook } = require("tapable");
// 同时发送多个请求
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(["name"]),
};
}
tap() {
this.hooks.arch.tapPromise("node", (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习node");
resolve();
}, 1000);
});
});
this.hooks.arch.tapPromise("webpack", (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习webpack");
resolve();
}, 2000);
});
});
}
start() {
this.hooks.arch.promise("xiaoxi").then(() => {
console.log("结束");
});
}
}
let l = new Lesson();
l.tap(); // 注册两个事件
l.start();
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习webpack
// 结束
```
4. 实现 promise
```js
// 异步钩子 -- 并发promise
class AsyncParallelHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tapPromise(name, task) {
this.tasks.push(task);
}
promise(...args) {
let tasks = this.tasks.map((task) => task(...args));
return Promise.all(tasks);
}
}
let hook = new AsyncParallelHook(["name"]);
hook.tapPromise("node", (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习node");
resolve();
}, 1000);
});
});
hook.tapPromise("webpack", (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("前端小溪(" + data + ") --- 学习webpack");
resolve();
}, 2000);
});
});
hook.promise("xiaoxi").then(() => {
console.log("结束");
});
// 前端小溪(xiaoxi) --- 学习node
// 前端小溪(xiaoxi) --- 学习webpack
// 结束
```
**AsyncParallelBailHook**:异步并行且运行中断。
### 06-异步钩子 AsyncSeriesHook
AsyncSeriesBailHook:执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数,并且注册的后续回调都不会执行
1. 使用
```js
const { AsyncSeriesHook } = require("tapable");
const hook = new AsyncSeriesHook(["name"]);
console.time("cost");
hook.tapAsync("hello", (name, cb) => {
setTimeout(() => {
console.log(`hello ${name}`);
cb();
}, 2000);
});
hook.tapPromise("hello again", (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`hello ${name}, again`);
// resolve("成功");
reject("error");
}, 1000);
});
});
hook.callAsync("前端小溪", (mes) => {
console.log("done", mes);
console.timeEnd("cost");
});
// hello 前端小溪
// hello 前端小溪, again
// done error
// cost: 3.034s
```
2. 实现
```js
// 异步钩子 -- 串联promise
class AsyncSeriesHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
tapPromise(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = (message) => {
if (this.tasks.length === index) return finalCallback(message);
let task = this.tasks[index++];
let rest = task(...args, next);
if (rest instanceof Promise) {
rest.then(() => next()).catch((error) => next(error));
}
};
next();
}
promise(...args) {
let [first, ...others] = this.tasks;
return others.reduce((p, n) => {
return p.then(() => n(...args));
}, first(...args));
}
}
const hook = new AsyncSeriesHook(["name"]);
console.time("cost");
hook.tapAsync("hello", (name, cb) => {
setTimeout(() => {
console.log(`hello ${name}`);
cb();
}, 2000);
});
hook.tapPromise("hello again", (name, cb) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`hello ${name}, again`);
// resolve();
reject("error");
}, 1000);
});
});
hook.callAsync("前端小溪", (msg) => {
console.log("done", msg);
console.timeEnd("cost");
});
// hello 前端小溪
// hello 前端小溪, again
// done error
// cost: 3.020s
```
**AsyncSeriesBailHook** :异步串行且运行中断。
### 07-异步钩子 AsyncSeriesWaterfallHook
AsyncSeriesWaterfallHook:异步串行瀑布流,上一个事件的结果作为下一个事件的参数。
1. 使用
```js
const { AsyncSeriesWaterfallHook } = require("tapable");
const hook = new AsyncSeriesWaterfallHook(["name"]);
console.time("cost");
hook.tapAsync("node", (name, cb) => {
setTimeout(() => {
console.log(`${name} 学习node`);
cb(null, "result");
}, 2000);
});
hook.tapAsync("webpack", (name, cb) => {
setTimeout(() => {
console.log(`学习 ${name}`);
// cb(null);
}, 1000);
});
hook.callAsync("前端小溪", (mes) => {
console.log("done", mes);
console.timeEnd("cost");
});
// 前端小溪 学习node
// 学习 result
// done null
// cost: 3.032s
// 关闭第二个cb的输出
// 前端小溪 学习node
// 学习 result
```
2. 实现
```js
class AsyncSeriesWaterfallHook {
constructor(args) {
this.tasks = [];
}
tapAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
let finalCallback = args.pop();
let index = 0;
let next = (err, data) => {
let task = this.tasks[index];
if (!task) return finalCallback();
if (index === 0) {
task(...args, next);
} else {
task(data, next);
}
index++;
};
next();
}
}
let hook = new AsyncSeriesWaterfallHook(["name"]);
console.time("cost");
hook.tapAsync("node", (name, cb) => {
setTimeout(() => {
console.log(`${name} 学习node`);
cb(null, "result");
}, 2000);
});
hook.tapAsync("webpack", (name, cb) => {
setTimeout(() => {
console.log(`学习 ${name}`);
cb(null);
}, 1000);
});
hook.callAsync("前端小溪", (mes) => {
console.log("done", mes);
console.timeEnd("cost");
});
// 前端小溪 学习node
// 学习 result
// done undefined
// cost: 3.017s
// 关闭第二个cb的输出
// 前端小溪 学习node
// 学习 result
```
## 五、 webpack 之 实现
### 1. 初始化项目
1. 创建 webpack 项目
```
yarn init -y
```
2. 创建 webpack.config.js
```js
let path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
};
```
### 2. 注册脚本
1. 修改 package.json
```json
{
"name": "xxpack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"bin": {
"xxpack": "./bin/xxpack.js"
}
}
```
2. 创建 bin/xxpack.js
```js
#! /usr/bin/env node
console.log("start 前端小溪");
```
3. 注册脚本
```
npm link
npm link xxpack
```
4. 测试脚本
```
npx xxpack
```
### 3. webpack 分析
1. 新建 src/base/b.js
```js
module.exports = "b";
```
2. 新建 src/a.js
```js
let str = require("./base/b");
module.exports = "a" + str;
```
3. src/index.js
```js
let str = require("./a");
console.log(str);
console.log("前端小溪写的webpack");
```
4. 安装 webpack, webpack-cli
```js
yarn add webpack webpack-cli -D
```
5. 生成打包文件
```
npx webpack
```
- 此步骤主要目的是分析 webpack 对文件依赖的处理
- 同时,将 bundle.js 文件去掉注释,保留下来以备后用
### 4. 创建依赖关系并 AST 递归解析
1. 改写 bin/xxpack.js 运行编译
```js
#! /usr/bin/env node
// 1. 需要找到当前执行名的路径, 拿到webpack.config.js
let path = require("path");
// config配置文件
let config = require(path.resolve("webpack.config.js"));
let Compiler = require("../lib/Compiler.js");
let compiler = new Compiler(config);
// 标识运行编译
compiler.run();
```
2. 创建 lib/Compiler.js
```js
const { log } = require("console");
let fs = require("fs");
let path = require("path");
let babylon = require("babylon");
let t = require("@babel/types");
let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
let ejs = require("ejs");
// babylon 主要就是把源码 转换成ast
// @babel/traverse
// @babel/types
// @babel/generator
class Compiler {
constructor(config) {
// entry output
this.config = config;
// 需要保存入口文件的路径
this.entryId; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry; // 入口路径
// 工作路径
this.root = process.cwd();
}
// 解析源码
parse(source, parentPath) {
// AST解析语法树
let ast = babylon.parse(source);
let dependencies = []; // 依赖的数组
traverse(ast, {
CallExpression(p) {
let node = p.node; // 对应的节点
if (node.callee.name === "require") {
node.callee.name = "__webpack_require__";
let moduleName = node.arguments[0].value; // 取到的就是模块的引用名字
moduleName = moduleName + (path.extname(moduleName) ? "" : ".js");
moduleName = "./" + path.join(parentPath, moduleName); // 'src/a.js'
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
},
});
let sourceCode = generator(ast).code;
return { sourceCode, dependencies };
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, "utf8");
return content;
}
// 构建模块
buildModule(modulePath, isEntry) {
// 拿到模块的内荣
let source = this.getSource(modulePath);
// 模块id modulePath = modulePath - this.root src/index.js
let moduleName = "./" + path.relative(this.root, modulePath);
if (isEntry) {
this.entryId = moduleName; // 保存入口的名字
}
// 解析需要把source源码进行改造, 返回一个依赖列表
let { sourceCode, dependencies } = this.parse(
source,
path.dirname(moduleName)
); // ./src
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode;
dependencies.forEach((dep) => {
// 父模块的加载, 递归加载
this.buildModule(path.join(this.root, dep), false);
});
}
emitFile() {
// 发射文件
// 用数据 渲染我们的
// 拿到输出到哪个目录下
let main = path.join(this.config.output.path, this.config.output.filename);
let templateStr = this.getSource(path.join(__dirname, "main.ejs"));
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules,
});
this.assets = {};
// 资源中 路径对应的代码
this.assets[main] = code;
fs.writeFileSync(main, this.assets[main]);
}
run() {
// 执行 并且创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true);
// 发射一个文件, 打包后的文件
this.emitFile();
}
}
module.exports = Compiler;
```
3. 安装几个依赖
```
yarn add babylon @babel/types @babel/traverse @babel/generator ejs -D
```
4. 以 bundle.js 为模板 创建解析模板 lib/main.ejs
```js
(() => {
var __webpack_modules__ = {
<%for(let key in modules){%>
"<%-key%>" :
(function(module, exports, __webpack_require__){
eval(`<%-modules[key]%>`);
}),
<%}%>
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = __webpack_require__("<%-entryId%>");
})();
```
### 5. 生成打包文件
1. 运行打包命令
```js
npx xxpack
```
2. 输出
```
ab
前端小溪写的webpack
```
### 6. 增加 loader
1. 创建 loader/less-loader
```js
let less = require("less");
function loader(source) {
let css = "";
less.render(source, function (err, c) {
css = c.css;
});
css = css.replace(/\n/g, "\\n");
return css;
}
module.exports = loader;
```
2. 创建 loader/style-loader
```js
function loader(sources) {
let style = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(sources)}
document.head.appendChild(style);
`;
return style;
}
module.exports = loader;
```
3. 安装 less
```
yarn add less -D
```
4. 改写 webpack.config.js
```js
let path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.less$/,
use: [
path.resolve(__dirname, "loader", "style-loader"),
path.resolve(__dirname, "loader", "less-loader"),
],
},
],
},
};
```
5. 修改 lib/Compiler.js 的 getSource 函数
```js
getSource(modulePath) {
let content = fs.readFileSync(modulePath, "utf8");
// 增加loader
let rules = this.config.module.rules;
// 拿到每个规则来处理
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
let len = use.length - 1;
// 这个模块需要通过loader来转化
if (test.test(modulePath)) {
// loader 获取对应的loader 函数
function normalLoader() {
let loader = require(use[len--]);
// 递归调用loader 实现转化功能
content = loader(content);
if (len >= 0) {
normalLoader();
}
}
normalLoader();
}
}
return content;
}
```
6. 结果展示

### 7. 增加 plugins
1. 使用自己写的 tapable, 也可以使用 tapable 库
```
yarn add tapable -D
```
或创建 /lib/hooks/SyncHook.js
```js
// 同步钩子 -- 并行
class SyncHook {
// args => ['name']
constructor(args) {
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
this.tasks.forEach((task) => task(...args));
}
}
/* 使用示例
let hook = new SyncHook(["name"]);
hook.tap("node", function (name) {
console.log("前端小溪(" + name + ") --- 学习node");
});
hook.tap("react", function (name) {
console.log("前端小溪(" + name + ") --- 学习react");
});
hook.call("xiaoxi");
*/
module.exports = { SyncHook };
```
2. 引入
```js
let { SyncHook } = require("tapable");
// 或
let { SyncHook } = require("./hooks/SyncHook");
```
3. 修改 lib/Compiler.js 的 constructor 函数添加钩子
```js
constructor(config) {
// entry output
this.config = config;
// 需要保存入口文件的路径
this.entryId; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry; // 入口路径
// 工作路径
this.root = process.cwd();
this.hooks = {
entryOptions: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
};
// 如果传递了 plugins 参数
let plugins = this.config.plugins;
if (Array.isArray(plugins)) {
plugins.forEach((plugin) => {
plugin.apply(this);
});
}
}
```
4. 修改 webpack.config.js, 添加插件
```js
let path = require("path");
class P1 {
apply(compiler) {
compiler.hooks.emit.tap("emit", function () {
console.log("前端小溪 emit");
});
}
}
class P2 {
apply(compiler) {
compiler.hooks.entryOptions.tap("emit", function () {
console.log("前端小溪 entryOptions");
});
}
}
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
plugins: [new P1(), new P2()],
module: {
rules: [
{
test: /\.less$/,
use: [
path.resolve(__dirname, "loader", "style-loader"),
path.resolve(__dirname, "loader", "less-loader"),
],
},
],
},
};
```
5. xxpack.js 注册 entryOption 生命周期钩子
```js
#! /usr/bin/env node
// 1. 需要找到当前执行名的路径, 拿到webpack.config.js
let path = require("path");
// config配置文件
let config = require(path.resolve("webpack.config.js"));
let Compiler = require("../lib/Compiler.js");
let compiler = new Compiler(config);
compiler.hooks.entryOptions.call();
// 标识运行编译
compiler.run();
```
6. Compiler.js 的 run 函数注册 run、compile、afterCompile、emit、done 生命周期钩子
```js
function run() {
this.hooks.run.call();
this.hooks.compile.call();
// 执行 并且创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true);
this.hooks.afterCompile.call();
// 发射一个文件, 打包后的文件
this.emitFile();
this.hooks.emit.call();
this.hooks.done.call();
}
```
7. Compiler.js 的 constructor 函数注册 afterPlugins 生命周期钩子
```js
constructor(config) {
// entry output
this.config = config;
// 需要保存入口文件的路径
this.entryId; // './src/index.js'
// 需要保存所有的模块依赖
this.modules = {};
this.entry = config.entry; // 入口路径
// 工作路径
this.root = process.cwd();
this.hooks = {
entryOptions: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
};
// 如果传递了 plugins 参数
let plugins = this.config.plugins;
if (Array.isArray(plugins)) {
plugins.forEach((plugin) => {
plugin.apply(this);
});
}
this.hooks.afterPlugins.call();
}
```
8. 测试
```
npx xxpack
```
输出
```
前端小溪 entryOptions
前端小溪 emit
```