五. webpack原理
- webpack就是基于事件流的编程范例,一系列的插件运行
webpck整体流程
一句话说:从入口文件开始,递归地构建一个依赖关系图,包含了应用程序需要的每个模块,将所有这些模块打包成一个或多个文件
简单说
- 初始化参数:确定行为,
- 加载插件:便于后续的监听,针对特定的事件执行特定的逻辑
- 编译模块:从入口文件开始递归编译所有依赖的模块,利用相应的loader对其进行转换,
- 生成文件:根据入口和模块间的依赖关系,生成chunk
- 输出文件:根据output及其他插件确定输出路径和文件名,输出到对应的文件系统中
详细说
webpack 的事件节点,较多,这里只说了一些关键的事件节点和做的事情
-
初始化参数:读取
shell
语句和package
配置文件的参数- 对参数的具体分析靠
webpack-cli
,命令调用webpack做相应的编译构建 【1】对参数使用错误进行报错 【2】对非构建命令执行相应的操作(如npm i
,npm init
等)
- 对参数的具体分析靠
-
实例化Compiler
- 利用参数初始化
Compiler
,来广播和监听事件
- 利用参数初始化
-
加载插件
- 调用
complier
的apply
方法,相当于注册各种插件的回调方法 - 内部插件挂载到
compiler
,便于后续的监听,针对特定的事件执行特定的逻辑
- 调用
-
options参数,转化成插件
- 主要利用
webpackOptionsApply
,将webpack&package配置文件和命令行里的中的options
参数,转化成插件 - 例如: externals -> ExtrenalsPlugin
- 主要利用
-
对entry的单多入口处理
itemToPlugin
方法判断单入口还是多入口文件,进而使用MultiEntryPlugin
或SingleEntryPlugin
-
开始编译(run)
- 调用compiler 的run方法开始,触发compile,创建compilation对象
-
模块构建(make)
- 执行
addEntry
方法,将entry加入到构建列表中,从 entry开始,对依赖模块进行build build-module
,构建 某个模块,使用对应的 Loader 去转换一个模块- 一个模块转换完后,使用
acorn
解析, 生成对应的抽象语法树(AST)
,便于后面对代码的分析
- 执行
-
chunk生成(seal阶段)
- 先将
entry
对应的module
都生成一个新的chunk
, - 遍历
module
的依赖列表,依赖的module也加入到chunk
中 - 如果依赖的
module
是动态引入的模块,那么根据module
创建一个新的chunk
,据徐遍历依赖, - 重复直到得到所有的
chunks
- 先将
-
输出 emit
- 获取输出的内容,将输出的内容输出到对应的磁盘中去
重点说
tapable
- 核心对象
compiler
和compilation
都继承于tapable
- tapable 类似node里
EventEmitter
发布订阅模块, 控制钩子函数的发布与订阅 - 众多插件监听 compiler 和compilation 上关键的事件节点
compile
- 继承于
tapable
对象, 包含了webpack环境的所偶的配置信息, - 在webpack启东时被实例化,全局唯一
- 可以广播和监听webpack事件
compilation
- 继承于
tapable
对象,包含了当前的模块资源、编译生成资源、变化的文件 - 每当文件变化,新的compilation被创建,
AST
- 抽象语法树
- 以树状的形式表现编程语言的语法结构
- 用处
- 比如在vsCode中的代码风格,语法的检查,错误提示,格式化,自动补全
- 代码压缩
- babel,TS,JSX等的转译
- 模版引擎
举例
const html = '<div><span>tom</span></div>'
const ast = {
tag: 'div',
children: [
{
tag: 'span'
},
],
}
具体看在线demo
模块化
随着前端发展,越来越复杂,模块化的必要性大大增加,
-
<script>
标签方式是存在问题的- 全局作用域下容易造成变量冲突
- 文件只能按照
<script>
的书写顺序进行加载
-
CommonJs(CJS)
- 使用require关键字,
- 可动态导入
- NodeJS使用
- 同步的加载方式不适合使用在浏览器异步资源中
-
AMD
- 借鉴了commonjs,浏览器使用
- define 定义模块,require使用模块
-
ESModule(ESM)
- 静态导入,编译时就可以确定模块的依赖关系,便于静态分析,
- treeShaking就是在此基础上实现的
- 浏览器支持不够友好
webpack简单实现
流程图
简单实现流程- 通过babylon对代码转换成AST,
- 获取文件的依赖,
- 通过babel-core将AST重新生成源码
- 以上三者记录在模块列表
modules
中 - 深度遍历依赖,都push进模块列表中,
- 遍历模块列表,利用字符串拼接写入指定文件目录
webpack打包文件代码大致格式
// 一个立即执行函数
(function(modules) {
...
})({
modules
})
// modules
{
"./src/index.js": (function (require, module, exports) {
...
}),
"./src/tools.js": (function (require, module, exports) {
...
}),
"./src/two.js": (function (require, module, exports) {
...
})
}
核心代码
// ./lib/index.js webpack入口执行文件
const Compiler = require('./compiler')
const options = require('../webpack.config.js')
new Compiler(options).run();
// ./lib/compiler.js 核心compiler类,负责编译和输出
const { getAST, getDepencies, transform } = require('./parser.js')
const path = require('path')
const fs = require('fs');
module.exports = class Compiler {
constructor(options) {
const { entry, output } = options
this.entry = entry;
this.output = output;
this.modules = []; // 生成的模块列表
}
run() { // 开始构建
const entryModule = this.buildModule(this.entry, true);
this.modules.push(entryModule)
this.modules.forEach((_module) => {// 遍历处理依赖
_module.dependencies.forEach((dependency)=> {
this.modules.push(this.buildModule(dependency)); //依赖也执行模块构建
})
})
this.emitFiles();
}
buildModule(filename, isEntry) { //模块构建
let ast;
if(isEntry) { // 文件是入口模块
ast = getAST(filename); // 源代码转换成AST树
} else {
// 将相对路径转化为绝对路径
const absolutePath = path.join(process.cwd(), './src', filename)
ast = getAST(absolutePath);
}
return {
filename, // 文件名
dependencies: getDepencies(ast), // 文件依赖
source: transform(ast), // 重新生成ES5源码
}
}
emitFiles() { // 输出
const outputPath = path.join(this.output.path, this.output.filename)
let modules = '';
this.modules.forEach((_module)=>{
modules += `'${_module.filename}': function(require, module, exports) {${_module.source}},`
})
// 实现类似webpack require函数来解析依赖,每个模块进行包裹
const bundle = `(function(modules) {
function require(filename) {
var fn = modules[filename]
var module = {exports : {}}
fn(require, module, module.exports)
return module.exports;
}
require('${this.entry}')
})({${modules}})`;
fs.writeFileSync(outputPath, bundle, 'utf-8')
}
}
// ./lib/parser.js 解析器,分析依赖,ast的获取与转化方法
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('babel-core');
module.exports = {
getAST: (path) => { // 通过babylon对代码转换成AST,获取AST树
const source = fs.readFileSync(path, 'utf-8')
return babylon.parse(source, {
sourceType: 'module'
})
},
getDepencies: (ast) => { // 分析依赖,
const depencies = [];
traverse(ast, {
ImportDeclaration: ({node}) => {
depencies.push(node.source.value)
}
})
return depencies
},
transform: (ast) => { // 通过babel-core将AST重新生成源码
const { code } = transformFromAst(ast, null, {
presets: ['env']
} );
return code
}
}
六. loader
1. 简介
- loader是一个导出为函数的javascript的模块,它就像一个纯函数,输入不变输出就不变, 举例如下:
module.exports = function (source){
return source;
}
- 多个loader的执行: 串行执行, 从右往左
举例
{
test: /.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
],
},
2. 运行环境
- loader有单独的运行环境
loader-runner
,允许在不安装webpack情况下运行loaders loader-runner作用 - 作为webpack的依赖,在webpack中使用他执行loader
- 进行loader的开发和调试
举例
import { runnerLoaders } from "loader-runner"
runner({
resource: '/path/index.js?query', // 要解析的文件
loaders: ['/path/loader.js?query',] // 解析使用的loader
context: { // 上下文参数,
minimize: true // 压缩参数
}
readResource: fs.readFile.bind(fs) // 查询resource的方式
}, function(err, res) { // err 错误信息,和执行结果
})
3. 参数获取
- 通过
loader-utils
插件 的geOptions
方法获取
举例
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const { name } = loaderUtils.getOptions(this)
}
4. 异常处理
- loader 通过 throw 抛出
- 通过 this.callback 传递错误(同步loader)
举例
module.exports = function (source) {
throw new Error('Error')
// 一般情况下使用this.callback 返回,第一个参数是error对象
this.callback(new Error('Error'), source)
// 回传多个值,也可使用this.callback返回
this.callback(new Error('Error'), source, 2, 3, 4)
}
5. 异步处理
- 通过
this.async()
来返回一个异步函数 - 第一个参数是Error, 第二个参数是处理的结果
举例
module.exports = function (source) {
const callback = this.async();
fs.readFile(// 异步读取文件
path.join(__dirname, './async.txt'),
'utf-8',
(err, res) => {
callback(null, res);
}));
}
6. 缓存的使用
-
webpack 默认开启缓存
-
可使用 this.cacheable(false) 关掉缓存
-
缓存条件:
【1】loader的结果在相同的输入下有确定的输出
【2】有依赖的loader 无法使用缓存
7. 文件输出
- 利用
this.emitFile
进行文件写入(它也是file-loader
实现的关键) loaderUtils.interpolateName
用占位符或一个正则表达式转换成一个文件名
举例
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const url = loaderUtils.interpolateName(
this, // 上下文执行环境,
"[hash].[ext]", // 占位符
{
source
}
)
this.emitFile(url, source)
return source
}
七. 插件(plugin)
1. 简介
- loader不能做的事就要plugin来做啦,
- 我们知道在webpack配置
plugins
数组里new Plugin()
来使用插件, - 所以我们编写一个插件就是一个Class ,实例化它,
- 插件都会有
apply
方法接收compiler
对象,plugin
可以监听compiler的hooks,在特定的时间去执行特定的逻辑
举例
module.exports = class SpecialPlugin {
apply(compiler) {
// 在某个hooks阶段
compiler.hooks.done.tap('SpecialPlugin',(compilation, callback) => {
console.log('hello ,world');
})
}
}
2. 运行环境
- 插件没有像loader那样独立运行环境
- 只能在webpack里面运行
- 如果编写一个插件测试,只能依赖 webapck, webpack-cli,webpack.config.js 把插件放入plugin中测试
举例
const path = require('path')
const SpecialPlugin = require('./plugins/specialPlugin')
module.exports = {
entry: './src/index.js'
output: {
path: path.join(__dirname, './dist')
filename: 'main.js'
}
plugins: [
new SpecialPlugin({
name: 'SpecialPlugin'
});
]
}
3. 获取参数
- 直接使用构造函数 获取参数,
举例
module.exports = class MyPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log('options', this.options);
}
}
4. 异常处理
- throw 抛出
- 通过compilation 对象的warnings 和 errors接收
举例
module.exports = class SpecialPlugin {
apply(compiler) {
compiler.hooks.done.tap('SpecialPlugin',
(compilation, callback) => {
throw new Error('Error)
compilation.warnings.push("warning")
compilation.errors.push('error')
})
}
}
5. 文件写入
- 监听
compiler
的emit
文件生成阶段, 获取compileation对象,把内容赋给assets对象 - 在最终emit生成的时候,会读取assets对象,输出到设定好的文件位置
- 生成文件时也需要
webpack-sources
的配合,比如下面的输出一段代码到文件中去,就可以使用RawSource
举例
// 简单的插件,输出一段代码到文件中去
const RawSource = require('webpack-sources').RawSource;
module.exports = class SpecialPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const { name } = this.options;
// 监听compiler的emit的hooks
compiler.hooks.emit.tap('SpecialPlugin', (compilation, callback) => {
// 将想要设置的内容,赋值给assets
compilation.assets[name] = new RawSource('content')
})
}
}
总结
webpack基本告一段落
- 核心概念
- 其他常用配置
- 优化手段
- 配置总结
- webpack整体流程和简单实现
- loader相关
- 插件相关