导图社区 webpack 前端工程化
全面介绍整理webpack基础知识、优化实践和原理解读,感兴趣的可以根据导图学起来。
编辑于2020-10-05 09:20:49webpack 前端工程化
webpack入门
什么是webpack
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器 webpack与grunt、gulp的区别:Grunt 、 Gulp 这类构建工具,打包的思路是: 遍历源文件 → 匹配规则 → 打包 ,这个过程中 做不到按需加载,即对于打包起来的资源,到底页面用不用,打包过程中是不关心的。 webpack 是从入口文件开始,经过模块依赖加载、分析和打包三个流程完成项目的构建。在加载、分析和打包的三个过程中,可以针对性的做一些解决方案,比如 code split (拆分公共代码等)。 webpack 解决什么问题:1、模块化打包,一切皆模块, JS 是模块, CSS 等也是模块;2、语法糖转换:比如 ES6 转 ES5 、 TypeScript ;3、预处理器编译:比如 Less 、 Sass 等;4、项目优化:比如压缩、 CDN ;5、解决方案封装:通过强大的 Loader 和插件机制,可以完成解决方案的封装,比如 PWA ;6、流程对接:比如测试流程、语法检测等。
构建解决什么问题
代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
构建工具对比
grunt
Grunt的优点是: 灵活,它只负责执行你定义的任务;大量的可复用插件封装好了常见的构建任务。 Grunt的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。
gulp
Gulp 基于流的自动化构建工具, 除了可以管理和执行任务,还支持监听文件、读写文件。 通过 gulp.task 注册一个任务;通过 gulp.run 执行任务;通过 gulp.watch 监听文件变化;通过 gulp.src 读取文件;通过 gulp.dest 写文件。 Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。其缺点是和 Grunt 类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。
Fis3
读写文件:通过 fis.match 读文件,release 配置文件输出路径。资源定位:解析文件之间的依赖关系和文件位置。文件指纹:通过 useHash 配置输出文件时给文件 URL 加上 md5 戳来优化浏览器缓存。文件编译:通过 parser 配置文件解析器做文件转换,例如把 ES6 编译成 ES5。压缩资源:通过 optimizer 配置代码压缩方法。图片合并:通过 spriter 配置合并 CSS 里导入的图片到一个文件来减少 HTTP 请求数。 Fis3的优点是集成了各种 Web 开发所需的构建功能,配置简单开箱即用。其缺点是目前官方已经不再更新和维护,不支持最新版本的 Node.js。
rollup
Rollup 是在 Webpack 流行后出现的替代品;Rollup 生态链还不完善,体验不如 Webpack;Rollup 功能不如 Webpack 完善,但其配置和使用更加简单;Rollup 不支持 Code Spliting,但好处是打包出来的代码中没有 Webpack 那段模块的加载、执行和缓存的代码。 Rollup 在用于打包 JavaScript 库时比 Webpack 更加有优势,因为其打包出来的代码更小更快。 但功能不够完善,很多场景都找不到现成的解决方案
webpack
Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。 Webpack的优点是:专注于处理模块化的项目,能做到开箱即用一步到位;通过 Plugin 扩展,完整好用又不失灵活;使用场景不仅限于 Web 开发;社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;良好的开发体验。 Webpack的缺点是只能用于采用模块化开发的项目。
为什么选择webpack
1、大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack 可以为这些新项目提供一站式的解决方案;2、Webpack 有良好的生态链和维护团队,能提供良好的开发体验和保证质量;3、Webpack 被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享。
核心概念和打包流程
Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。Loader:模块转换器,用于把模块原内容按照需求转换成新内容。Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。 Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。
模块化
模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性 模块化是指把一个复杂的系统分解到多个模块以方便编码 使用场景:模块化解决的是功能耦合问题 没有模块化会有什么问题: 1、命名空间冲突,两个库可能会使用同一个名称2、无法合理地管理项目的依赖和版本;3、无法方便地控制依赖的加载顺序。
CommonJS
Nodejs 广泛使用的一套模块化规范,是一种 同步加载模块依赖的方式, require :引入一个模块,它是值的拷贝exports :导出模块内容module :模块本身 优点:1、代码可复用于 Node.js 环境下并运行,例如做同构应用2、通过 NPM 发布的很多第三方模块都采用了 CommonJS 规范 缺点:无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5
AMD
AMD :异步加载模块依赖的方式,是 RequireJS 提出并且完善的一套模块化规范 require :引入模块define:定义一个模块 id :模块的 iddependencies :模块依赖factory :模块的工厂函数,即模块的初始化操作函数 优点:1、可在不转换代码的情况下直接在浏览器中运行2、异步加载依赖3、可并行加载多个依赖4、代码可运行在浏览器环境和 Node.js 环境下 缺点:JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用
CMD
SeaJS 提出来的一套模块规范
UMD
兼容CommonJS 和 AMD 一套规范,目前多数模块的封装,是既可以在 Node.js 环境又可以在 Web 环境运行,那么一般会采用 UMD 的规范
ES6 Module
ES6 推出的一套模块化规范 import :引入模块依赖,只读值的引用export :模块导出
npm
nodejs 版本管理
在软件版本中碰见的: rc 、 1.x.x 、 alpha 、 beta 等名词,简单来说规范:主版本号 . 次版本号 . 修订号 1. 主版本号:当你做了不兼容的 API 修改;2. 次版本号:当你做了向下兼容的功能性新增;3. 修订号:当你做了向下兼容的问题修正
npm 常用命令
安装和删除:npm install / i packageName@x.x.x --save(-S) / --save-dev(-D)初始化文件,npm init 配置npm config set registry .... npm set :设置环境变量,例如: npm set init-author-name 'Your name' ,初始化的时候会使用默认环境变量;npm info :查看某个包的信息,例如: npm info lodash ;npm search :查找 npm 仓库,后面可以跟字符串或者正则表达式,例如: npm search webpack ;npm list :树形的展现当前项目安装的所有模块,以及对应的依赖,例如: npm list --global 查看全局安装的模块。
npx 命令行
npx 是一个方便开发者访问 node_modules 内的 bin 命令行的小工具, npx webpack -v 相当于执行了node ./node_modules/bin/webpack -v
核心概念
通常你可用如下经验去判断如何配置 Webpack:想让源文件加入到构建流程中去被 Webpack 控制,配置 entry。想自定义输出文件的位置和名称,配置 output。想自定义寻找依赖模块时的策略,配置 resolve。想自定义解析和转换文件的策略,配置 module,通常是配置 module.rules 里的 Loader。其它的大部分需求可能要通过 Plugin 去实现,配置 plugin。
entry
类型:字符串、数组、对象
output
path 目录
filename 文件名
publicPath
指定了一个在浏览器中被引用的 URL 地址,可以作为实际上线到服务器之后的 url 地址
chunkFilename
配置无入口的 Chunk 在输出时的文件名称
占位符
nameidhashchunkhashcontenthash
id
name
hash
整个项目的 hash 值,hash 无法实现前端静态资源在浏览器上长缓存
chunkhash
根据不同的入口文件(entry)进行依赖文件解析,构建对应的 chunk,生成相应的 hash,只要组成 entry 的模块文件没有变化,则对应的 hash 也是不变的,所以一般项目优化时,会将公共库代码拆分到一起,因为公共库代码变动较少的,使用 chunkhash 可以发挥最长缓存的作用
contenthash
使用 chunkhash 存在一个问题,当在一个 JS 文件中引入了 CSS 文件,编译后它们的 hash 是相同的。而且,只要 JS 文件内容发生改变,与其关联的 CSS 文件 hash 也会改变,针对这种情况,可以把 CSS从 JS 中使用mini-css-extract-plugin 或 extract-text-webpack-plugin抽离出来并使用 contenthash。
都可以设置位数,默认20
library
指定库的名称,可以供别人使用的库
libraryTarget
指定库打包出来的规范, output.libraryTarget 取值范围为: var 、 assign 、 this 、 window 、 global 、 commonjs 、 commonjs2 、 commonjs-module 、 amd 、 umd 、 umd2 、 jsonp ,默认是 var
crossOriginLoading
异步加载跨域的资源时使用的方式
module
noParse
忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能,接收的类型为正则表达式,或者正则表达式数组或者接收模块路径参数的一个函数 module: {// 使用正则表达式noParse: /jquery|lodash/// 使用函数,从 Webpack 3.0.0 开始支持noParse: (content) => {// content 代表一个模块的文件路径// 返回 true or false return /jquery|lodash/.test(content); }} 注意:一定要确定被排除出去的模块代码中不能包含 import 、 require 、 define 等内容
rules
将符合规则条件的模块,提交给对应的处理器来处理;数组类型
test
use
loader
模块转化器,模块的处理器,对模块进行转换处理 require('html-loader!./loader.html') 中 ! 类似 Unix 系统中命令行的管道,这里 ! 隔开的命令是 从右到左解析的,即先加载 loader.html 然后在将加载的文件内容传给 html-loader 处理。
postcss
PostCSS 核心是将 CSS 解析成 AST,然后通过插件做转换,最终生成处理后的新 CSS,跟 Babel 在功能和实现上类似,所以真正起作用的还需要依赖其强大的插件系统 PostCSS 的配置写法有以下三种方式:1. 通过配置文件 postcss.config.js ,一般放置在项目的根目录下;2. 通过 loader 的配置项 options ;3. 直接在 package.json 中添加个 postcss 属性
autoprefixer
自动补齐 css 前缀,处理浏览器兼容性
postcss-preset-env
使用最新的 CSS 语法来写样式,不用关心浏览器兼容性
cssnano
CSS 压缩优化 去除空格注释,支持根据 CSS 语法解析结果智能压缩代码,比如合并一些类写法
enforce
loader 的执行顺序放到最前( pre )或者是最后( post )
options
cacheDirectory
开启缓存
oneOf
只应用第一个匹配的规则 {test: /\.css$/,oneOf: [ { resourceQuery: /inline/, // foo.css?inline use: 'url-loader' }, { resourceQuery: /external/, // foo.css?external use: 'file-loader' }]}
include
exclude
parser
更细粒度的配置哪些模块语法要解析,哪些不解 rules: [{test: /\.js$/,use: ['babel-loader'],parser: { amd: false, // 禁用 AMD commonjs: false, // 禁用 CommonJS system: false, // 禁用 SystemJS harmony: false, // 禁用 ES6 import/export requireInclude: false, // 禁用 require.include requireEnsure: false, // 禁用 require.ensure requireContext: false, // 禁用 require.context browserify: false, // 禁用 browserify requireJs: false, // 禁用 requirejs }}] Tips:parser是语法层面的限制,noParse只能控制哪些文件不进行解析
plugin
扩展插件,插件可以处理 chunk , loader 面向的是解决某个或者某类模块的问题,而 plugin 面向的是项目整体,解决的是 loader 解决不了的问题。
extract-text-webpack-plugin
导出 css 文件到单独的内容
mini-css-extract-plugin
将 CSS 从 JS 中抽离到单独到文件
optimize-css-assets-webpack-plugin
压缩css
html-webpack-plugin
生成html模版
clean-webpack-plugin
清空目录重新打包
webpack.optimize.UglifyJsPlugin
压缩JS
webpack-dev-server
devServer: { // DevServer 相关的配置 proxy: { // 代理到后端服务接口 '/api': 'http://localhost:3000' }, contentBase: path.join(__dirname, 'public'), // 配置 DevServer HTTP 服务器的文件根目录 compress: true, // 是否开启 gzip 压缩 historyApiFallback: true, // 是否开发 HTML5 History API 网页 hot: true, // 是否开启模块热替换功能 https: false, // 是否开启 HTTPS 模式 }, profile: true, // 是否捕捉 Webpack 构建的性能信息,用于分析什么原因导致构建性能不佳 cache: false, // 是否启用缓存提升构建速度 watch: true, // 是否开始 watchOptions: { // 监听模式选项 // 不监听的文件或文件夹,支持正则匹配。默认为空 ignored: /node_modules/, // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高 // 默认为300ms aggregateTimeout: 300, // 判断文件是否发生变化是不停的去询问系统指定文件有没有变化,默认每隔1000毫秒询问一次 poll: 1000 }
hot
为 true,开启模块热更新
inline
配置是否自动注入这个代理客户端到将运行在页面里的 Chunk 里去
historyApiFallback
是否开发 HTML5 History API 网页
contentBase
配置 DevServer HTTP 服务器的文件根目录
headers
可以在 HTTP 响应中注入一些 HTTP 响应头
host
port
allowHosts
配置一个白名单列表,只有 HTTP 请求的 HOST 在列表里才正常返回
disableHostCheck
配置项用于配置是否关闭用于 DNS 重绑定的 HTTP 请求的 HOST 检查。 DevServer 默认只接受来自本地的请求,关闭后可以接受来自任何 HOST 的请求
https
clientLogLevel
配置在客户端的日志等级,值为 none | error | warning | info
compress
配置是否启用 gzip 压缩
open
第一次构建完时自动用你系统上默认的浏览器去打开要开发的网页,同时还提供 devServer.openPage 配置项用于打开指定 URL 的网页
proxy
profile
watch
watchOptions
resolve
配置匹配模块的规则,帮助 Webpack 查找依赖模块的,也可以替换对应的依赖(比如开发环境用 dev 版本的 lib 等) 下面是不常用的或者比较简单的配置:resolve.mainFiles :解析目录时候的默认文件名,默认是 index ,即查找目录下面的 index + resolve.extensions 文件;resolve.modules :查找模块依赖时,默认是 node_modules ;resolve.symlinks :是否解析符合链接(软连接,symlink);resolve.plugins :添加解析插件,数组格式;resolve.cachePredicate :是否缓存,支持 boolean 和 function,function 传入一个带有 path 和 require 的对象,必须返回 boolean 值。
extensions
解析扩展名的配置,默认值: ['.wasm', '.mjs', '.js', '.json'],注意在同一个目录下面不能有重名的文件
alias
修改引入的文件路径,编写代码更加方便路径简写引入,使用vscode可能检测不到引入的内容,可以在项目根目录创建 jsconfig.json 来帮助我们定位 //jsconfig.json{"compilerOptions": {"baseUrl": "./src", "paths": { "@lib/": ["src/lib"] } }}
mainFields
针对不同宿主环境提供几份代码,例如提供 ES5 和 ES6 的两份代码,或者提供浏览器环境和 nodejs 环境两份代码 resolve: { mainFields: ['browser', 'module', 'main']}
modules
解析的模块,默认 node_modules
enforceExtension
如果配置为 true 所有导入语句都必须要带文件后缀
enforceModuleExtension
enforceModuleExtension 和 enforceExtension 作用类似,但 enforceModuleExtension 只对 node_modules 下的模块生效
performance
// 输出文件性能检查配置 performance: { hints: 'warning', // 有性能问题时输出警告 hints: 'error', // 有性能问题时输出错误 hints: false, // 关闭性能检查 maxAssetSize: 200000, // 最大文件大小 (单位 bytes) maxEntrypointSize: 400000, // 最大入口文件大小 (单位 bytes) assetFilter: function(assetFilename) { // 过滤要检查的文件 return assetFilename.endsWith('.css') || assetFilename.endsWith('.js'); } },
externals
去除输出的打包文件中依赖的某些第三方 js 模块(例如 jquery , vue 等等),减小打包文件的体积
target
配置输出代码的运行环境,指定构建的目标(target)target 的值有两种类型:string 和 function string 类型支持下面的七种:web :默认,编译为类浏览器环境里可用; node :编译为类 Node.js 环境可用(使用 Node.js require 加载 chunk);async-node :编译为类 Node.js 环境可用(使用 fs 和 vm 异步加载分块);electron-main :编译为 Electron 主进程;electron-renderer :编译为 Electron 渲染进程;node-webkit :编译为 Webkit 可用,并且使用 jsonp 去加载分块。支持 Node.js 内置模块和 nw.gui 导入(实验性质);webworker :编译成一个 WebWorker 函数接收一个 compiler 作为参数,如下面代码可以用来增加插件: target: compiler => { compiler.apply(new webpack.JsonpTemplatePlugin(options.output), new webpack.LoaderTargetPlugin('web'));}
devtool
devtool 构建速度 重新构建速度 生产环境 品质 (quality) 留空, none +++ +++ yes 打包后的代码eva l +++ +++ no 生成后的代码cheap-eval-source-map + ++ no 转换过的代码(仅限行)cheap-module-eval-source-map o ++ no 原始源代码(仅限行)eval-source-map – + no 原始源代码cheap-source-map + o no 转换过的代码(仅限行)cheap-module-source-map o - no 原始源代码(仅限行)inline-cheap-source-map + o no 转换过的代码(仅限行)inline-cheap-module-source-map o - no 原始源代码(仅限行)source-map – – yes 原始源代码inline-source-map – – no 原始源代码hidden-source-map – – yes 原始源代码nosources-source-map – – yes 无源代码内容 +++ 非常快速 , ++ 快速 , + 比较快 , o 中等 , - 比较慢 , -- 慢
stats
stats: { // 控制台输出日志控制 assets: true, colors: true, errors: true, errorDetails: true, hash: true, },
ResolveLoader
告诉 Webpack 如何去寻找 Loader
其他概念
module
开发中每一个文件都可以看做 module ,模块不局限于 js ,也包含 css 、图片等
chunk
代码块,一个 chunk 可以由多个模块组成
bundle
最终打包完成的文件,一般就是和 chunk 一一对应的关系, bundle 就是对 chunk 进行便意压缩打包等处理后的产出
动态导入
动态导入方式: import ()require.ensure()
基础配置
Tips:1、项目逐渐变大, Webpack 的编译时间会变长,可以通过参数让编译的输出。内容带有进度和颜色: webpack --progress --colors ;2、通过参数 --display-error-details 来打印错误详情3、--watch 开启监听模式后,没有变化的模块会在编译后缓存到内存中,而不会每次都被重新编译,所以监听模式的整体速度是很快的4、webpack-cli 支持两个快捷选项: -d 和 -p ,分别代表一些常用的开发环境和生产环境的打包 命令参数: –config :指定一个 Webpack 配置文件的路径;–mode :指定打包环境的 mode ,取值为 development 和 production ,分别对应着开发环境和生产环境;–json :输 mode 出 Webpack 打包的结果,可以使用 webpack --json > stats.json 方式将打包结果输出到指定的文件;–progress :显示 Webpack 打包进度;–watch, -w : watch 模式打包,监控文件变化之后重新开始打包;–color, --colors / –no-color, --no-colors :控制台输出的内容是否开启颜色;–hot :开启 Hot Module Replacement 模式,后面会详细介绍;–profile :会详细的输出每个环节的用时(时间),方便排查打包速度瓶颈
babel
babel 原理: Babel 是一个 JavaScript 的静态分析编译器,babel 进行语法转换需要经过三个过程,解析( Parse ),转换( Transform ),生成( Generate ) 解析:指的是首先将代码经过词法解析和语法解析,最终生成一棵 AST (抽象语法树),在 Babel 中,语法解析器是 Babylon ; 转换:得到 AST 之后,可以对其进行遍历,在此过程中对节点进行添加、更新及移除等操作, Babel 中 AST 遍历工具是 @babel/traverse; 生成:经过一系列转换之后得到的一棵新树,要将树转换成代码,就是生成的过程, Babel 用到的是 @babel/generator
plugins
转换插件
转换插件主要职责是进行语法转换的,比如@babel/preset-env 将 ES6+ 语法转换为ES5语法
解析插件
polyfill
最佳实践: // .babelrc{"plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": false, // 默认值,可以不写 "helpers": true, // 默认,可以不写 "regenerator": false, // 通过 preset-env 已经使用了全局的 regeneratorRuntime, 不再需要 transform-runtime 提供的 不污染全局的regeneratorRuntime "useESModules": true // 使用 es modules helpers, 减少 commonJS 语法代码 } ]],"presets": [ [ "@babel/preset-env", { "targets": {}, // 这里是 targets 的配置,根据实际 browserslist 设置 "corejs": 3, // 添加 core-js 版本 "modules": false, // 模块使用 es modules ,不使用 commonJS 规范 "useBuiltIns": "usage" // 默认 false, 可选 entry , usage } ]]}
preset
插件预设(插件组合)
Browserslist
设置目标浏览器的工具 "browserslist": ["last 2 version", "> 1%", "maintained node versions", "not ie < 11"] 范围 说明last 2 versions caniuse.com 网站跟踪的最新两个版本,假如 iOS 12 是最新版本,那么向后兼容两个版本就是 iOS 11 和 iOS 12> 1% 全球超过 1% 人使用的浏览器,类似 > 5% in US 则指代美国 5% 以上用户cover 99.5% 覆盖 99.5% 主流浏览器chrome > 50 ie 6-8 指定某个浏览器版本范围unreleased versions 说有浏览器的 beta 版本not ie < 11 排除 ie11 以下版本不兼容since 2013 last 2 years 某时间范围发布的所有浏览器版本maintained node versions 所有被 node 基金会维护的 node 版本current node 当前环境的 node 版本dead 通过 last 2 versions 筛选的浏览器中,全球使用率低于 0.5% 且官方声明不在维护或者事实上已经两年没有再更新的版本defaults 默认配置, > 0.5% last 2 versions Firefox ESR not dead
webpack 进阶
代码检查
JS 代码规范
配置 ESLint 代码规范
ESLint 是通过配置规则(Rules)来检测 JavaScript 语法规范的 相比JSLint 和 JSHint ,ESLint 对 ES6 语法支持更好 安装ESLint1、npm install -D eslint2、eslint --init 命令创建 .eslintrc.json3、安装类似 airbnb 、 google 和 standard 的规则,npm install --save-dev eslint-config-standard4、在 .eslintrc.json 配置文件中修改 extends 到对应值(extends 继承那个模块校验)5、制定规则校验
webpck使用 eslint
ESLint 使用npm install -D eslint-loader { test: /\.js$/, loader: 'eslint-loader', enforce: 'pre', include: [path.resolve(__ dirname, 'src')], // 指定检查的目录 options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范 }} ESLint 配置方式 1. ESLint 的配置单独做了一个 Webpack 的 module.rule 配置,所以使用了 enforce: 'pre' 来调整了 loader 加载顺序,保证先检测代码风格,之后再做 Babel 转换等工作;2. 也可以放到 Babel 放到一起,不过要将 eslint-loader 放到 babel-loader 之前检测;3. 这里为了让 ESLint 报错更加好看一些,使用了eslint-formatter-friendly这个 ESLint formatter ,记得安装它: npm i -D eslint-formatter-friendly
CSS 代码规范
检测 CSS 语法使用StyleLint
官方推荐的代码风格有两个:stylelint-config-recommendedstylelint-config-standard StyleLint 的配置文件是 .stylelintrc.json ,其中的写法跟 ESLint 的配置类似,都是包含 extend 和 rules 等内容,下面是一个示例: {"extends": ["stylelint-config-standard", "stylelint-config-recess-order"],"rules": { "at-rule-no-unknown": [true, {"ignoreAtRules": ["mixin", "extend", "content"]}]}} 在 stylelint-webpack-plugin 插件中有两个跟 Webpack 编译相关的配置项:emitErrors :默认是 true ,将遇见的错误信息发送给 webpack 的编辑器处理;failOnError :默认是 false ,如果是 true 遇见 StyleLint 报错则终止 Webpack 编译。
静态资源管理
file-loader
url-loader
包含 file-loader 的全部功能,并且能够根据配置将符合配置的文件转换成 Base64 方式引入,将小体积的图片 Base64 引入项目可以减少 http 请求,也是一个前端常用的优化方式 limit 选项来控制不超过一定限制的图片才使用 Base64 { test: /\.(png|svg|jpg|gif)$/, use: { loader: 'url-loader', options: { limit: 3*1024 // 3k } }} 注意:1、HTML 和 CSS 中使用 alias,要使用 ~ 符号。<img src="~@assets/img/large.png" alt="背景图" />2、HTML 中使用 <img> 引入图片等静态资源的时候,需要添加 html-loader 配置,不然也不会处理静态资源的路径问题。
img-webpack-loader
作用:图片体积优化 用法和 url-loader 一样,不同对是设置 enforce: 'pre' ,这样可以提高了 img-webpack-loader 的优先级,保证在 url-loader 和 svg-url-loader 之前就完成了图片的优化。
postcss-sprites
CSS Sprite 雪碧图制作,在 css-loader 之前配置上 postcss-loader // postcss.config.jsconst postcssSprites = require('postcss-sprites');module.exports = { plugins: [ postcssSprites({ // 在这里制定了从哪里加载的图片被主动使用css sprite // 可以约定好一个目录名称规范,防止全部图片都被处理 spritePath: './src/assets/img/' }) ]};
多页面实践
html-webpack-plugin
template
引用的模版,相当于复制页面
filename
chunks
根据entry不同入口打包构建的chunks
多页面最佳实践
const glob = require("glob")const path = require("path")const htmlWebpackPlugin = require('html-webpack-plugin') // 多页面打包通用方案const setMPA = () => { const entry = {} const htmlWebpackPlugins = [] const entryFiles = glob.sync(path.join(__dirname, "../src/module/*/index.js")) Object.keys(entryFiles).map(item => { const entryFile = entryFiles[item] const match = entryFile.match(/src\/module\/(.*)\/index\.js$/) const pageName = match && match[1] entry[pageName] = entryFile htmlWebpackPlugins.push( new htmlWebpackPlugin({ title: pageName, template: path.join(__dirname, `../src/module/${pageName}/index.html`), filename: `${pageName}.html`, chunks: [pageName], inject: true }) ) }) return { entry, htmlWebpackPlugins }}const { entry, htmlWebpackPlugins } = setMPA()
webpack-dev-server 本地开发服务
webpack-dev-server 是由 webpack-dev-middleware 和 webpack-hot-middleware 中间件来实现 命令参数--host ip地址--port 修改端口号--hot --inline 启动 inline 模式自动刷新--config 指定 webpack confg 文件--mode 指定的 webpack 打包mode模式--watch 文件变化则触发重新编译--content-base 修改最基本的目录 Webpack Dev Server 常用配置devServer.historyApiFallback :配置如果找不到页面就默认显示的页面;devServer.compress :启用 gzip 压缩;devServer.hotOnly :构建失败的时候是否不允许回退到使用刷新网页;devServer.inline :模式切换,默认为内联模式,使用 false 切换到 iframe 模式;devServer.open :启动后,是否自动使用浏览器打开首页;devServer.openPage :启动后,自动使用浏览器打开设置的页面;devServer.overlay :是否允许使用全屏覆盖的方式显示编译错误,默认不允许;devServer.port :监听端口号,默认 8080;devServer.host :指定 host,使用 0.0.0.0 可以让局域网内可访问;devServer.contentBase :告诉服务器从哪里提供内容,只有在你想要提供静态文件时才需要;devServer.publicPath :设置内存中的打包文件的虚拟路径映射,区别于 output.publicPath ;devServer.staticOptions :为 Expressjs 的 express.static 配置参数,参考文档:http://expressjs.com/en/4x/api.html#express.staticdevServer.clientLogLevel :在 inline 模式下用于控制在浏览器中打印的 log 级别,如 error , warning , info or none ;devServer.quiet :静默模式,设置为 true 则不在控制台输出 log;devServer.noInfo :不输出启动 log;devServer.lazy : 不监听文件变化,而是当请求来的时候再重新编译;devServer.watchOptions :watch 相关配置,可以用于控制间隔多少秒检测文件的变化;devServer.headers :自定义请求头,例如自定义 userAgent 等;devServer.https :https 需要的证书签名等配置。
自动刷新页面
iframe 模式:页面被放到一个 iframe 内,当发生变化时,会重新加载;inline 模式:将 webpack-dev-server 的重载代码添加到产出的 bundle 中。 使用方式: webpack-dev-server --hot --inline 是开启 inline 模式的自动刷新。
proxy代理
module.exports = {//... devServer: { proxy: { context: ['/auth', '/api'], // 匹配两个代理地址 '/api': { target: 'http://baidu.com', secure: false, // 转发的网站支持 https pathRewrite: {'^/api': ''} } } }} webpack-dev-server 使用了 http-proxy-middleware中间件来实现的 proxy 功能,所以更多配置项及其实现可以直接参考 http-proxy-middleware的文档
自定义中间件
devServer.before 和 devServer.after 可以实现自己的中间件,在webpack-dev-server 加载所有内部中间件之前和之后两个时机。
mock server
webpack-dev-server 提供了自定义中间件的 Hook,下面代码是在 devServer.before 插入一个接口 /api/mock.json 的接口响应: module.exports = {//... devServer: { port: 9000, before(app, server) { app.get('/api/mock.json', (req, res) => { res.json({hello: 'world'}); }); } }};
devServer.compress:服务开启 Gzip 压缩
webpack 环境配置
开发环境和生产环境的区别 生产环境可能需要分离 CSS 成单独的文件,以便多个页面共享同一个 CSS 文件;生产环境需要压缩 HTML/CSS/JS 代码;生产环境需要压缩图片;开发环境需要生成 SourceMap 文件;开发环境需要打印 debug 信息;开发环境需要 HMR、devServer 等功能… 按环境划分 Webpack 配置文件webpack.config.js :所有环境的默认入口配置文件;webpack.base.js :基础部分,即多个文件中共享的配置;webpack.development.js :开发环境使用的配置;webpack.production.js :生产环境使用的配置。
webpack 优化实践
构建速度优化
影响 Webpack 构建速度的有两个「大户」:一个是 loader 和 plugin 方面的构建过程,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间 优化构建过程,可以从减少查找过程、多线程、提前编译和 Cache 多个角度来优化
缩小文件查找范围
test,include, exclude
优化 resolve.modules 配置
modules: [path.resolve(__dirname, 'node_modules')]
优化 resolve.mainFields 配置
resolve.alias
resolve.extensions 优先查找
优化 module.noParse 配置
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件递归解析处理,例如:jQuery、ChartJS,它们体积庞大又没有采用模块化标准,
使用 webpack.DllPlugin 来预先编译
预先编译和打包不会变动存在的文件,在业务代码中直接引入,加快 Webpack 编译打包的速度,但是并不能减少最后生成的代码体积。 要使用 DllPlugin 的功能,需要配合 webpack.DllReferencePlugin 来使用。
利用多线程提升构建速度
多线程打包有两种方案:thread-loader和HappyPack thread-loader 是针对 loader 进行优化的,它会将 loader 放置在一个 worker 池里面运行,以达到多线程构建。thread-loader 在使用的时候,需要将其放置在其他 loader 之前 HappyPack 是通过多进程模型,来加速代码构建 给 loader 配置使用 HappyPack 需要对应的 loader 支持才行,例如 url-loader 和 file-loader 就不支持 HappyPack,在 HappyPack 的 wiki 中有一份支持 loader 的列表。
使用 ParallelUglifyPlugin 多进程并行打包
使用自动刷新
module.export = { // 只有在开启监听模式时,watchOptions 才有意义 // 默认为 false,也就是不开启 watch: true, // 监听模式运行时的参数 // 在开启监听模式时,才有意义 watchOptions: { // 不监听的文件或文件夹,支持正则匹配 // 默认为空 ignored: /node_modules/, // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高 // 默认为 300ms aggregateTimeout: 300, // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的 // 默认每隔1000毫秒询问一次 poll: 1000 }}
压缩速度优化
使用terser-webpack-plugin的时候可以通过下面的配置开启多线程和缓存: const TerserPlugin = require("terser-webpack-plugin");module.exports = { optimization: { minimizer: [ new TerserPlugin({ cache: true, // 开启缓存 parallel: true, // 多线程 }), ], },};
其他构建优化
1. sourceMap 生成耗时严重,根据之前 sourceMap[TODO](sourcemap 表格链接)表格选择合适的 devtool 值;2. 切换一些 loader 或者插件,比如:fast-sass-loader可以并行处理 sass 文件,要比 sass-loader 快 5~10 倍;3. babel-loader 的 cacheDirectory
体积优化
JavaScript 压缩
推荐 terser-webpack-plugin 长期维护,跟 uglifyjs-webpack-plugin 相同的参数,我们在 Webpack 中可以通过配置文件直接调用 const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [new TerserPlugin()] }}; 在实际开发中,我们可以通过移出一些不用的代码从而达到优化代码体积的作用,Tree-Shaking 也是依赖这个插件的: new TerserPlugin({// 使用 cache,加快二次构建速度 cache: true, terserOptions: { comments: false, compress: { // 删除无用的代码 unused: true, // 删掉 debugger drop_debugger: true, // eslint-disable-line // 移除 console drop_console: true, // eslint-disable-line // 移除无用的代码 dead_code: true // eslint-disable-line } }}); 在 Webpack 配置中可以通过开启 terser-webpack-plugin 的多线程压缩来加速我们的构建压缩速度: new TerserPlugin( parallel: true // 多线程)],
CSS 优化
1、分离 CSS 文件,按需引入—— mini-css-extract-plugin2、CSS 压缩:cssnano,css-loader 已经集成了 cssnano,还可以使用 optimize-css-assets-webpack-plugin来自定义 cssnano 的规则 删除空格和最后一个分号;删除注释;优化字体权重;丢弃重复的样式规则;压缩选择器;减少手写属性;合并规则; // webpack.config.jsconst OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');module.exports = { plugins: [ new OptimizeCssAssetsPlugin({ assetNameRegExp: /\.optimize\.css$/g, cssProcessor: require('cssnano'), // 这里制定了引擎,不指定默认也是 cssnano cssProcessorPluginOptions: { preset: ['default', {discardComments: {removeAll: true}}] }, canPrint: true })]};
Tree-Shaking
移除 JavaScript 上下文中没用的代码,这样可以有效地缩减打包体积 使用情况 1. 减少代码中的副作用代码;2. 配置 sideEffects 告诉 webpack 模块是安全的,不会带有副作用,可以放心优化。3、要使用 Tree-Shaking 必然要保证引用的模块都是 ES6 规范的4、按需引入模块,避免「一把梭」,例如我们要使用 lodash 的 isNumber ,可以使用 import isNumber from 'lodash-es/isNumber'; ,而不是 import {isNumber} from 'lodash-es' ;
Scope Hoisting
作用域提升(Scope Hoisting)是指 webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同一个函数中。 开启了 Scope Hoisting // webpack.config.jsmodule.exports = { optimization: { concatenateModules: true }};
图片资源优化
1、url-loader设置 limit 转化为 Base64 减少请求,svg-url-loader 同理2、雪碧图插件合并小图3、用image-webpack-loader来压缩图片
其他优化技巧和方案
1. 合理划分代码职责,适当使用按需加载方案;2. 善用 webpack-bundle-analyzer 插件,帮助分析 Webpack 打包后的模块依赖关系;3. 设置合理的 SplitChunks 分组;4. 对于一些 UI 组件库,例如 AntDesign、ElementUI 等,可以使用bable-plugin-import这类工具进行优化;5. 使用 lodash、momentjs 这类库,不要一股脑引入,要按需引入,momentjs 可以用 date-fns 库来代替;6. 合理使用 hash 占位符,防止 hash 重复出现,导致文件名变化从而 HTTP 缓存过期;7. 合理使用 polyfill,防止多余的代码;8. 使用 ES6 语法,尽量不使用具有副作用的代码,以加强 Tree-Shaking 的效果;9. 使用 Webpack 的 Scope Hoisting(作用域提升)功能。 Tips:其实 webpack 4 中,在 production 模式下已经根据大多数项目的优化经验做了通用的配置,类似Tree-Shaking、Scope Hoisting 都是默认开启的,而且最新版本的 Webpack 使用的压缩工具就是 terser-webpack-plugin。
按需加载
import( )require.ensure( )
增强缓存命中率
利用 HTTP 请求头的缓存相关字段 chunkhash 和 contenthash 可以做到根据文件内容和依赖关系变化而增强浏览器缓存 Webpack 中拆分代码用到的是动态加载方式和 optimization.splitChunks
代码拆分
在 Webpack 中,总共提供了三种方式来实现代码拆分(Code Splitting): entry 配置:通过多个 entry 文件来实现; 动态加载(按需加载):通过写代码时主动使用 import() 或者 require.ensure 来动态加载; 抽取公共代码:使用 splitChunks 配置来抽取公共代码。
spitChunks 拆分代码
默认配置
module.exports = { optimization: { splitChunks: { chunks: 'async', // 三选一: "initial" | "all" | "async" (默认) minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk // 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了 maxSize: 0, // 文件的最大尺寸,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小 maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了 maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候 // 就不会抽取 initial common 了 automaticNameDelimiter: '~', // 打包文件名分隔符 name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个 cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级 }, default: { minChunks: 2, priority: -20, // 优先级 reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个 } } } }};
核心配置
splitChunks.chunks 三个值
initial 即原始的最初的意思,原则就是有共用的情况即发生拆分
cacheGroups 缓存组,最核心的配置
默认有两个 cacheGroup : vendors 和 default
priority 权重
cacheGroup.test
满足这个条件的才会被缓存组命中 module :每个模块打包的时候,都会执行 test 函数,并且传入模块 module 对象,module 对象包含了模块的基本信息,例如类型、路径、文件 hash 等; chunks :是当前模块被分到哪些 chunks 使用,module 跟 chunks 关系可能是一对一,也可能是多对一,所以一旦我们使用 chunks 做匹配,那么符合条件的 chunk 内包含的模块都会被匹配到。 test(module, chunks) {//... return module.type === 'javascript/auto';}
小结
Webpack 配置的最佳实践1. 生产环境使用mini-css-extract-plugin导出 CSS 文件;2. 生产环境使用压缩功能,包括 JavaScript、CSS、图片、SVG 等;3. 合理配置查找路径,减少查找时间,比如设置 alias 、添加项目路径、排查 nodemodules 查找等;4. 在 rule 配置上,有 test 、 include 、 exclude 三个可以控制范围的配置,最佳实践是: 只在 test 和 文件名匹配 中使用正则表达式; 在 include 和 exclude 中使用绝对路径数组; 尽量避免 exclude ,更倾向于使用 include 。5. icon 类图片文件太多使用 CSS Sprite 来合并图片,防止设置 url-loader 和 svg-url-loader 的 limit 值不合理,导致 icon 文件都以 Base64 方式引入 CSS 文件中,导致 CSS 文件过大。 其他方面最佳实践 1. 规范化 Git 工作流: 1. 使用 Git Hook,类似 Husky这类 Git Hook 库,可以帮助我们在每次提交之前( pre-commit )自动做 lint 检查; 2. 使用Commitizen来规范 Git 的提交 Commit Log;2. 组件化开发,公共 UI 组件、公共函数库建设;这里说的比较抽象,需要具体项目来划分,例如我们可以将多个页面常用的 UI 组件抽象出来;也可以将通用的工具函数库建设起来,类似 Lodash 这类库;3. 选择一个顺手的 CSS 预处理语言,Sass、Less、Stylus,只需要团队使用顺手即可;4. 指定规则约定,包括代码规范、目录结构,文档规范等;5. 前后端分离,选择合适的 Mock 方案;6. 将最佳实践做成标准项目的脚手架,新项目使用脚手架工具来创建;7. 抽象解决方案,融合到 Webpack 配置中,甚至是基于 Webpack 做自己的最佳实践工具链!这个部分在实战篇会用具体案例来介绍一些实用的解决方案。
webpack 源码解读
Webpack 本质上上一种事件流机制 Tapable 的执行流程可以分为四步: 1. 使用 tap* 对事件进行注册绑定。根据类型不同,提供三种绑定的方式: tap 、 tapPromise 、 tapAsync ,其中 tapPromise 、 tapAsync 为异步类 Hook 的绑定方法; 2. 使用 call* 对事件进行触发,根据类型不同,也提供了三种触发的方式: call 、 promise 、 callAsync ; 3. 生成对应类型的代码片段(要执行的代码实际是拼字符串拼出来的); 4. 生成第三步生成的代码片段。
webpack 调试
使用 VSCode debugger 调试工具,在命令行执行 npm run build 或 npx webpack 实际上是执行 node_modules 下 bin 目录下 对应的 webpack 文件,webpack 文件会执行 webpack-cli 下 bin 目录下的 cli.js 文件,整个文件中主要使用 Yargs 包来做 bin 命令的选项解析器,解析出执行的是什么命令,从而调用对应的方法做对应的事情,
Tapable 模块
Webpack 负责编译的 Compiler 和创建 Bundle 的 Compilation 都是继承自 Tapable
同步 Hook
类型 | 使用要点Basic 基础类型 | 不关心监听函数的返回值,不根据返回值做事情Bail 保险式 | 只要监听函数中有返回值(不为 undefined ),则跳过之后的监听函数Waterfal 瀑布式 | 上一步的返回值继续交给下一步处理和使用Loop 循环类型 | 如果该监听函数返回 true 则这个监听函数会反复执行,如果返回 undefined 则退出循环
SyncHook 串行
SyncBaiHook 串行
SyncWaterfallHook 串行
SyncLoopHook 循环
异步 Hook
AsyncSeriesHook 串行
AsyncSeriesBailHook 串行
AsyncSeriesWaterfallHook 串行
AsyncParallelHook 并行
AsyncParallelBailHook 并行
compiler
每次执行 Webpack 构建的时候,在 Webpack 内部,会首先实例化一个 Compiler 对象,然后调用它的 run 方法来开始一次完整的编译过程 compiler.run 工作流程 -> beforeRun-> run-> normalModuleFactory-> contextModuleFactory-> beforeCompile-> compile-> thisCompilation-> compilation-> make-> afterCompile-> shouldEmit-> emit-> afterEmit-> done compiler 是管理整个生命周期的,而 compilation 是每次编译触发都会重新生成一次的。
entryOption
Tapable 类型: SyncBailHook 触发时机:在 webpack 中的 entry 配置处理过之后
afterPlugins
Tapable 类型: SyncHook 触发时机:初始化完内置插件之后
afterResolvers
Tapable 类型: SyncHook 触发时机:resolver 完成之后
environment
Tapable 类型: SyncHook 触发时机:准备编译环境,webpack plugins配置初始化完成之后
afterEnvironment
Tapable 类型: SyncHook 触发时机:编译环境准备好之后
beforeRun
Tapable 类型: SyncSeriesHook 触发时机:开始正式编译之前
run
Tapable 类型: SyncSeriesHook 触发时机:开始编译之后,读取 records 之前;监听模式触发 watch-run
watchRun
Tapable 类型: SyncSeriesHook 触发时机:监听模式下,一个新的编译触发之后
normalModuleFactory
Tapable 类型: SyncHook 触发时机:NormalModuleFactory 创建之后
contextModuleFactory
Tapable 类型: SyncHook 触发时机:ContextModuleFactory 创建之后
beforeCompile
Tapable 类型: SyncSeriesHook 触发时机:compilation 实例化需要的参数创建完毕之后
compile
Tapable 类型: SyncHook 触发时机:一次 compilation 编译创建之前
thisCompilation
Tapable 类型: SyncHook 触发时机:触发 compilation 事件之前执行
compilation
Tapable 类型: SyncHook 触发时机:compilation 创建成功之后
make
Tapable 类型: SyncParallelHook 触发时机:完成编译之前
afterCompile
Tapable 类型: AsyncSeriesHook 触发时机:完成编译和封存(seal)编译产出之后
shouldEmit
Tapable 类型: SyncBailHook 触发时机:发布构建后资源之前触发,回调必须返回 true / false , true 则继续
emit
Tapable 类型: AsyncSeriesHook 触发时机:生成资源到 output 目录之前
afterEmit
Tapable 类型: AsyncSeriesHook 触发时机:生成资源到 output 目录之后
done
Tapable 类型: SyncHook 触发时机:compilation完成之后
failed
Tapable 类型: SyncHook 触发时机:compilation 失败
invalid
Tapable 类型: SyncHook 触发时机:编译模式下,编译无效时
watchClose
Tapable 类型: SyncHook 触发时机:监听模式停止
compilation
在 Compilation 阶段,模块会被加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed)和重新创建(restored) 当 Webpack 以监听(watch)模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展,通过 Compilation 也能读取到 Compiler 对象。
工作流程
在 Compilation 中处理的对象分别是 module 、 chunk 、 asset ,由 modules 组成 chunks ,由 chunks 生成 assets ,处理顺序是: module → modules → chunks → assets ,先从单个 module 开始处理,查找依赖关系,最后完成单个 module 处理,完成全部modules 之后,开始 chunks 阶段处理,最后在根据优化配置,按需生成 assets。
Stats
在 Webpack 的回调函数中会得到 stats 对象。这个对象实际来自于 Compilation.getStats() ,返回的是主要含有modules 、 chunks 和 assets 三个属性值的对象 modules:记录了所有解析后的模块;chunks:记录了所有chunk;assets:记录了所有要生成的文件。 使用场景:webpack-bundle-analyzer这类分析打包结果的插件都是通过分析 Stats 对象来得到分析报告的
webpack 工作流程
Webpack 可以看做是一个工厂车间, plugin 和 loader 是车间中的两类机器,工厂有一个车间主任和一个生产车间。车间主任叫 Compiler ,负责指挥生产车间机器 Compilation 进行生产劳动, Compilation 会首先将进来的原材料( entry )使用一种叫做 loader 的机器进行加工,生产出来的产品就是 Chunk ; Chunk 生产出来之后,会被组装成 Bundle ,然后通过一类 plugin 的机器继续加工,得到最后的 Bundle ,然后运输到对应的仓库(output)。这个工厂的生产线就是 Tapable,厂子运作的整个流程都是生产线控制的,车间中有好几条生产线,每个生产线有很多的操作步骤( hook ),一步操作完毕,会进入到下一步操作,直到生产线全流程完成,再将产出传给下一个产品线处理。整个车间生产线也组成了一条最大的生产线。
基本流程分析阶段
基本工作流程分为三个阶段: 准备阶段:主要任务是创建 Compiler 和 Compilation 对象;编译阶段:这个阶段任务是完成 modules 解析,并且生成 chunks; - module 解析:包含了三个主要步骤,创建实例、loaders 应用和依赖收集; - chunks 生成,主要步骤是找到每个 chunk 所需要包含的 modules 。产出阶段:这个阶段的主要任务是根据 chunks 生成最终文件,主要有三个步骤:模板 Hash 更新,模板渲染chunk,生成文件。
工作流程伪代码分析
1. 初始化参数:包括从配置文件和 shell 中读取和合并参数,然后得出最终参数;shell 中的参数要优于配置文件的;2. 使用上一步得到的参数实例化一个 Compiler 类,注册所有的插件,给对应的 Webpack 构建生命周期绑定Hook;3. 开始编译:执行 Compiler 类的 run 方法开始执行编译;4. compiler.run 方法调用 compiler.compile ,在 compile 内实例化一个 Compilation 类, Compilation 是做构建打包的事情,主要事情包括: 1)查找入口:根据 entry 配置,找出全部的入口文件; 2)编译模块:根据文件类型和 loader 配置,使用对应 loader 对文件进行转换处理; 3)解析文件的 AST 语法树; 4)找出文件依赖关系; 5)递归编译依赖的模块。5. 递归完后得到每个文件的最终结果,根据 entry 配置生成代码块 chunk;6. 输出所有 chunk 到对应的 output 路径。
小结
Tapbale:Webpack 事件流程核心类; Compiler:Webpack 工作流程中最高层的对象,初始化配置,提供 Webpack 流程的全局钩子,比如 done 、 compilation 这类; Compilation:由 Compiler 来创建的实例对象,是每次打包流程最核心的流程,该对象内进行模块依赖解析、优化资源、渲染 runtime 代码等事情,下面在 Compilation 中还有用到的一些对象: Resolver:解析模块(module)、loader 等路径,帮助查找对应的位置; ModuleFactory:负责构造模块的实例,将 Resolver 解析成功的组件中把源码从文件中读取出来,然后创建模块对象; Template:主要是来生成 runtime 代码,将解析后的代码按照依赖顺序处理之后,套上 Template 就是我们最终打包出来的代码。
webpack 原理
编写loader
Loader 是一个函数,单一的职责,纯函数 Loader 链式调用是从右往左,使用了 compose 函数组合方式,pitch 函数可以做到从左到右
loader-runner 调试 loader
loader-utils 参数获取
通过 loader-untils 的 getOptions 方法获取参数
同步处理
callback 返回值
异步处理
this.async 返回一个异步的 callback
在 loader 使用缓存
loader 如何进行文件输出
this.emitFile 输出文件内容
使用场景
开发自动合成雪碧图的loader
spritesmith
线上代码异常捕获
函数执行加上 try …… catch 捕获异常
国际化
编写plugin
webpack 调试 plugin
开启调试模式:node filepath --inspect --inspect-brk --inspect 开启调试模式--inspect-brk 第一行打断点
使用场景
插件拓展:编写插件的插件
编写压缩构建资源为zip包的插件
webpack 项目实践
使用 Vue 框架
loader
vue-loader
解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理
vue-template-compiler
把 vue-loader 提取出的 HTML 模版编译成对应的可执行的 JavaScript 代码,这和 React 中的 JSX 语法被编译成 JavaScript 代码类似。预先编译好 HTML 模版相对于在浏览器中再去编译 HTML 模版的好处在于性能更好。
移动端适配
vw + rem 兼容
// 此段代码需要放到html页面 head 中(function(doc, win) { // 创建一个元素,检测是否支持1vw var dummy = doc.createElement('_').style; dummy.width = '1vw'; if (dummy.width) { // 设置root fontsize 为1vw doc.documentElement.style.fontSize = '1vw'; // 支持就不在做处理 return; } // 如果不支持,那么就用 JavaScript 来计算 font-size var docEl = doc.documentElement, resizeEvt = 'orientationchange' in win ? 'orientationchange' : 'resize', recalc = function() { var clientWidth = docEl.clientWidth; if (!clientWidth) { return; } docEl.style.fontSize = clientWidth / 100 + 'px'; }; // 初始化 font-size recalc(); // 添加事件绑定 win.addEventListener(resizeEvt, recalc, false);})(document, window);
postcss 插件:postcss-plugin-pr2rem