- 本阶段主要以前端工程化为主题,分别从脚手架工具、自动化构建、模块化开发、规范化标准四个维度介绍前端工程化具体该如何落地、如何实践,以此应对复杂前端应用的开发和维护过程,提高开发者的工作效率,降低项目维护成本,从而更好地适应大前端时代下的前端开发工作。
模块二 模块化开发与规范化标准
- 此模块中会介绍当下前端开发过程中最重要的开发范式:模块化,我们会介绍模块化在前端行业的演进过程、如何实现模块化开发以及 Webpack 打包工具的使用和核心工作原理;另外我们还会为你介绍几个其它的打包工具;最后通过落实规范化,通过工具保证团队的代码质量,让整个团队的代码更漂亮。
任务一:模块化开发
- 模块化概述
- 模块化开发:当前最重要的前端开发范式
- 内容概要
- 模块化演变过程
- 模块化规范
- 常用的模块化打包工具
- 基于模块化工具构建现代Web应用
- 打包工具的优化技巧
- 模块化的演进过程
- 第一阶段:基于文件划分的形式
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖关系
- 早期模块化完全依靠约定
- 第二阶段:命名空间方式
- 每个模块只暴露一个对象,其它所有属性都挂载在这个对象上
- 第三阶段: IIEF 需要暴露的成员挂载到全局对象上,实现了私有成员,通过传入参数,可以很清楚的知道依赖关系
- 都是通过script标签手动的引入。模块加载不受代码控制
- 模块化规范的出现
- 模块化标准 + 模块加载器
- Commonjs 规范 Nodejs提出的标准
- 一个就是一个模块
- 每个模块都有单独的作用域
- 通过 module.exports导出成员
- 通过require函数载入模块
- CommonJS是以同步模式加载模块,因为Node是在启动时加载模块,执行过程中不需要加载
- 如果在浏览器端使用会出现问题,每一次加载都会出现大量的同步请求
- AMD 异步模块定义规范
- Require.js 实现了AMD这个规范,本身是个非常强大的模块加载器
- 目前绝大部分第三份库都支持AMD规范
- AMD 使用起来相对复杂
- 如果模块划分过细,那么模块JS文件请求频繁,导致页面效率低下
- Sea.js + CMD 淘宝 类似CommonJS规范,后来这种方式也被Require.js兼容了
- 模块化标准规范
- 浏览器: ES Modules ; Node: Commonjs
- CommonJS in Node.js 内置的模块系统
- ES Modules ECMAScript2015(ES6)中定义的模块规范,在语言层面实现了模块化
- ES Modules 特性
- 通过给Script 添加type=module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码
- ESM 自动采用严格模式, 忽略 ‘use strict’
- 每个 ES Module 都是运行在单独的私有作用域中
- ESM 是通过CORS 的方式请求外部JS 模块的
- CORS 不支持文件的形式,必须通过http server 的方式去访问(开发环境不会遇到,因为都是通过 http server方式)
- ESM 的script 标签会延迟执行脚本
- ES Modules 导入和导出
- export 导出 import 导入
- browser-sync . --files **/*.js
- export { name as fooName }
- ES Modules 导入导出的注意事项
- export 后面必须带花括号{},这是一种固定的语法,并不是字面量。如果要带字面量,export default {}
- 导出的是这个成员的引用,并不是复制,给的只是引用关系
- 导入的成员是只读的,外部并不能修改
- 外部导入的值会受模块内部对该值修改的影响
- import {}也不是解构,只是一个固定的语法
- ES Modules 导入用法
- import { name } from ‘./module.js’ from后面的文件路径和后缀不能省略
- ./ 也不能省略,省略会认为是加载第三方的模块
- 也可以使用 /绝对路径,从网站根目录开始
- 也可以通过完整 url 加载模块
- import {}from ‘./module.js’ 如果花括号为空,只会去执行这个模块,不会提取其中的成员,简写: import ‘./module.js’
- import * as mod from ‘./module.js’ 导出成员过多,通过* + as的方式放到一个变量中
- import 是一个声明,只能出现在最顶层
- 通过 import(’./module.js’) 这个函数可以动态的加载模块,返回的是一个promsie
- import abc, {name, age} from ‘./module.js’ 逗号左边的导出的abc是默认成员
- ES Modules 导出导入成员
- 可以在当前模块直接导出导入的成员,直接在当前模块把 import 改成 export
- import { name } from ‘./module.js’ 》 export { name } from ‘./module.js’
- 在index 中会用到,把本项目中散落的模块暴露出去,方便外部使用
- ES Modules in Browser 浏览器环境Polyfill
- browser-es-module-loader 只要在网页中引用这个文件,就可以解决浏览器不兼容ES Modules的问题
- 针对npm 的模块,可以通过 unpkg.com 这个网站提供的cdn服务拿到下面所有的js文件
- unpkg.com/包名
- 需要给为支持Polyfill引入 的scirpt 标签 加上 nomodule 属性,不然在支持module的浏览器中,模块会执行两次
- 原理是在运行阶段,动态的解析脚本,效率会非常差,不适合开发环境用
- ES Modules in Node.js 支持情况
- Node.js 8.5 后就开始以实验的方式支持 ES Modules
- nodejs/node 》 cjs + esm = 用于加载Commonjs 模块
- 需要把模块文件后缀由.js改成.mjs
- 执行时需要加允许实验的特性: node --experimental-modules index.mjs
- 无论是原生模块还是第三方模块都可以通过 import 方式导入
- 第三方模块只支持默认导出,不支持{}的方式,因为import {}不是解构
- 内置模块兼容了ESM 的提取成员方式。导出了多个对象,再做了默认导出
- ES Modules in Node.js 与 CommonJS 模块交互
- ES Module 中可以导入 CommonJS 模块
- 不能直接提取成员,注意import 不是解构导出对象
- CommonJS 模块始终只会导出一个默认成员
- 不能在CommonJS 模块中通过 require 载入 ES Module
- ES Modules in Node.js - 与 CommonJS 差异
- ESM 中没有CommonJS 中的那些模块全局成员了
- CommonJS 中的模块全局成员:require 加载模块函数 module 模块对象 exports 导出对象别名 __filename 当前文件的绝对路径 __dirname 当前文件所在目录,看源码并不是全局对象,而是伪全局对象,包装的形参
- import.meta.url 拿到的就是当前文件的工作地址
- import { fileURLToPath } from ‘url’
- import { dirname } from ‘path’
- const __filename = fileURLToPath(import.meta.url)
- const __dirname = dirname(__filename)
- ES Modules in Node.js 新版本进一步支持ESM
- nvm use 12.10.0
- 在 package.json 中添加 type: ‘module’,就不用修改为.mjs的扩展名了
- 但是如果还想要使用CommonJS,就需要给当前文件修改为.cjs 为扩展名
- node --experimental-modules index.cjs
- ES Modules in Node.js 兼容方案
- 早期node版本,可以使用Babel进行兼容,最主流的一款编译器,可以把使用了新特性的代码编译成当前环境支持的代码
- yarn add @babel/node @baabel/core @babel/preset-env --dev
- babel 基于插件实现的,核心模块并不会转换代码
- @babel/preset-env 是一个插件的集合
- yarn babel-node index.js --presets=@babel/preset-env
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oYmqvDdU-1626104766415)(…/img/1625201409619.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TCCmYIr-1626104766418)(…/img/1625201487046.jpg)]
- 也可以通过添加 .babelrc 文件 》 { “presets”: ["@babel/preset-env"] } 》 yarn babel-node index.js
- yarn add @babel/plugin-transform-modules-commonjs --dev 实际转换的插件
- .babelrc 文件 》 { “plugin”: ["@babel/plugin-transform-modules-commonjs "] } 》 yarn babel-node index.js 需要用到哪个引哪个
任务二:Webpack 打包
- 模块打包工具的由来
- ES Modules 存在环境兼容问题
- 通过模块化划分模块文件过多,网络请求频繁
- 所有的前端资源都需要模块化
- 开发阶段 》编译 打包 》 生产环境
- 对于整个前端应用而言的模块化解决方案,不单单是JavaScript
- 新特性代码编译
- 模块化JavaScript打包
- 支持不同类型的模块资源
- 模块打包工具概要
- webpack 模块打包器(Module bundler)
- 有兼容问题的代码,可以在打包阶段通过模块加载器(Loader)进行编译转换
- 代码拆分(Code Solitting)将应用中所有的代码按照需要去打包。渐进式加载:首次先加载需要立即执行的代码,后续再加载其它代码
- 资源模块(Asset Module) 支持以模块化的方式加载任意资源
- 打包工具解决的是前端整体的模块化
- Webpack 快速上手
- yarn init --yes
- yarn add webpack webpack-cli --dev
- yarn webpack --version
- yarn webpack 打包
- packaage.json “scripts”: { “build”: “webpack” } yarn build
- Webpack 配置文件
-
Webpack 4以后的版本支持0配置打包
- 按照约定 ‘src/index.js’ > ‘dist/main.js’
- webpack.config.js 运行在node环境,按照CommonJS规范编写
const path = require('path') module.exports = { entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output') } }
- Webpack 工作模式
- 针对于不同环境的几组预设配置(工作模式)
- 默认是打包上线
- yarn webpack --mode development
- yarn webpack --mode none 运行最原始的打包,不会做如何处理
- 也可以在配置文件中添加配置属性 mode: ‘development’
- Webpack 打包结果运行原理
- 把 mode:‘none’ 可以看到webpack 打包的原始结果
- 有一个IEEF 自执行函数把全部代码包裹
- 一个数组,里面放着所有模块
- 一个模块管理的函数,在函数上挂载三个方法
- .d: 如果 需要导出的对象这个属性是自身的且模块列表(exports)中没有,则加入exports)
- .o: 判断对象自身(非继承)是否有这个属性
- .r: 把每个模块导出结果都加一个__esModule,标记为 ES Module 规范
- 最后是入口函数自调用
- Webpack 资源模块加载
- 默认default-loader只能处理js文件,处理其它文件需要加载other-loader
- yarn add css-loader --dev 只会把css打包成一个模块
- yarn add style-loader --dev 会把打包好的css模块以style 标签的形式追加到页面
- 在配置文件中配置load
module.exports = { module: { rules: [ { test: /.css$/, use: ['style-loader','css-loader'] // 由后往前调用 } ] } }
- Loader 是 Webpack的核心特性,通过Loader 就可以加载任意类型的资源
- Webpack 导入资源模块
- 打包入口 》运行入口
- 根据代码的需要动态导入资源
- 需要资源的不是应用,而是代码
- 建立代码和资源的依赖关系
- JavaScript 驱动整个前端应用
- 逻辑合理,JS确实需要这些资源文件
- 确保上线资源不丢失,都是必要的
- Webpack 文件资源加载器
- yarn add file-loader --dev
- output: {publicPath: ‘dist/’} 配置资源默认路径
- 图片加载器先是把图片拷贝到输出目录,如何把图片路径当成是这个图片模块的返回值返回,这样对于应用来说,所需要的资源就发布成功了,可以通过模块的导出成员拿到图片路径
- Webpack URL 加载器
- Data URLs 于 url-loader
- Data URL可以用来表示一个文件
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RYMl48l-1626104766419)(…/img/1625267306520.jpg)]
- 打包静态资源也可以通过这种方式去实现
- 例如可以把png图片转换成Data URL 的形式
- 这种形式适合体积比较小的资源
- 小文件使用Date URLs,减少请求次数
- 大文件单独提取存放,提高加载速度
- yarn add url-load --dev
- 超过10kb文件单独提取存放
- 小于10kb文件转换为 Date URLs 嵌入代码中
- 对于超出10kb的文件,还是会自动去调用 file-load
module.exports = {
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 10kb
}
}
}
]
}
}
- Webpack 常用加载器分类
- 编译转换器
- 把加载到的资源模块转换为JavaScript代码,
- 如 css-loader 把css转换成以JS形式工作的模块
- 文件操作类
- 把加载到的资源文件拷贝到输出目录
- file-loader 处理图片,将文件的访问路径向外导出
- 代码检查类
- eslint-loader
- Webpack 与 ES 2015
- Webpack因为模块打包需要,所有会处理import和export
- yarn add babel-loader @babel/core @babel/preset-env --dev
- babel-loader 只是一个转换js的平台,基于这个平台通过不同的插件来转换代码中具体的一些特性
- Webpack 只是打包工具
- 加载器可以用来编译转换代码
- Webpack 加载资源的方式
- 遵循 ES Modules 标准的import 声明
- 遵循 CommonJS 标准的rquire 函数
- 如果是用require 导出 一个 ES Modules声明的模块,需要:rquire().default
- 遵循AMD标准的define函数和require函数
- *样式代码中的 @import 指令和url函数
- *HTML代码中图片标签的src属性
- Loader 加载的非JavaScript也会触发资源加载
- HTML 代码中图片标签的src属性
- yarn add html-loader --dev
- 处理js文件中引用了html资源
- import footer from ‘footer.html’
// <a href="1.png"></a> 处理a标签也引用了资源的情况 module.exports = { module: { rules: [ { test: /.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src', 'a:href'] // 默认html中图片标签的src属性才会触发资源加载 } } } ] } }
- Webpack 核心工作原理
- 一般是以一个js文件为打包入口,根据代码中import或require的语句,解析推断出这个文件的依赖的资源模块,然后递归的去解析这些资源模块,形成一个依赖树,递归这个依赖树,找到每个节点对应的资源文件,最后根据reles配置的加载器进行转换然后将结果放入bundle.js中
- Loader 机制是Webpack的核心
- 如果没有Loader就没有办法实现各种各样资源的加载,那么Webpack只能算是打包合并js代码的工具
- Webpack 开发一个Loader
- 开发一个markdown-loader
- yarn add marked --dev
- markdown-loader.js
const marked = require('marked') module.exports = source => { let html = marked(source) // console.log(html) // return JSON.stringify(html) 可以通过转字符串直接处理,也可以通过html-loader处理 // use: ['html-loader', './markdown-loader.js'] return html }
- Loader 负责资源文件从输入到输出的转换
- 对于同一个资源可以依次使用多个Loader
- Webpack 插件机制介绍
- 增强Webpack 自动化能力
- Load 专注实现资源模块加载
- Plugin 解决其它自动化工作
- 例如:在打包前清除dist目录
- 拷贝不需要打包的静态文件至输出目录
- 压缩输出代码
- Webpack 自动清除输出目录插件
- yarn add clean-webpack-plugin --dev
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
- Webpack 自动生成HTML插件(上)
- yarn add html-webpack-plugin --dev
- 自动生成使用bundle.js的HTML
- 解决项目中html还在开发项目中,不方便发布的问题
- 解决html路径引用容易出问题的情况
- 会在html中动态的把bundle.js的引用注入进去
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
// 用于生成index.html 文件
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
meta: {
viewport: 'with=device-width'
},
template: './src/index.html' // 配置原始模版
}),
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
}
- Webpack 自动生成HTML插件(中)
- html-webpack-plugin 选项
- Webpack 自动生成HTML插件(下)
- 同时输出多个页面文件
- 多配置几个HtmlWebpackPlugin 就解决了
- Webpack 插件使用总结
- 不需要模块化处理的静态资源文件 放在public文件夹下,可以用copy-webpack-plugin 处理
- 只会在上线前使用,如果需要打包的文件比较多,比较打,每次都去执行这个文件,性能消耗就比较大
- new CopyWebpackPlugin([]) 需要传入一个数组,需要处理的文件路径,可以是通配符
- Webpack 开发一个插件
- 相比于Loader,Plugin 拥有更宽的能力范围
- Loader 只在Webpack 加载模块的环节工作
- Plugin 可以涉及 Webpack的每一个环节
- Plugin 通过钩子机制实现
- 插件必须是一个函数或者是一个包含apply 方法的对象
- 通过在生命周期的钩子中挂载函数实现扩展
class MyPlugin { apply(compiler) { console.log('MyPlugin启动') compiler.hooks.emit.tap('MyPlugin', compilation => { for (const name in compilation.assets) { if (name.endsWith('.js')) { const contents = compilation.assets[name].source() const withoutComments = contents.replace(/\/\*\*+\*\//g, '') compilation.assets[name] = { source:() => withoutComments, size: () => withoutComments.length } } } }) } }
- Webpack 开发体验问题
- 以HTTP Server 运行
- 自动编译+自动刷新
- 提供 Sounce Map 支持 可以根据错误信息快熟定位源代码中的问题
- Webpack 自动编译
- 使用watch 的模式:监听文件变化,自动重新打包
- yarn webpack --watch 以监视模式运行
- Webpack 自动刷新浏览器
- browser-sync dist --files “**/*”
- 监控dist下文件变化就刷新浏览器
- 操作上比较麻烦,同时使用两个工具,开两个命令行
- 效率上降低,这个过程中,webpack会不断将文件写入磁盘,browser-sync再不断从磁盘中把它读出来。每次都需要进行两次磁盘读写操作
- Webpack Dev Server
- 官方提供的开发工具,提供用于开发的HTTP Server
- 集成 自动编译 和 自动刷新浏览器 等功能
- yarn add webpack-dev-server --dev
- yarn webpack-dev-server
- 为了提高打包效率,并没有将文件写入磁盘当中,将打包结果暂时存放在内存当中,而内部的http server也是从内存中把文件读出来,发送给浏览器。减少了磁盘操作,提升了效率
-
- yarn webpack-dev-server --open 直接在浏览器打开
- Webpack Dev Server 静态资源访问
- Dev Server 默认只会serve打包输出文件,只要是webpack打包输出的文件,都可以正常被访问
- 其它静态资源文件也需要serve,需要额外的告诉Webpack Dev Server
- contentBase 额外为开发服务器指定查找资源目录
- devServer: { contentBase: ‘./public’ }
- Webpack Dev Server 代理API
module.exports = {
devServer: {
contentBase: './public',
proxy: {
'/api': {
target: 'https"//api.xx.com',
pathRewrite: {'^/api': ''},
// 不要使用localhost:8000作为请求xx的主机名,
// 服务器会根据主机名判断这个请求属于哪个网站,
// 从未把请求指派当对应的网站,这个设置会以实际发生代理的这个请求为主机名
changeOrigin: true
}
}
}
}
- Source Map 简介
- 调试和报错都是基于运行代码 源代码地图
- Compoiled 通过 Source Map 逆向解析到源代码
- 如 jquery
- 在 jquery-3.4.1.min.js 文件最后一行添加注释: //# sourceMappingURL-jquery-3.4.1.min.map
- 如果浏览器解析到这行注释,在调试模式下,会自动去请求这个Source Map 文件,根据文件内容,逆向解析源代码,便于调试
- Source Map 解决了源代码与运行代码不一致所产生的问题
- Webpack 配置Source Map
- 配置webpack.config.js devtool: ‘source-map’
- Webpack 支持12种不同方式的Source Map
- 每种方式的效率和效果各不相同
- 效果最好的往往生成速度最慢
- 而速度最快的一般生成的Source Map往往效果不好
- Webpack eval 模式的 Source Map
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDHcc046-1626104766420)(…/img/1625296027733.jpg)]
- eval() 可以直接运行js代码,在一个临时的虚拟机中
- eval(‘console.log(123) //# sourceURL=./foo/bar.js’) 这段代码运行的环境就是./foo/bar.js
- 可以通过sourceURL改变这段代码运行环境的名称,还是运行在临时的虚拟机中,只是告诉了这段代码的临时文件路径
- devtool: ‘eval’ 会将每个模块的代码放在eval中执行,并在最后用sourceURL注释上路径,浏览器执行的时候就知道这段代码对应的原文件,只能定位错误出现的文件,不会生成source map ,和source map没有什么关系,构建速度最快
- Webpack devtool 模式对比(上)
- module.exports = [] 可以一次生成多个打包配置
- Webpack devtool 模式对比(下)
- eval- 是否使用eval 执行模块代码
- cheap-source Map 是否包含行处理信息
- module- 是否能够得到Loader处理之前的代码
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'eval-cheap-source-map',
'eval-cheap-module-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
},
plugins: [new HtmlWebpackPlugin({
filename: `${item}.html`
})]
}
})
- Webpack 选择Source Map 模式
- 开发模式
- cheap-module-eval-source-map
- 编码风格每行代码不超过80个字符,定位到行就够了
- 一般使用vue和react经过Loader转换过后的差异较大
- 首次打包速度慢无所谓,重写打包相对较快
- 生产环境
- none
- 因为Source Map会暴露源代码
- 调试是开发阶段的事情
- 如果实在有问题,可以选择 nosources-source-map 不会暴露源代码内容
- Webpack 自动刷新的问题
- 自动刷新功能有点积累,如果页面有填写输入框,每次修改刷新后输入框的值都消失了
- 自动刷新导致页面状态丢失
- 更好的办法是:在页面不刷新的前提下,模块也可以及时更新
- Webpack HMR 体验
- 模块热替换 Hot Module Replacement
- 应用运行过程中实时替换某个模块,而运行状态不改变
- 热替换只将修改的模块实时替换至应用中
- HMR 是webpack 中最强大的功能之一
- 极大程度的提高了开发者的工作效率
- Webpack 开启 HMR
- 集成在webpack-dev-server中
- webpack-dev-server --hot 开启,也可以在配置文件中开启
- 样式文件可以热更新,但是js文件不行
const webpack = require('webpack')
module.exports = {
devServer: {
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
// yarn webpack-dev-server --open
- webpack HMR 的疑问
- Webpack 中的HMR并不可以开箱即用
- Webpack 中的 HMR 需要手动处理模块热替换逻辑
- 为什么样式文件的热更新开箱即用?
- 样式文件在style-loader 里处理了热更新的情况
- 凭什么样式可以自动处理
- 因为样式文件的更新比较简单,只要把更新过后的css替换到页面,就能覆盖前面的样式,从而实现样式更新
- 而JavaScript代码是没有规律的,可能导出的是一个字符串,也可能是个函数,导出的成员使用也是各不相同的,webpack面对这些毫无规律的js模块,就不知道如何处理更新过后的模块,没有办法实现一个通用所有情况的模块替换方案
- 我的项目没有手动处理,JS照样可以热替换
- 框架下的开发,每个文件都是有规律的,例如react要求每个模块导出的是一个函数或者一个类
- 通过脚手架创建的项目内部都集成了HMR方案,所以不需要手动处理
- 总结:我们需要手动处理JS模块更新后的热替换
- Webpack 使用 HMR API
- 如果js模块的更新被手动处理,就不回去刷新页面了
import a from './a'
const b = a()
module.hot.accept('./a', () => {
console.log('a 模块更新了,需要这里处理一下热更新替换逻辑')
// 修改模块a不会导致页面刷新,而是会触发这里
})
- Webpack 处理JS 模块热替换
- 不是一个通用的方式,只是针对当前的模块
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KJ0xozSG-1626104766421)(…/img/1625302722081.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sf52OEek-1626104766422)(…/img/1625302787509.jpg)]
- Webpack 处理图片模块热替换
import bgImg from './b.png'
const img = new Image()
img.src = bgImg
if (module.hot) {
module.hot.accept('./b.png', () => {
img.src = bgImg
})
}
- WebPack HMR 注意事项
- 处理HMR 的代码报错会导致自动刷新,这样就不好排查错误
- devServer: { hotOnly: ture }, 是否被处理了热替换,浏览器都不会去刷新
- 没启动HMR的情况下,HMR API 报错
- if (module.hot) 先判断,再使用
- 如果关闭配置文件中的热更新,那么代码中多了一些与业务无关的代码(自己写的热更新API代码),这个会在编译后被移除,只剩下 if(false){},这个没有意义的判断在代码压缩后也会去掉,不会影响生产环境
- webpack 生产环境优化
- 生产环境注重运行效率
- 开发环境注重开发效率
- 为不同的工作环境创建不同的配置
- webpack 不同环境的配置文件
- 配置文件根据环境不同导出不同配置
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ic6oSi5-1626104766423)(…/img/1625307578973.jpg)]
- yarn webpack --env production
- 只适合中小型项目,如果项目大了,配置也会复杂起来
- 一个环境对于一个配置文件
- Webpack 不同环境的配置文件
- 大型项目,不同环境对应不同配置文件
- yarn add webpack-merge --dev 合并对象的包
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xsp7Aue-1626104766424)(…/img/1625308031835.jpg)]
- yarn webpack --config webpack.prod.js 或者 package.json > “scripts”: {“build”: “webpack --config webpack.prod.js” }
- webpack DefinePlugin
- 为代码注入全局成员
- 代码启动起来,会注入一个 prosess.env.NODE_ENV
- 其它模块根据这个判断全局的执行环境,判断是否打印日志等操作
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({ // 参数是一个对象,表示需要注入的全局变量,注入的是一个代码片段,而不是一个字符串
API_BASE_URL: '"https://api.xx.com"'
})
]
}
- Webpack 体验 Tree Shaking
- 摇掉 代码中未引用部分 生产模式下自动开启
- webpack 使用 Tree Shaking
- Tree Shaking 不是指某个配置选项,而是一组功能搭配使用后的优化效果
- prodution 模式下自动启用
- 在其它环境配置使用 Tree Shaking
module.exports = {
optimization: { // 专门配置优化选项的地方
usedExports: ture, // 负责标记 枯树叶
minimize: ture // 负责摇掉 它们
}
}
- webpack 合并模块
module.exports = {
optimization: { // 专门配置优化选项的地方
usedExports: ture, // 负责标记 枯树叶
concatenateModules: true, // 尽可能的将所有模块合并输出到一个函数中,既提升了运行效率,又减小了代码的体积
// Scope Hoisting 也就是作用域提升 webpack 3中添加的特性
// minimize: ture // 负责摇掉 它们
}
}
- webpack Tree Shaking 与 Babel
- 很多资料表示,使用Babel会导致Tree Shaking失效
- Tree Shaking 前提是 ES Module,由Webpack打包的代码必须使用ESM实现的模块化
- Babel 会把 ESM 转换成 CommonJS 模块化的规范
- 在最新版本的 Babel-loader 中自动帮我们关闭了ESM 转换的插件,标记了当前环境支持ESM
module.exports = {
module: {
rules: [
{
test: /\.jd$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {modules: 'commonjs'}] // 配置为commonjs,那么Tree Shaking又不生效了
// modules: false 确保 preset-env 中不会开启 ESM转换的插件
]
}
}
}
]
}
}
- webpack sideEffects
- webpack4新特性,可以通过配置的方式允许代码中是否有副作用
- 副作用:模块执行时除了导出成员之外所做的事情
- sideEffects 一般用于npm包标记是否有副作用
- 要配置两个地方,
- 如 components 下有多个模块,全部在 index.js 中导出,然后main.js 引用了index.js,导出了其中一个成员,有副作用会全量打包
- package.json > “sideEffects”: false 标识这个package所影响的项目都没有副作用,没有用到的模块,没有副作用后,都会被移除
- webpack.config.js > optimization: { sideEffects: true } 表示开启这个功能
- webpack sideEffects 注意
- 确保代码真的没有副作用,否则在webpack打包时就会误删掉这些有副作用的代码
- 例如一个模块中给 Number的原型加一个方法,但没有导出。在 index.js引用,就可以使用Number原型的方法,并不用导出
- 如果标识代码没有富作用,这个扩展操作就不会被打包
- 还有在代码中载入的css模块
- 解决方法:可以在package.json 中标记有副作用的模块; package.json > “sideEffects”: ["./src/extend.js", “*.css”]
- Webpack 代码分割
- Code Splitting 代码分包、代码分割
- 所有代码最终都被打包到一起
- 如果应用过于复杂,代码过多,bundle体积过大
- 然而在应用开始工作时,并不是每个模块在启动时都是必要的
- 模块打包是有必要的
- 同域并行请求限制。每次请求都会有一定的延迟
- 请求过多的Header 浪费带宽流量
- 有两种解决方案
- 多入口打包
- 动态导入 实现ESM 的动态加载的功能
- webpack 多入口打包
- 适用多页应用程序
- 一个页面对应一个打包入口
- 公共部分单独提取
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23QQO1Fy-1626104766424)(…/img/1625322017033.jpg)]
- webpack 提取公共模块
- 不同入口肯定会有公共模块
- webpack.config.js > optimization: { splitChunks:{ chunks: “all” } }
- 表示把所有公共模块都提取到一个文件(bundle)中
- Webpack 动态导入
- 按需加载:需要用到某个模块时,再加载这个模块
- 所有被动态导入的模块会被自动分包,提取到单独的bundle中
- 比起多入口更加灵活,可以通过代码的方式决定需不需要加载某个模块
- 在需要动态导入的地方使用ESM的 import().then()函数导入
- webpack 会自动处理分包和按需加载
- 如果使用的是单页应用开发框架,在项目中的路由映射组件就可以通过这种动态导入的方式按需加载
- webpack 魔法注释
- import(/* webpackChunkName: ‘album’ */ ‘./album/album’).then()
- 可以通过这种注释的方式给动态导入的模块打包后的bundle进行命名
- 相同的chunkName会被打包在一起
- Webpack MiniCssExtractPlugin
- 提取css到单个文件,可以实现css的按需加载
- yarn add mini-css-extract-plugin --dev
- 如果样式文件的体积不是很大,那提取到单个文件中效果可能适得其反
- 如果css 超过了150kb左右,才需要考虑是否要提取到单独的文件中
const MiniCssExtractPlugin = require('MiniCssExtractPlugin')
module.exports = {
moudule: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader' // 将样式通过style 标签注入页面
MiniCssExtractPlugin.loader, // 痛殴 style 标签的 link 属性引用css文件
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
- webpack OptimizeCssAssetsWebpackPlugin
- 压缩输出的css文件
- yarn add optimize-css-assets-webpack-plugin --dev
- 这个插件官方推荐配置在 minimizer中的原因是,只在需要压缩的时候才压缩,如果配置在plugins中,调试模式css文件也会被压缩
- 以普通模式打包,不开始压缩,也就不会调用
- yarn add terser-webpack-plugin --dev
- webpack自带的js压缩插件,如果配置了 minimizer 属性,原来的被覆盖,要重写
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2V6dr4l-1626104766425)(…/img/1625324133106.jpg)]
- webpack 输出文件名 Hash
- 一般会开启客户端静态资源缓存提升性能
- 缓存时间设置过短,效果不明显
- 如果设置的时间比较长,应用发生更新,重新部署过后,又没有办法及时更新到客户端
- 为了解决这个问题,推荐在生产模式下,文件名使用Hash
- 有三种hash
- hash 项目级别的hash,整个项目的文件hash一样,修改一个文件,重新打包,所有文件hash都变了
- chunkhash chunk级别hash 在打包过程中,同一入口的打包,模块hash都是相同的
-(动态导入也算多个chunk,如果其中一个变了,引用这个动态导入的文件也会被动改变,因为路径更新) - contenthash 文件级别的hash 根据输出文件的内容输出的hash值,不同文件不同hash值,也会路径更新
- 精确的定位到了文件级别的hash,最适合解决缓存问题
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d8qgERFy-1626104766425)(…/img/1625325323722.jpg)]
任务三:其它打包工具
- Rollup 概述
- Rollup 更为小巧,仅仅是一款ESM 打包器
- Rolluo 中并不支持类似HMR这种高级特性
- Rolluo并不是要与Webpack 全面竞争,只是为了提供一个充分利用ESM各项特性的高效打包器
- Rollup 快速上手
- yarn add rollup -dev
- yarn rollup ./src/index.js --format iife --file ./dist/bundle.js 参数为打包入口和打包模式(格式)以及打包输出目录
- rollup 打包后代码很简洁,基本等于自己手写的转换的iife代码,没有引用的代码会被去掉
- Tree Shaking 摇树最早出现在rollup,默认开启
- Rollup 配置文件
- rullup.config.js
- yarn rollup --config rullup.config.js
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
export default {
input: 'src/index.js',
output: {
file:'dist/bundle.js',
format: 'iife'
},
plugins: [
json(), // 将函数的结果放入
resolve()
]
}
- Rollup 使用插件
- 插件是Rollup 扩展的唯一方式
- yarn add rollup-plugin-json --dev 解析json 数据,可以直接在项目中 import {} from ‘./a.json’ 那数据
- Rollup 加载 NPM 模块
- yarn add rollup-plugin-node --dev 只有通过这种方式, Rollup才能通过模块名称导入第三方模块
- Rollup 默认只能处理ESM模块
- Rollup 加载CommonJS 模块
- yarn add rollup-plugin-commonjs --dev 这个插件解决Rollup不能使用CommonJS模块的问题
- Rollup 代码拆分
- 使用动态导入实现 import(’./logger’).then({ log } => { log(’’) })
- yarn rollup --config amd 不支持iife方式,因为Rollup打包过后比价简单,没有像webpack 一样的模块机制,只能使用amd标准
export default {
input: 'src/index.js', // ['src/a.js', 'src/b.js'] {a: 'src/a.js', b: 'src/b.js'}
output: {
// file:'dist/bundle.js',
// format: 'iife'
dir: 'dist', // 多文件都打包到dist目录下
format: 'amd' // 多文件打包和多入口打包都不能使用IIFE模式
}
}
- Rollup 多入口打包
- 支持多入口打包,公共文件也会自动提取到单独的bundle
- 多入口在 input 入口配置为对象或者数组即可
- amd 打包的文件,不能直接在html页面调用,要使用管理amd模块的库去调用
- require.js
- Rollup 选用原则
- 优点
- 输出的结果更加扁平,执行效率更高
- 自动移除为引用代码
- 打包结果依然完全可读,和手写的差不多
- 缺点
- 加载非ESM 的第三方模块比较复杂
- 模块最终都被打包到一个函数中,无法实现HMR
- 浏览器环境中,代码拆分功能依赖AMD库
- 开发应用
- 大量引用第三方模块
- 需要HMR 这样的功能提升开发体验
- 应用大了要分包
- rollup 在满足这些上有欠缺
- 开发一个框架或者类库
- 优点很明显,缺点不太重要了,开发框架或库,很少依赖第三方模块
- 大多数知名框架/库都在使用Rollup作为模块打包器,如vue 和react
- webpack 大而全,Rollup小而美
- 应用开发使用webpack
- 库/框架开发使用Rollup
- 随着webpack这几年的发展,rollup的很多优势被慢慢磨平了,如扁平化输出,webpack可以通过插件实现
- Parcel 零配置的前端应用打包器
- yarn add parcel-bundler --dev
- parcel 和webpack 一样,可以用任意文件做打包入口,但是官方推荐html,理由是html是应用运行在浏览器中的入口
- yarn parcel src/index.html
- 支持热更新 if(module.hot) { module.hot.accept(() => {}) } 与webpack不一样,只支持一个参数,如果当前模块,或者依赖模块修改触发
- 支持自动安装依赖,不用重启调试,代码保存过后自动下载依赖
- 同样支持加载其它类型的资源模块,加载任意模块的资源,都是0配置
- 支持动态导入,自动拆分代码
- yarn parcel build src/index.html 生产模式打包
- 相同体积的项目打包,percel会比webpack快很多,percel内部使用多进程同时工作,充分发挥多核cpu性能,webpack 可以通过happypack的插件实现这一点
- 2017年发布,出现的原因就是当时wbpack 使用上过于繁琐,官方文档也不是很清晰明了
- 完全零配置,对项目没有任何侵入
- 没有普及的原因:webpack 有更好的生态,随着这两年发展,webpack越来越好用
任务四:规范化标准
- 规范化介绍
- 为什么要有规范化标准
- 软件开发需要多人协同
- 不同开发者具有不同的编码习惯和喜好
- 不同的喜好增加项目维护成本
- 每个项目或者团队需要明确统一的标准
- 那些需要规范化标准
- 代码、文档、甚至是提交日志
- 开发过程中人为编写的产出
- 代码标准化规范最为重要
- 实施规范化的方法
- 编码前人为的标准约定
- 通过工具实现Lint
- 常见的规范化实现方式
- ESLint 工具使用
- 定制ESLint校验规则
- ESLint对TypeScript的支持
- ESLint 结合自动化工具或者webpack
- 基于ESLint的衍生工具
- Stylelint 工具的使用
- ESLint 介绍
- 最为主流的JavaScript Lint工具 监测JS代码质量
- ESLint 很容易统一开发者的编码风格
- ESLint 安装
- 安装ESLint模块为开发依赖
- 通过CLI命令验证安装结果
- yarn add eslint --dev 或 npm install eslint --save-dev
- ESLint 快速上手
- yarn eslint --init
- 第一个选项:出现以下三种检查选项
- 语法错误
- 语法错误+问题代码(如变量声明没有使用)
- 语法错误+问题代码+编码风格
- 代码存在问题,eslint是没有办法去找到问题代码和编码风格问题的
- yarn eslint 文件名 --fix 自动修复编码风格绝大多数问题
- ESLint 配置文件解析
- 支持同时配置多个环境,可以同时配置browser和node,那么这两个环境的全局成员就都可以使用
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bw7Vvmmh-1626104766426)(…/img/1625384928864.jpg)]
- parserOptions 配置监测语法是否可用
- rules 配置规则是否可用
- globals 配置支持的全局变量,比如jquery
- 配置注释
- 将配置的通过注释的方式写在脚本文件中
- 直接在某行代码后 + // eslint-disable-line 具体规则 如果不加具体规则,就所有的规则都不检测
- ESLint 结合自动化工具(Gulp)
- 集成之后,确保ESLint每次都会工作
- 与项目统一,管理更加方便
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGX2wTg6-1626104766427)(…/img/1625385679827.jpg)]
- mac command + k + 0 快速折叠代码
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UIXwpRDl-1626104766427)(…/img/1625385986619.jpg)]
- ESLink 结合 Webpack
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrPr5jJY-1626104766428)(…/img/1625386403254.jpg)]
- eslint 在webpack中的两种配置方式
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9MXGrvo-1626104766428)(…/img/1625386698044.jpg)]
- ESLint 结合 Webpack 后续配置
- yarn add eslint-plugin-react --dev 针对react编写的
- 两种方式
- 在.eslintrc.js 中配置: plugins: [‘react’] rules: [‘react/jsx-uses-react’: 2, ‘react/jsx-uses-vars’: 2]
- .eslintrc.js 》 extends: [‘plugin:react/recommended’]
-
现代化项目集成ESLint
-
ESLint 检查 TypeScript
-
Stylelint认识
- 提供默认的代码监测规则
- 提供CLI工具,快速调用
- 通过插件支持Sass Less PostCSS
- 支持Gulp或Webpack集成
- 使用
- yarn add stylelint --dev
- .stylelintrc.js
- yarn add stylelint-config-standard
module.exports = { extends: ["stylelint-config-standard", "stylelint-config-sass-guidelines"] }
- yarn add stylelint-config-sass-guidelines --dev 检查sass代码的插件
- Prettier 的使用
- 前端代码格式化工具
- yarn add prettier --dev
- yarn prettier 文件名 --write 直接将格式化的代码输出到命令行,加上–write才会覆盖原文件
- yarn prettier . --write 格式化所有代码
- Git Hooks 介绍
- 代码提交至仓库之前未执行lint工作
- Git Hook 也称之为git钩子,每个钩子都对应一个任务
- 通过shell脚本可以编写钩子任务触发时要具体执行的操作
- .git > pre-commit.sample git commit 提交本地仓库前执行的钩子
- ESLint 结合 Git Hooks
- 很多前端开发者并不擅长使用shell
- Husky 可以实现Git Hooks 的使用需求,在不编写shell脚本情况下,也能使用git钩子
- yarn add husky --dev
- package.json > “husky”: { “hooks”: { “pre-commit”: “npm run precommit” } }
- 如果接下来想自动格式化,然后把代码推入暂存区就无能为力了
- yarn add lint-staged --dev
- package.json > “lint-staged”:{ “*.js”: [“eslint”, “git add”] } “scripts”: { “precommit”: “lint-staged” }
- Husky + lint-staged 配合使用
任务五:webpack 源码
- 内容概述
- 将webpack打包任务拆解成不同的环节,掌握每个环节所做的工作
- 实现一个自己的打包器,不同模块的加载,以及不同loader和插件的使用
- 打包后文件分析
- webpack 4 打包后文件解析,
- webpack5 打包后,模块不再是传入,而是直接在自执行函数中声明一个对象储存
- 如果入口模块没有导出,直接就在最下面调用执行了,不会储存在开始的模块对象中
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0u33GDzr-1626104766429)(…/img/1625466911614.jpg)]
- webpack_require 方法,它的核心作用是返回模块的exports
-
单文件打包后源码调试
-
函数功能说明(CommonJS模块打包)
-
CommonJS模块打包(CommonJS模块打包 和 ESM 模块打包)
- 对比 CommonJS模块 和ESM 模块 webpack打包后文件的区别
- webpack 默认就支持CommonJS,没有对模块做任何处理
- esModule 模块打包
- 主入口是 ESM,调用了commonjs模块和ESM打包后输出
// webpack 5 打包输出
(() => { // webpackBootstrap
var __webpack_modules__ = ({
/***/ "./src/login.js": // ESM 模块
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { // module, module.exports, __webpack_require__
"use strict";
__webpack_require__.r(__webpack_exports__); // 先给exports 标记这是一个 _esModule
__webpack_require__.d(__webpack_exports__, { // 把导出数据挂载到exports 上
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
"age": () => (/* binding */ age)
});
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ('登陆');
console.log(22)
const age = 22
}),
/***/ "./src/msg.js": ((module) => { module.exports = { msg: '今天红烧肉'} }) // commonjs模块,基本不做处理
});
/************************************************************************/
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) { // 通过模块ID (一般是文件名)返回模块导出结果 (exports)
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function 通过模块ID调用模块
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
/************************************************************************/
/* webpack/runtime/compat get default export */
(() => {
// getDefaultExport function for compatibility with non-harmony modules 用于与非和谐模块兼容的 getDefaultExport 函数
// 把ESM 默认值处理往comoonjs方向上统一
__webpack_require__.n = (module) => { // module 》esm 的导出对象
var getter = module && module.__esModule ?
() => (module['default']) :
() => (module);
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();
(() => {
// 给模块导出数据挂载到exports,通过defineProperty get使导出结果修改无效
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
/* 判断对象是否有该属性 */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
// define __esModule on exports 给 exports加上 __esModule标记
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode.
(() => {
"use strict";
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
__webpack_require__.r(__webpack_exports__); // 打标记
__webpack_require__.d(__webpack_exports__, { // 把导出数据挂载到exports 上
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
var _login_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./login.js */ "./src/login.js"); // 调用模块拿到导出数据
var _msg_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./msg.js */ "./src/msg.js");
var _msg_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_msg_js__WEBPACK_IMPORTED_MODULE_1__); // 特殊处理ESM 模块的默认值
console.log('index.js 内容', _login_js__WEBPACK_IMPORTED_MODULE_0__.default, _msg_js__WEBPACK_IMPORTED_MODULE_1__.msg)
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ('入口文件导出内容');
})();
})();
-
功能函数手写实现01
-
功能函数手写实现02
-
懒加载实现流程梳理
- import() 可以实现指定模块的懒加载操作
- 当前懒加载核心原理就是 jsonp
- t 方法可以针对于内容进行不同的处理 (处理方式取决于传入的数值 8 6 7 3 2 1)
-
t 方法分析及实现
-
单文件懒加载源码分析1
-
单文件懒加载源码分析2
-
单文件懒加载手写实现
-
webpack 与 tapable
- 专注于自定义事件的触发和处理
- webpack 编译流程
- 配置初始化
- 内容编译
- 输出编译后内容
- 事件驱动型事件流工作机制
- 负责编译的complier
- 负责创建bundles的compilation
- tapable 本身是一个独立的库
- 实例化hook注册事件监听
- 通过hook触发事件监听
- 执行懒编译生成的可执行代码
- hook 本质是tapable实例对象
- hook 执行机制可分为同步和异步(异步存在并行和串行两种机制)
- hook执行特点
- hook:普通钩子,监听器之间互相独立不干挠
- BailHook:熔断钩子,某个监听返回非undefined 时后续不执行
- WaterfallHook:瀑布钩子,上一个监听返回值可传递至下一个
- LoopHook:循环钩子,如果当前未返回false 则一直执行
- tapable 库同步钩子
- SynckHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
- tapable 库异步串行钩子
- AsyncSeriesHook
- AsyncSeriesHailHook
- AsyncSeriesWaterfallHook
- tapable 库异步并行钩子
- AsyncParelleHook
- AsyncParelleBailHook
-
同步钩子使用及调试
-
异步钩子的使用
-
SyncHook 源码调试1
-
syncHook 源码调试2
-
手写 SyncHook1
-
手写 SyncHook2
-
AsyncParallelHook源码分析
-
AsyncParallelHook实现
-
定位webpack 打包入口
- 01 node_modules/.bin/webpack.cmd cmd 文件核心的作用就组装了 node *****/webpack/bin/webpack.js
- 02 webpack.js 中核心的操作就是require了 node_modules/webpack-cli/bin/cli.js
- 03 cli.j
- 当前文件一般有两个操作,处理参数,将参数交给不同的逻辑(分发业务)
- options
- complier
- complier.run
- 编译主流程调试
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oWtlNHCq-1626104766430)(…/img/1625839062839.jpg)]
- 手写webpack实现
- lgpack
- lib
- webpack.js
- npm init -y
- “main”: “./lib/webpack.js”
- lib
- webpack.js
- 实例化compiler 对象
- 初始化 NodeEnvironmentPlugin(让compiler具备文件读写能力)
- 挂载所有 plugins 插件 至compiler 对象上
- 挂载所有webpack内置的插件(入口)
- 返回 compiler 对象即可
- npm i tapable@1 -D
- EntryOptionPlugin 执行分析
- 入口模块
- EntryOptionPlugin 流程手写
- WebpackOptionsApply > 290行
-
run 方法分析及实现
-
compile 分析及实现
-
newCompilationParams 方法调用,返回params, normalModuleFactory
-
上述操作是为了获取 params
-
接着调用了beforeComlile 钩子监听,在它的回调中又触发了compile 监听
-
调用 newCompilation 方法,传入了上面 params,返回了一个 compilation
-
调用了一个 createNewCompilation (Compilation.js)
-
上述操作完成之后就可以触发 make 钩子监听
-
make 前流程回顾
-
步骤
-
实例化 compiler 对象 (它会贯穿整个webpack工作流程)
-
由 compiler 调用 run方法
-
compiler 实例化操作
-
compiler 继承 tapable, 因此它具备钩子的操作能力 (监听事件,触发事件,webpack 是一个事件流)
-
在实例化 compiler 对象之后就往它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作就让它具备流文件读写的能力(我们在模拟时采用的是 node 自带的 fs)
-
具备了 fs 操作能力之后又将 plugins 中插件都挂载到了 compiler 对象身上
-
new WebpackOptionsApply().process(options, compiler)
- 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin(290行) 处理了入口模块 id
05. 在实例化 compiler 时候只是监听了 make 钩子 (EntryOptionPlugin > SingleEntryPlugin)
1. 在 SingleEntryPlugin 模块的 apply 方法中有两个钩子监听
2. 其中 compilation 钩子就是让compilation 具备了利用 NormalModuleFactory 工厂创建一个普通模块的能力
3. 因为它就是利用一个自己创建的模块来加载需要被打包的模块
4. 其中 make 钩子在 compiler.run 的时候会被触发,走到这里意味着某个模块执行打包之前的所有准备工作就完成了
5. addEntry 方法调用
- run 方法执行 (当前想看的是什么时候出发了 make 钩子)
- run 方法里就是一堆钩子按着顺序触发 (beforeRun run compile)
- compiler 方法执行
- 准备参数(其中 NormalModuleFactory 是我们后续用于创建模块的)
- 触发 beforeCompile
- 将第一步的参数传递给一个函数,开发创建一个 compilation(newCompilation)
- 在调用 newCompilation 的内部
- 调用了 createCompilation
- 触发了 this.compilation 钩子和 compilation 钩子的监听
03. 当创建了 compilation 对象之后就触发了 make 钩子
04. 当我们触发 make 钩子监听的时候,将 compilation 对象传递了过去
-
总结
-
实例化 compiler
-
调用 compiler 方法
-
newCompilation
-
实例化一个 compilation 对象 (它和 compiler 有关系)
-
触发 make 监听
-
addEntry 方法 (这个时候就带着 context name entry 一堆的东西) 奔着编译去了
-
addEntry 流程分析1
-
make 钩子在被触发的时候,接受到了 compilation 对象实现, 它的身上挂载了很多内容
-
从 compilation 当中解构了三个值
- entry: 当前需要被打包的模块的相对路径
- name: main
- context: 当前项目的跟路径
-
dep 是对当前的入口模块中的依赖关系进行处理
-
调用了 addEntry 方法
-
在 compilation 实例的身上有一个 addEntry 方法,然后内部调用了 _addmoduleChain 方法,去处理依赖
-
在 compilation 当中我们可以通过 NormalModuleFactory 工厂来创建一个普通模块对象
-
在 webpack 内部默认启动一个100 并发量的打包操作(this.semaphore.acquire),当前我们看到的是 normalModule.create()
-
在(moduleFactory.create中) beforeResolve 里面会触发一个 factory 钩子监听 【这个部分的操作其实是处理loader】
-
上述操作完成之后获取到了一个函数存在 factory 里,然后对它进行了调用
-
在这个函数调用里又触发了一个 resolver 的钩子(处理 loader的,拿到了 resolver 方法就意味者所有的loader处理完毕)
-
调用 resolver() 方法之后,就会进入到 afterResolve 这个钩子里,然后就会触发 new NormalModule
-
addEntry 流程分析2
-
addEntry 初始化
-
_addModuleChain实现
-
buildModule 实现
-
build及parse 实现
- yarn add babylon --dev
- 处理模块依赖01
- astexplorer.net 这个网站可以看js代码的语法树
-
处理模块依赖02
-
抽离createModule方法
-
编译依赖模块
- yarn add neo-async --dev
-
chunk流程分析及实现
-
生成chunk代码
- yarn add ejs --dev
- 生成打包文件
- yarn add mkdirp --dev
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttBEKBlf-1626104766431)(…/img/1626064121728.jpg)]