一篇搞懂webpack工作流 您所在的位置:网站首页 快手礼物特效展示 一篇搞懂webpack工作流

一篇搞懂webpack工作流

2023-12-15 09:34| 来源: 网络整理| 查看: 265

写在前面

我们每天都在和webpack打交道,都知道他是一个模块和和打包工具,那还有哪些替代工具呢,为什么最终选择webpack?前面也在深入理解ast编译解析原理中讲到webpack是基于ast语法树进行拓展的,那如何通过操作ast的呢?webpack的工作流是怎样的呢?下面都会展开

webpack类似的工具有哪些,为什么要选择webpack

grunt/gulp/rollup/webpack

grunt 介绍: 自动化程度高,对于需要反复重复的任务:压缩、编译、单元测试、linting;它运用的是配置的思想。 缺点: 1. 配置项太多 2. 不同的插件可能会有自己的扩展字段 3. 因此学习成本会很高,运用的时候需要明白各种插件的配置规则和配合方式 gulp 1. 基于node.js的stream流打包工具 2. 定位是基于任务流的自动化打包工具 3. 通过task对整个开发过程进行构建 rollup 1. rollup是ES6模块化工具,最大的亮点是利用了ES6的模块设计,利用tree-shaking生成更简洁、更简单的代码 2. 一般情况下开发类库使用rollup,开发项目使用webpack 3. 代码是基于ES6模块,希望代码直接被他人直接使用的情况使用rollup 4. 不建议使用情况:需要处理的静态资源比较多、需要代码拆分、构建的项目需要引入很多的CommonJS模块依赖时 webpack 是一个模块化管理工具和打包工具。通过loader的转换,任何形式的资源都可以视为模块,比如CommonJS模块、AMD模块、ES6模块、CSS、图片等,也就是可以将松散的模块按照依赖和规则打包成符合生产环境部署的前端资源 可以将按需加载的模块进行分割,等需要的时候再异步加载 webpack被定义为一个模块打包器,而gulp和grunt属于构建工具。 loder和plugin的区别

简单的区别

loader也就是加载器。webpack将一切文件视为模块,但是webpack原生只是能解析js文件,有了loader就能把其他所有的文件转换成js文件,那么就能通过webpack对所有的文件进行解析。也就是loader给webpack提供了加载非js文件的能力。 plugin也就是插件,plugin可以扩展webpack的功能。因为在webpack的运行周期中会广播许多的事件,plugin只需要监听这些事件,在合适的时候通过webpack提供的API对ast进行操作,就达到了目的。 深入分析一下webpack的构建流程 先画一个概念图进行总结

image.png

注意: 在webpack整个构建过程中,webpack会在特定时间广播出特定的事件,插件在监听到对应的事件后执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果。

你知道loader的分类以及执行顺序吗?

分类: inline-loader pre-loader post-loader normal-loader

布局: post-loader1 post-loader2 inline-loader1 inline-loader2 normal-loader1 normal-loader2 pre-loader1 pre-loader2

但是执行顺序是这样的,我简单画一下示意图

image.png

因此有了loader.pitch的概念loader执行顺序才是从下到上从右到左反着执行的。但是本质上是正常的顺序执行的。

而其中的这些前缀: post、inline、normal、pre是控制执行顺序的属性值。

module: { rules: [ { enforce: "pre",//post/inline/normal/pre test: /\.jsx?$/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"] }, }, include: path.join(__dirname, "src"), exclude: /node_modules/, } ] }, 实现loader的思路

深入理解ast编译解析原理中提到过今天我们再回顾一下。

下面是宏观一些的写法不够底层,其实loader就是操作ast语法树就行修改完成对应功能,可以看下上面的文章有详细解读

const babel = require("@babel/core"); function loader(source, inputSourceMap,data) { const options = { presets: [@babel/preset-env], inputSourceMap: inputSourceMap, sourceMaps: true, filename: this.request.split("!")[1].split("/").pop(), }; let {code, map, ast } = babel.transform(source, options); return this.callback(null, code, map, ast) } module.exports = loader; 实现plugin的思路

上面我们提到: webpack会在特定的时间广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的api改变webpack的运行结果

可以加载插件的常用对象 可以加载插件的对象钩子Compiler(重点)run,compile, compilation, make, emit, doneCompilation(重点)buildModule, normalModuleLoader, finishModules, seal,optimiz, after-sealModule FactorybeforeResolver,afterResolver,module,parserModuleParserprogram, statement, call, expressionTemplatehash,bootstrap, localVars, render 创建插件的思路 创建一个js命名函数(这个就是插件函数) 在插件函数prototype上定义一个apply方法 指定一个绑定到webpack自身的事件钩子 处理webpack内部实例的特定数据 功能完成后调用webpack提供的回调 compiler & compilation compiler是webpack完整的配置,该对象在启动webpack时被一次性建立,并配置好所有可操作的设置(options,loader,plugin)。当webpack环境中应用一个插件的时候插件将会受到compiler对象的引用,然后再插件中就可以访问到webpack主环境了。 基本插件架构

插件是由「具有 apply 方法的 prototype 对象」所实例化出来的

这个 apply 方法在安装插件时,会被 webpack compiler 调用一次

apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象

插件代码如何执行 github.com/webpack/web…

完整的compiler是什么样子的: github.com/webpack/web…

模拟实现webpack构建流程

RunPlugin.js

module.exports = class RunPlugin { apply(compiler) {//compiler是完整的webpack环境配置,插件通过访问compiler来访问webpack主环境。 compiler.hooks.run.tap("RunPlugin", () => { console.log('RunPlugin'); }) } }

DonePlugin.js

module.export = class DonePlugin { apply(compiler) { compiler.hooks.run.tap("DonePlugin", () => { console.log('DonePlugin'); }) } }

flow.js

const fs = require('fs'); const path = require('path'); const { SyncHook } = require("tapable");//这个包会暴露出webpack的api function babelLoader(source) { return `val sum = function sum(a, b) { return a + b; }` } class Compiler { constructor(options) { this.options = options; this.hooks = { run: new SyncHook(), done: new SyncHook() } } run() { this.hooks.run.call(); let modules = []; let chunks = []; let files = []; //确定入口: 根据配置中的entry找出所有的入口文件 let entry = path.join(this.options.context, this.options.entry); //从入口文件出发,调用所有配置的Loader对模块进行编译 let entryContent = fs.readFileSync(entry, 'utf8'); let entrySource = babelLoader(entryContent); let entryModule = {id: entry, source: entrySource}; modules.push(entryModule); //再找出该模块依赖的模块,再递归本步骤直到所有依赖的文件都经过了本步骤的处理。 let title = path.join(this.options.context, './src/title.js'); let titleContent = fs.readFileSync(title, 'utf8'); let titileSource = babelLoader(titleContent); let titleMoudle = {id: title, source: titleSource}; modules.push(titleMoudle); //根据入口和模块之间的依赖关系,组成一个个包含多个模块的chunk let chunk = {name: 'main', modules}; chunks.push(chunk); // 再把每一个chunk转换成一个单独的文件加入到输出列表 let file = { file:this.options.output.filename, source: ` (function(modules) { function __webpack_require__(moduleId) { var module = {exports: {}}; modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); return module.exports; } return __webpack_require__("./src/app.js") })({ "./src/app.js": function(module, exports, _webapck_require__) { var title = __webpack_require__("./src/title.js"); console.log('title') }, "./src/title.js": function(module) { module.exports = 'title'; } , })` } files.push(file); //在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 let outputPath = path.join( this.options.output.path,this.option.output.filename ); fs.writeFileSync(outputPath, file.source,'utf8'); this.hooks.done.call(); } } //1. 从配置文件和shell语句中读取并合并参数,得出最终的参数 let options = require('./webpack.config'); //2. 用上一步得到的参数初始化compiler对象 let compiler = new Compiler(options); //3. 加载所有配置的插件 if(options.plugins && Array.isArray(options.plugins)) { for(const plugin of options.plugins) { plugin.apply(compiler); } } //4. 执行对象的run方法开始执行编译 compiler.run();

webpack.config.js

const path = require('path'); const RunPlugin = require("./plugins/RunPlugin"); const DonePlugin = require("./plugins/RunPlugin"); module.exports = { context: process.pwd(), mode: "development", devtool: false, entry: "./src/app.js", output: { path: path.resolve(__dirname,, "dist"), filename: "main.js" }, module: { rules: [ { test: /\.jsx?$/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"] }, }, include: path.join(__dirname, "src"), exclude: /node_modules/, } ] }, plugins: [new RunPlugin(), new DonePlugin()], devServer: {} } 顺便记录一下常见的loader和plugin以及解决的问题 loader loader功能babel-loader把ES6或react转成ES5css-loader加载css,支持模块化、压缩、文件导入等特性eslint-loader通过eslint检查javascript代码file-loader把文件输出到一个文件夹中,在代码中通过相对路径来引用url-loader类似file-loader,但是当文件很小的时候会把文件内容以base64方式注入到代码中sass-loader将sass/scss文件编译成csspostcss-loader使用postcss处理csscss-loader处理background:(url)、@import语法,让webpack能根据正确的路径进行模块化style-loader把css代码注入到javascript中,通过DOM操作区加载css plugin 插件功能case-sensitive-paths-webpack-plugin路径有误报错terser-webpack-plugin使用terser来压缩javascripthtml-webpack-plugin自动生成带有入口文件引用的index.htmlwebpack-manifest-plugin生产资产的显示清单文件optimize-css-assets-webpack-plugin用于优化或者压缩css资源mini-css-extract-plugin将css提取成独立的文件,对每个包含css的js文件都会创建一个css文件,支持按需加载css和sourceMapModuleScopePlugin引用了src目录外的文件进行报警interpolateHtmlPlugin和htmlWebpackPlugin串行使用,允许在index.html中添加变量ModuleNotFoundPlugin找不到模块的时候提供更详细的上下文信息DefinePlugin创建一个在编译时可配置的全局常量,如果你自定义了一个全局变量PRODUCTION,可在此设置其值来区分开发还是生产环境HotModuleReplacementPlugin启用模块热替换WatchMissingNodeModulesPlugin安装库后自动重新构建打包文件 sourceMap cheap(不包含列信息) module(包含loader文件的sourceMap,否则无法定义源文件) eval(使用eval包裹模块代码) source-map(产生.map文件)

总结:

开发环境: cheap-module-eval-source-map 生产环境: cheap-module-source-map webpack热更新的原理 什么是HMR 实现不刷新浏览器前提下更新页面: 就是当我们的代码修改并保存后,webpack将会对代码进行更新打包,并将新的模块发送到浏览器端,浏览器用新的模块替代就的模块。 搭建HMR项目

npm install webbpack webpack-cli webpack-dev-server mime html-webpack-plugin express scoket.io events -S

package.json

{ "name": "san_niu", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack", "dev": "webpack-dev-server" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "webpack": "4.39.1", "webpack-cli": "3.3.6", "webpack-dev-server": "3.7.2" } } 热更新流程 调用webpack 创建compiler对象 const config = require("./webpack.config"); const compiler = webpack(config); 创建webpack-dev-server ......热更新这部分我还要抽时间看下源码,暂时就不解析了,总之就是利用Socket.io独立出来的数据传输部分(engine.io)实现的跨浏览器/跨设备双向数据通信。而engine.io是对WebSocket和AJAX轮询做的封装形成的api,屏蔽了细节差异和兼容性问题。 如何利用webpack优化性能? 压缩 清除无用css tree shaking Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断 出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)。 Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需 要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对 javascript来说更有意义。 Scope Hoisting(将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突) 代码分割 为什么需要提取公共代码

大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题

- 相同的资源被重复的加载,浪费用户的流量和服务器的成本; - 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。 - 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本

基础类库,方便长期缓存

页面之间的公用代码

各个页面单独生成文件

文档

common-chunk-and-vendor-chunk

webpack将会基于以下条件自动分割代码块:

新的代码块被共享或者来自node_modules文件夹 新的代码块大于30kb(在min+giz之前) 按需加载代码块的请求数量应该 { this.setState({ Comp: result.default }); }); } render() { let Comp = this.state.Comp; return Comp ? : null; } }; } */ const AppTitle = React.lazy(() => import(/* webpackChunkName: "title" */ "./components/Title") ); class App extends Component { constructor(){ super(); this.state = {visible:false}; } show(){ this.setState({ visible: true }); }; render() { return ( {this.state.visible && ( )} 加载 ); } } ReactDOM.render(, document.querySelector("#root")); prelaod 预加载

提高资源下载优先级: 关键资源,包括关键js、字体、css文件。使得关键数据提前下载好,优化页面打开速度

等级:

Highest 最高 High 高 Medium 中等 Low 低 Lowest 最低

异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是 Low

提取公共代码 splitChunks cdn

太晚了,明天接着写

hash/chunkhash/contenthash的区别 什么是hash? hash是文件指纹 hash是打包后输出的文件名和后缀 hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应 的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL 地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。 占位符有哪些? 占位符名称含义ext资源后缀名name文件名称path文件的相对路径folder文件所在文件夹hash每次webpack构建时生成一个唯一的hash值chunkhash根据chunk生成hash值,来源于同一个chunk则hash值就一样contenthash根据内容生成hash值,文件内容相同hash值就相同 hash 每次编译之后都会生成新的hash,即修改任何文件都会引起hash的改变。 问题: 也就是即使有的文件内容没有发生改变,那个文件的hash还是会发生改变,那么cdn就没有办法实现缓存效果 chunkhash 根据不同的入口文件进行依赖文件的解析、构建对应的chunk生成对应的hash值。我们在生产环境里把一些公共库和入口文件区分开,只要不改动公共库里面的代码,就可以保证hash不受影响。 问题: 当一个js文件中引入了css文件,编译之后这两个文件的hash是相同的,那么只要.js文件改变,即使css文件没有发生改变,它的hash值也会改变,导致css文件重复构建 contenthash 基于chunkhash的问题,对于css文件可以使用mini-css-extract-plugin中的contenthash。只要css文件内容没有改变则hash就不会变。 对bundle体积进行监听 什么是bundle,bundle和module、chunk是什么关系呢?

假设模板是这样子的:

src/ ├── index.css ├── index.html ├── index.js ├── common.js └── utils.js

webpack.config.js

{ entry: { index: '../src/index.js', utils: '../src/utils.js', }, output: { filename: "[name].bundle.js" }, module: { rules: [{ test: /\.css$/, use: [ MiniCssExtractPlugin.loader, //创建一个link标签 'css-loader'//解析css处理css中的依赖 ] }] }, plugins: [ //抽离出css文件,以link标签的形式引入样式文件 new MiniCssExtractPlugin({ filename: 'index.bundle.css'//这里写死输出css文件名 }) ] } 每个文件都是一个module 当我们写的module源文件传到webpack进行打包的时候,webpack会根据文件引用关系生成chunk文件,webpack会对这个chunk文件进行一系列的操作 webpack处理好chunk文件之后最后输出bundle文件,bundle文件包含了经过加载和编译的最终源文件,所以可以在浏览器中直接运行。 一般来说一个chunk对应一个bundle,但是也有例外,比如 这里的css文件和js文件都属于chunk0 因为用了MiniCssExtractPlugin将css文件从chunk0中抽离出了第二个bundle文件 utils.js是另一个入口,所以独立一个chunk文件

image.png

监控和分析

npm install webpack-bundle-analyzer -D

const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer') module.exports={ plugins: [ new BundleAnalyzerPlugin() // 使用默认配置 // 默认配置的具体配置项 // new BundleAnalyzerPlugin({ // analyzerMode: 'server', // analyzerHost: '127.0.0.1', // analyzerPort: '8888', // reportFilename: 'report.html', // defaultSizes: 'parsed', // openAnalyzer: true, // generateStatsFile: false, // statsFilename: 'stats.json', // statsOptions: null, // excludeAssets: null, // logLevel: info // }) ] } { "scripts": { "dev": "webpack --config webpack.dev.js --progress" } }

如果想先生成文件再分析

const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer') module.exports={ plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'disabled', // 不启动展示打包报告的http服务器 generateStatsFile: true, // 是否生成stats.json文件 }), ] } { "scripts": { "generateAnalyzFile": "webpack --profile --json > stats.json", // 生成分析文件 "analyz": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 启动展示打包报告的http服务器 } }

npm run generateAnalyzFile / npm run analyz

如何提高webpack构建速度 费时分析: speed-measure-webpack-plugin 缩小范围: extensions resolve: { extensions: [".js",".jsx",".json",".css"] }, alias 配置别名加快查找模块的速度 const bootstrap = path.resolve(__dirname,'node_modules/[email protected]@bootstrap/dist/css/bootstrap.css'); resolve: { alias:{ "bootstrap":bootstrap } }, modules(一些直接声明依赖名的模块,会进行路径检索,这样很耗费性能) 对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样 进行路径搜索,搜索node_modules目录

这个取值就是resolve.modules字段值

resolve: { modules: ['node_modules'], }

如果确定项目内所有的第三方模块都在项目的根目录下node_modules,可以直接指定

resolve: { modules: [path.resolve(__dirname, 'node_modules')], } noParse 不需要解析依赖的的第三方大型类库 module.exports = { // ... module: { noParse: /jquery|lodash/, // 正则表达式 // 或者使用函数 noParse(content) { return /jquery|lodash/.test(content) }, } }... IgnorePlugin(webpack内置插件) (不把指定的模块打包进行) moment会将所有本地化内容和核心功能一起打包,你可使用 IgnorePlugin 在打包时忽略本地化内容: /* 以下代码的含义: 在打包moment这个库的时候, 将整个locale目录都忽略掉 只用中文时直接这样子:import 'moment/locale/zh-cn.js'; * */ new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 日志优化: friendly-errors-webpack-plugin DLL 动态链接库 什么是动态链接库 .dll为后缀的文件称为动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据 把基础模块独立出来打包到单独的动态连接库里 当需要导入的模块(react)在动态连接库里的时候,模块(react)不能再次被打包,而是去动态连接库里获取 webpack.dll.config.js const path = require("path"); const DllPlugin = require("webpack/lib/DllPlugin"); module.exports = { mode: "development", entry: { react: ["react", "react-dom"], }, output: { path: path.resolve(__dirname, "dist"), filename: "[name].dll.js", //react.dll.js library: "_dll_[name]", }, plugins: [ new DllPlugin({ name: "_dll_[name]", path: path.join(__dirname, "dist", "[name].manifest.json"), //react.manifest.json }), ], };

最终会出现一个react.manifest.json文件

插件 DllPlugin插件: 用于打包出一个个动态连接库

DllReferencePlugin: 在配置文件中引入DllPlugin插件打包好的动态连接库 4. 使用DllReferencePlugin在配置文件中引入打包后的动态链接库

const path = require("path"); const glob = require("glob"); const PurgecssPlugin = require("purgecss-webpack-plugin"); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const DllReferencePlugin = require("webpack/lib/DllReferencePlugin.js"); const PATHS = { src: path.join(__dirname, 'src') } module.exports = { mode: "development", entry: "./src/index.js", module: { rules: [ { test: /\.js/, include: path.resolve(__dirname, "src"), use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], }, }, ], }, { test: /\.css$/, include: path.resolve(__dirname, "src"), exclude: /node_modules/, use: [ { loader: MiniCssExtractPlugin.loader, }, "css-loader", ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", }), new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), }), + new DllReferencePlugin({ + manifest: require("./dist/react.manifest.json"), + }), ], }; html中使用

多进程处理 thread-laoder

缓存

babel-loader开启缓存/cache-loader/hard-source-webpack-plugin

Babel在转义js文件过程中消耗性能较高,将babel-loader执行的结果缓存起 来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗 { test: /\.js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", options: { cacheDirectory: true } }] }, cache-laoder对开销大的loader使用,因为是缓存到磁盘上 webpack5中会内置hard-source-webpack-plugin 配置 hard-source-webpack-plugin后,首次构建时间并不会有太大的变化,但是从第二次开始,构建时间大约可以减少 80%左右

HardSourceWebpackPlugin为模块提供了中间缓存,缓存默认的存放路径是 node_modules/.cache/hard-source。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有