# 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 ![](https://img-blog.csdnimg.cn/797061ba3097492bada70d41e27fc4bf.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMwMTAxMTMx,size_16,color_FFFFFF,t_70) ### 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. 结果展示 ![在这里插入图片描述](https://img-blog.csdnimg.cn/47a05998bce545339b762b714ddee6d6.png) ### 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 ```