# Webpack 原理
# 准备
- 新建 bin 目录,将打包工具主程序放入其中
主程序的顶部当有:
#!/usr/bin/env node标识,指定程序的执行环境为 node
#!/usr/bin/env node
console.log('测试')
- 在 package.json 中配置 bin 脚本
"bin": {
"pack": "./bin/pack.js"
}
- 通过 npm link 链接到全局包中,供本地测试使用
# webpack_require函数
# 输出 bundle.js 过程分析
- 配置简单的 entry 和 output 参数
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
mode: 'development'
}
- 读取 index.js 文件的内容,通过
@babel/parser进行语法分析,输出 AST(抽象语法树); 通过@babel/traverse遍历操作 AST,找到p.node.callee.name === 'require'的节点, 修改 AST 节点属性p.node.callee.name = '__webpack_require__', 修改路径,并避免 windows 出现反斜杠:\p.node.arguments[0].value, 通过@babel/generator把 AST 映射回 JS 代码。
depAnalyse(modulePath) {
// 读取index.js文件的内容
let source = this.getSource(modulePath)
let ast = parser.parse(source)
// 存储当前模块的所有依赖
let dependencies = []
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name === 'require') {
// 修改require
p.node.callee.name = '__webpack_require__'
// 修改路径,并避免windows出现反斜杠:\
let oldVal = p.node.arguments[0].value
oldVal = `./${path.join('src', oldVal)}`
p.node.arguments[0].value = oldVal.replace(/\\+/g, '/')
dependencies.push(p.node.arguments[0].value)
}
}
})
// 解析新的ast
let sourceCode = generator(ast).code
// 构建modules对象
let modulePathRelative = `./${path.relative(this.root, modulePath)}`.replace(/\\+/g, '/')
this.modules[modulePathRelative] = sourceCode
// 递归加载所有依赖
dependencies.forEach(item => {
this.depAnalyse(path.resolve(this.root, item))
})
}
- 使用 ejs 语法编写 output.ejs 的文件,渲染 modules 对象并生成类似使用 webpack 构建输出的 bundle.js 文件。
emitFile() {
let template = this.getSource(path.join(__dirname, '../template/output.ejs'))
let reuslt = ejs.render(template, {
entry: this.entry,
modules: this.modules
})
let output = this.config.output
let outputPath = path.join(output.path, output.filename)
fs.writeFileSync(outputPath, reuslt)
}
# loader
加载顺序:pre 前置,post 后置,遵循:pre > inline > normal > post (前置 > 行内 > 普通 > 后置)
{
test: /\.js$/,
use: './loaders/loader.js',
enforce: 'pre'
}
# tapable 简介
在 webpack 内部实现事件流机制的核心在于tapable,有了它就可以通过事件流的形式, 将各个插件串联起来,tapable 类似于 node 中的 events 库,核心原理也是发布订阅模式
# 在 itheima-pack 中添加 loader 的功能
通过配置 loader 和手写 loader 可以发现,其实 webpack 能支持 loader,主要步骤如下:
- 读取 webpack.config.js 配置文件的 module.rules 配置项,进行倒序迭代(rules 的每项匹配规则按倒序匹配)
- 根据正则匹配到对应的文件类型,同时再批量导入 loader 函数
- 倒序迭代调用所有 loader 函数(loader 的加载顺序从右到左,也是倒叙)
- 最后返回处理后的代码
在实现 itheima-pack 的 loader 功能时,同样也可以在加载每个模块时,根据 rules 的正则来匹配是否满足条件,如果满足条件则加载对应的 loader 函数并迭代调用
depAnalyse()方法中获取到源码后,读取 loader:
handleModule(modulePath, source) {
// 读取rules规则,倒序遍历
let rules = this.config.module.rules
let len = rules.length - 1
const handleLoader = (use, obj) => {
let loaderPath = path.join(this.root, use)
let loader = require(loaderPath)
return loader.call(obj, source)
}
for (let i = len; i >= 0; i--) {
const {use, test} = rules[i]
// 获取每条规则,与当前modulePath进行匹配
if (test.test(modulePath)) {
if (Array.isArray(use)) {
for (let j = use.length - 1; j >= 0; j--) {
source = handleLoader(use[j])
}
}
else if (typeof use === 'string') {
source = handleLoader(use)
}
else if (use instanceof Object) {
source = handleLoader(use.loader, { query: use.options })
}
}
}
// console.log(source)
return source
}
# webpack 插件的组成 (opens new window)
- 一个 JavaScript 命名函数
- 在插件函数的 prototype 上定义一个 apply 方法
- 指定一个绑定到 webpack 自身的事件钩子
- 处理 webpack 内部实例的特定数据
- 功能完成后调用 webpack 提供的回调
# plugin 生命周期钩子函数
# Compiler 和 Compilation 的区别
- compiler 对象表示不变的 webpack 环境,是针对 webpack 的
- compilation 对象针对的是随时可变的项目文件,只要文件有改动,compilation 就会被重新创建
# cheerio
cheerio 是 nodejs 的抓取页面模块,为服务器特别定制的,快速、灵活、实施的 jQuery 核心实现。 适合各种 Web 爬虫程序