-
2021/01/09 更新
-
2021/07/27 更新
导航
[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[react] Hooks
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI
[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend
[源码-vue06] Vue.nextTick 和 vm.$nextTick
前置知识
一些单词
compiler:编译
npm link
- (1) 先把需要link的包在根目录执行:----------------------------------
npm link
- 通过 npm link 可以把包link到全局
- 该包需要有
bin/wpack.js
- 在package.json中设置
bin: { wpack: '路径'}
- (2) 在需要使用该包的项目中的根目录,执行命令:-------------------
npm link wpack
- 则会把wpack包安装到node_modules中
- (3) 验证
- 在使用到wpack包的项目中,执行命令:-------------------------
npx wpack
- 在使用到wpack包的项目中,执行命令:-------------------------
process.cwd() ------------------------ 当前工作目录
- process.cwd() 返回 Node.js 进程的当前工作目录
process.cwd() === path.resolve()
- process.cwd()
fs.readFileSync(path[, options]) ----- 读文件
- 作用:返回path的内容
- 参数:
- path:文件名或文件描述符
- options: 配置项,object|string
- encoding:编码格式,可选
path.relative(from, to) --------------- from 到 to 的行对路径
- path.relative() 方法根据当前工作目录返回 ( from ) 到 ( to ) 的 ( 相对路径 )
path.dirname(path) ------------------ 最后一段的父目录
- path.dirname() 方法返回 path 的目录名
- 即 ( 返回路径中最后一段文件或者文件夹所在的文件夹,即最后一段文件或文件夹的父目录 )
path.extname(path) ------------------ 返回path的扩展名
- path.extname(path) 返回path的扩展名
- ext是 extend:扩展
arguments.callee --------------------- 指向当前执行的函数 (严格模式下禁止)
- arguments.callee --------------------- 指向当前执行的函数 (严格模式下禁止)
AST explorer
源码:
require('./a.js')
AST:
{
"type": "Program",
"start": 0,
"end": 17,
"body": [
{ // --------------------------------------- body数组可能包含多个statement状态对象
"type": "ExpressionStatement",
"start": 0,
"end": 17,
"expression": {
"type": "CallExpression", // ----------- 调用表达式
"start": 0,
"end": 17,
"callee": { // ------------------------- callee.name = 'require'
"type": "Identifier",
"start": 0,
"end": 7,
"name": "require"
},
"arguments": [ // ---------------------- 参数列表
{
"type": "Literal",
"start": 8,
"end": 16,
"value": "./a.js",
"raw": "'./a.js'"
}
]
}
}
],
"sourceType": "module"
}
babel相关的AST插件
- @babel/core
- 核心文件
- @babel/parser
- 将源码string转成AST
- @babe/traverse
- 遍历AST
-
enter(path)进入
和exit(path)退出
等钩子
- @babel/types
- 修改,添加,删除等,操作AST
- 用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转换 AST 的成本
babelTypes.stringLiteral(modulePath)
- @bebe/generator
- 将修改后的AST转换成源码string
const options = loaderUtils.getOptions(this)
Loader - 编写一个自定义loader
( loader ) 是一个 ( 函数 ),函数的第一个参数表示 ( 该loader匹配的文件的 源代码 )
loader 不能写成 ( 箭头函数 ),因为需要通过this获取更多的api
-
loader-utils
- 用来获取 module -> rules 中的 loader的 ( options ) 对象
- 通过 ( loader-utils ) 中的 ( getOptions ) 来获取 ( options ) 对象
- 安装:
npm install loader-utils -D
- 使用:
const options = loaderUtils.getOptions(this)
- loader-utils
-
this.callback
- 第一个参数:err // Error 或者 null
- 第二个参数:content // string或者buffer,即处理过后的源代码
- 第三个参数:sourceMap? // 可选,必须是一个可以被这个模块解析的 source map
- 第四个参数:meta? //可选,即元数据
- this.callback - webpack官网文档
-
this.async
- this.async 主要用于处理loader中的异步操作
- 返回值是: this.callback()
-
编写好的loader,如何在webpack.config.js中引入?
- 在根目录中新建 loaders 文件夹,里面存放 replace-loader.js
- 单个loader
module.exports = { ... module: { rules: [{ test: /\.js$/, use: [{ loader: path.resolve(__dirname, 'loaders/replace-loader'), // 需要用到path模块 options: { name: 'aaaaa' } }] }] } }
- 多个loader
module.exports = { ... resolveLoader: { // resolveLoader配置项 modules: ['node_modules', path.resolve(__dirname, 'loaders')] // 告诉 webpack 该去那个目录下找 loader 模块 // 先从node_modules中寻找,再在loaders文件夹中寻找 // modules: ['node_modules', './loaders/'] }, module: { rules: [{ test: /\.js$/, use: [{ loader: 'upper-loader', options: { name: 'aaaaa' } },{ loader: 'replace-loader', options: { name: 'hi!!!!???&&&&' } // 直接加载在loaders文件夹中的 replace-loader.js,这里只需要写上loader的名字即可 }] }] } }
-
自定义loader实例
- loaders/replace-loader.js
const loaderUtils = require('loader-utils') // loader-utils插件 // 可以通过loader-utils中的getOptions拿到loader中的options对象 module.exports = function(source) { // source就是该loader匹配的文件的源码 const options = loaderUtils.getOptions(this) // 通过 loader-utils的getOptions获取options对象 const callback = this.async() // this.async()用来处理loader中的异步操作, -------- 返回值是:this.callback() // this.callback(err, content, sourceMap?, meta?) setTimeout(function() { const result = source.replace('hello', options.name) callback(null, result) }, 1000) }
- webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: { index: './src/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: 'index.js' }, module: { rules: [ // { // test: /\.js$/, // use: [{ // loader: path.resolve(__dirname, 'loaders/replace-loader.js'), // options: { // name: 'woow_wu7' // } // }] // } { test: /\.js$/, use: [{ loader: 'replace-loader', // 这里的名字就是 loaders 文件夹中的 replace-loader.js 文件名 options: { name: 'woow_wu77' } }] } ] }, resolveLoader: { // 规定加载loader的地方限制在 node_modules 文件夹中,和 './loaders/'文件夹中 // 先找 node_modules 再找 './loaders/' modules: ['node_modules', './loaders/'] } }
Compiler - 生命周期钩子函数
- entryOption
- 在 webpack 选项中的 entry 配置项 处理过之后,执行插件
- afterPlugins
- 设置完初始插件之后,执行插件
- run
- compiler.run() 方法执行时触发 - 开始读取 records 之前,钩入(hook into) compiler
- compile
- buildMoudle()执行前触发 - 一个新的编译(compilation)创建之后触发
- afterCompile
- buildMoudle()执行后触发
- emit
- emitFile() 执行时触发 - 生成资源到 output 目录之前。
- done
- 编译完成时触发
Plugin - 编写一个自定义plugin
plugin是一个具有 ( apply ) 方法的类,apply方法参数是 ( compiler ) 调用,并且 compiler 对象可在整个编译生命周期访问
-
过程:
- (1) 在Compiler类所在项目安装 tapable
- (2) 编写plugin类
- 必须有apply()方法
- 在方法中调用compiler实例的hooks属性对应的生命周期钩子的tap()等注册方法
- (3) 在webpack.config.js中的plugins中new注册插件实例
- 就可以在Compiler类的构造函数中循环plugins,执行apply方法
plugin的编写,在plugin中通过tap()注册监听,因为是SyncHook所以tap()注册,还有tapAsync(),tapPromise()等
class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap('EntryOptionPlugin', function() {
console.log('EntryOptionPlugin')
})
}
}
class AfterPlugin {
apply(compiler) {
compiler.hooks.afterPlugins.tap('AfterPlugin', function() {
console.log('AfterPlugin')
})
}
}
class RunPlugin {
apply(compiler) {
compiler.hooks.run.tap('RunPlugin', function() {
console.log('RunPlugin')
})
}
}
class CompilePlugin {
apply(compiler) {
compiler.hooks.compile.tap('CompilePlugin', function() {
console.log('CompilePlugin')
})
}
}
class AfterCompilePlugin {
apply(compiler) {
compiler.hooks.afterCompile.tap('AfterCompilePlugin', function() {
console.log('AfterCompilePlugin')
})
}
}
class EmitPlugin {
apply(compiler) {
compiler.hooks.emit.tap('emit', function() {
console.log('emit')
})
}
}
class DonePlugin {
apply(compiler) {
compiler.hooks.done.tap('DonePlugin', function() {
console.log('DonePlugin')
})
}
}
- 在webpack.config.js中注册插件
plugins: [
new EntryOptionPlugin(),
new AfterPlugin(),
new RunPlugin(),
new CompilePlugin(),
new AfterCompilePlugin(),
new EmitPlugin(),
new DonePlugin()
]
- 在Compiler类中引入tapable并new出不同的生命周期
- 在不同的函数执行的不同时机执行tapbale中的调用call()方法,这里用的是SyncHook所以用tap()注册,用call()调用
class Compiler {
constructor(config) {
this.hooks = {
entryOption: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
}
}
}
run() {
// run方法主要做两件事情
// 1. 创建模块的依赖关系
// 2. 发射打包后的文件
this.hooks.run.call()
this.hooks.compile.call()
this.buildModule(path.resolve(this.root, this.entry), true)
// buildModule()的作用:建模块的依赖关系
// 参数:
// 第一个参数:是entry指定路径的绝对路径
// 第二个参数:是否是主模块
this.hooks.afterCompile.call()
console.log(this.modules, this.entryId)
// 发射一个文件,打包后的文件
this.emitFile()
this.hooks.emit.call()
this.hooks.done.call()
}
webpack打包后文件分析
- 精简代码,去除 webpack_require 上的无关属性,代码如下
- 再继续简化
(function(modules){
var initialMoudles = {}
function __webpack_require__(moduleId)
return __webpack_require__('./src/index.js')
})()
自执行后,相当于调用 __webpack_require__('./src/index.js'),并且 initialMoudles 成为闭包变量,常驻内存
- 参数对象 modules
{
"./src/a.js": function () { eval("") },
"./src/base/b.js": function () { eval("") },
"./src/base/c.js": function () { eval("") },
"./src/index.js": function () { eval("") },
}
- 第一步:
- 调用
__webpack_require__('./src/index.js')
- 执行
modules[moduleId].call()
即执行modules参数对象'./src/index'中的eval()源码
- 调用
- 第二步
- 调用
__webpack_require__('./src/a.js')
- 执行
modules[moduleId].call()
即执行modules参数对象'./src/a.js'中的eval()源码
- 调用
- 第三步
- 调用
__webpack_require__('./src/b.js')
- 执行
modules[moduleId].call()
即执行modules参数对象'./src/b.js'中的eval()源码
- 调用
- 第四步
- 调用
__webpack_require__('./src/c.js')
- 执行
modules[moduleId].call()
即执行modules参数对象'./src/c.js'中的eval()源码
- 调用
- 直到modules中的所有moudleId对应的源码都执行完
手写webpack - compiler
流程
- buildModul() - modules对象的赋值过程
- (1) 将webpack.config.js作为参数传入Compiler类
- (2) 通过new命令调用Compiler,生成compiler实例,并调用Compiler.prototype上的 run 方法
- 在new命令执行的时候,遍历webpack.config.js中的plugins数组中的plugin实例上的apply()方法
- tap => apply()方法中会调用
compiler.hooks.钩子函数.tap()
注册监听事件 - call => 在不同的compiler的函数中去call()执行事件,从而在不同生命周期实现监听
- (3) 在 run 方法中调用 buildModule() 和 emitFile()
- (4) buildModule() 方法接受webpack.config.js中的 ( 入口文件的绝对路径 ) 和 ( 是否是主模块 ) 为参数
- (5) 在 buildModule() 中调用 getSource('absolutePath') 方法
- 参数是模块的绝对路径
- 通过 fs.readFileSync(path, options) 读取源码
- 循环webpack.config.js中的module->rules数组->test,用test和absolutePath做正则匹配,匹配成功的话,就递归调用loader函数解析该文件,并返回该文件,直到moudle->rules->use中的数组成员loader都调用完
- (6) 在 buildModule() 中调用 parse() 方法解析源码,修改源码,返回源码
- (7) parse()方法
- 参数有两个:模块的源码 和 模块文件所在的文件夹路径 - 即文件所在的文件夹
- 返回值有两个:修改后的模块源码 和 该模块的依赖数组
- 注意:修改部分(替换require名,moudules中的key要是'./src/xxxxx'的格式,匹配loader并处理源文件)
- (8) 将 模块的相对路径 和 模块修改后的源码 一一对应作为 modules对象的 key和value值
- (9) 如果 parse()返回的该模块的依赖数组不为空,则遍历该模块的依赖数组,并递归调用 buildModule 方法,直到最后一个模块没有依赖为止
- emitFile() - 将源码发射到webpack.config.js指定的目录的过程
- (1) 安装ejs模板引擎 并编写模板 传入两个参数 entryId 和 modules
- (2) 获取webpack.config.js中的output对象的path,filename
- (3) fs.readFileSync()读取ejs模板源文件
- (4) 将esj.render() 生成可以执行的文件
- (5) fs.writeFileSync(file, data[, options])将生成的经esj编译后的源文件写入output.path中,文件名是outpt.name
wpack.js
#! /usr/bin/env node
// 一.需要拿到 webpack.config.js 文件
const path = require('path')
const config = require(path.resolve('webpack.config.js')) // 获取webpack.config.js
const Compiler = require('../lib/compiler.js')
const compiler = new Compiler(config)
compiler.run() // 调用run方法,
Compiler - run()方法
- ( run ) 方法主要做 ( 两件 ) 事情
- (1) 调用buildModul() -> modules = { } ------------- 依赖关系对象的key和vlue的收集
- key:所有模块的相对路径
- value:所有模块的源码
- (2) 调用emitFile方法 -> 发射打包后的文件到指定的文件夹中
- (1) 调用buildModul() -> modules = { } ------------- 依赖关系对象的key和vlue的收集
- 具体流程
- 在run中调用 ( buildModule ) 方法
- 在run中调用 ( emitFile ) 方法
- buildMoudle(moduleAbsolutePath, isEntryModule)
- buildMoudle()参数
- moduleAbsolutePath:每个模块的绝对路径
- isEntryModule:布尔值,是否是入口主模块,入口模块一般是index.js
- buildmoudle主要做以下几件事情:
- 通过
fs.readFileSync(modulePath, { encoding: 'utf8' })
读取传入的模块路径对应的源码- 注意:这里一定要用utf8格式,不然@babel/parse解析时会报错
- 如果是 ( 主入口模块 ),就用 ( this.entryId ) 来标记主入口模块的路径 (路径需要处理成想要的格式)
- 调用 parse() 方法
- 传入:( 未修改的源码 ) 和 入口文件所在 ( 文件夹 )
- 返回:( 修改过后的源码 ) 和 当前模块的依赖数组,即 ( 当前模块require的文件 )
- 修改源码
- 通过 @babel/parser 将源码转成AST
- 通过 @babel/traverse 遍历AST,并在遍历过程中通过 @babel/types完成修改,添加,删除等操作
- 通过 @babel/types 修改,添加,删除AST的各个节点
- 通过 @babel/generator将修改后的AST转成源码字符串
- 修改源码
- 如果 ( 当前模块还有依赖项 ),即返回的当前模块的依赖项数组不为空,就 ( 递归执行buildMoudle() ) 方法
- 最终收集所有的模块对应关系到 modules对象中
this.modules[moduleRelativePath] = sourceCode
- 通过
buildModule(moduleAbsolutePath, isEntry) {
// 参数
// moduleAbsolutePath:是模块的绝对路径,通过path.resolve(this.root, this.entry)获得
// isEntry:是否是入口主模块
const source = this.getSource(moduleAbsolutePath)
// 读取模块的源文件内容
const moduleRelativePath = './' + path.relative(this.root, moduleAbsolutePath)
// path.relative(from, to)
// path.relative(from, to)方法根据当前工作目录返回 from 到 to 的相对路径
// moduleRelativePath
// 表示模块文件的相对路径
// moduleRelativePath = moduleAbsolutePath - this.root
// console.log(source, moduleRelativePath)
if (isEntry) {
this.entryId = moduleRelativePath
// 如果是主入口,把改造后的形如 ./src/index.js 的文件路径赋值给 entryId
}
const fatherPath = path.dirname(moduleRelativePath)
// fatherPath 即获取 ./src/index.js 的最后一段文件或文件夹的父目录 => ./src
const {sourceCode, dependencies} = this.parse(source, fatherPath).replace(/\\/g, '/');
// parse()主要功能
// 1. 对入口文件源码进行改造
// 2. 返回改造后的源码 和 依赖列表
// 参数:
// 改造前的源码
// 和父路径
// 返回值
// 改造后的源码
// 依赖列表
this.modules[moduleRelativePath] = sourceCode;
// this.modules
// 模块的路径 和 模块的源码一一对应
// key => moduleRelativePath
// value => sourceCode
dependencies.forEach(dep => { // 附模块的加载 递归加载
this.buildModule(path.join(this.root, dep), false)
})
// 递归依赖数组,将this.modules对象的所有key,vaue收起到一起
}
Compiler - run() - buildMoudle() - getSource()方法 - 增加loader解析源码后再给到parse()去转换
- less-loader
const less = require('less')
const lessLoader = function(source) {
const that = this;
let res;
less.render(source, function(err, content) {
res = content.css.replace(/\n/g, '\\n').replace(/\r/g, '\\r')
// res = that.callback(null, content.css.replace(/\n/g, '\\n'))
})
return res;
}
- style-loader
const styleLoader = function(source) {
const style = `
const styleElement = document.createElement('style');
styleElement.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(styleElement);
`
return style
}
module.exports = styleLoader
- getSource()方法中添加loader部分的代码
getSource()方法中添加loader部分的代码
getSource(modulePath) {
let content = fs.readFileSync(modulePath, { encoding: 'utf8' })
// { encoding: 'utf8' } 一定要用utf8格式
// 不然在@babel/parse中.parse()解析时会报错
const { rules } = this.config.module // 获取rule数组
for(let i = 0; i < rules.length; i++) { // 循环rules数组
const {test, use} = rules[i] // 取出每个对象中的test和use
let reverseIndex = use.length - 1; // use也是一个数组,从后往前,从下往上执行
if (test.test(modulePath)) {
function runLoader() {
const loader = require(use[reverseIndex--])
// 先去use数组中的最后一个,再一次取前一个
// require('absolute path') 引入loader函数
content = loader(content)
// 执行loader函数,返回loader修改后的内容
if (reverseIndex >= 0) { // 循环递归结束条件
runLoader()
}
}
runLoader()
}
}
// content
// fs.readFileSync(modulePath, {encoding: 'utf8'}) 读取模块源码,返沪utf8格式的源码
// 参数:
// modulePath:这里是模块的 绝对路径
return content
}
Compiler - run() - buildMoudle() - parse()方法
parse(source, parentPath) { // AST (解析 -> 遍历 -> 转换 -> 生成)
const dependencies = [] // 依赖数组
// 解析
const AST = babelParser.parse(source)
// 遍历
babelTraverse(AST, {
CallExpression(p) { // 调用表达式,注意这里参数不能写成path,和node的path冲突了
// 修改
// 主要做两件事情
// 1. require() => __webpack_require__()
// 2. require('./a.js') => require('./src/a.js)
const node = p.node
if (node.callee.name === 'require') { // 找到节点中的callee.name是require的方法,修改名字
node.callee.name = '__webpack_require__' // 替换require的名字
let modulePath = node.arguments[0].value;
modulePath = "./" + path.join(parentPath, modulePath).replace(/\\/g, '/') + (path.extname(modulePath) ? '' : '.js'); // 后缀存在就加空字符串即不做操作,不存在加.js
// 例如:modulePath = './' + '/src' + 'index' + '.js'
// 获取require的参数
dependencies.push(modulePath)
// 转换
node.arguments = [babelTypes.stringLiteral(modulePath)] // 把AST中的argumtns中的Literal修改掉 => 修改成最新的modulePath
}
}
})
// 生成
const sourceCode = babelGenerator(AST).code;
// 返回
return {sourceCode, dependencies}
}
Compiler - run() - emitFile()
emitFile() { // 发射文件
console.log(111111111)
const {path: p, filename} = this.config.output
const main = path.join(p, filename)
// main 表示打包后的文件的路径
const templeteSourceStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 读取模块源文件 main.ejs
const code = ejs.render(templeteSourceStr, {
entryId: this.entryId,
modules: this.modules
})
// 渲染模板
// 模板中有两个参数 entryId 和 modules
this.assets = {}
this.assets[main] = code;
// key:打包后的文件路径
// value: 打包后的文件源码
fs.writeFileSync(main, this.assets[main])
// 写文件按
// fs.writeFileSync(file, data[, options])
}
------
main.ejs
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
({
<%for(let key in modules){%>
"<%-key%>":
(function (module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`)
}),
<%}%>
});
Compiler 总文件
const fs = require('fs')
const path = require('path')
const babelParser = require('@babel/parser')
const babelTypes = require('@babel/types')
const babelTraverse = require('@babel/traverse').default
const babelGenerator = require('@babel/generator').default
const ejs = require('ejs')
const {SyncHook} = require('tapable')
class Compiler {
constructor(config) {
this.config = config // webapck.config.js中的内容,即webpack配置文件模块
this.entryId = null // 入口文件的相对路径
this.modules = {}
// 用来保存所有模块信息
// key:模块的相对路径
// value:模块的源码
this.entry = config.entry.index; // 入口文件路径
this.root = process.cwd(); // 当前工作路径,返回node.js进程的当前工作目录
this.hooks = {
entryOption: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
}
// plugins获取
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(plugin => {
plugin.apply(this) // this是compiler实例
})
}
this.hooks.afterPlugins.call()
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, { encoding: 'utf8' }) // 记得一定要utf8格式
const { rules } = this.config.module
for(let i = 0; i < rules.length; i++) {
const {test, use} = rules[i]
let reverseIndex = use.length - 1;
if (test.test(modulePath)) {
function runLoader() {
const loader = require(use[reverseIndex--])
content = loader(content)
console.log(content, '6666666666');
if (reverseIndex >= 0) {
runLoader()
}
}
runLoader()
}
}
// content
// fs.readFileSync(modulePath, {encoding: 'utf8'}) 读取模块源码,返沪utf8格式的源码
// 参数:
// modulePath:这里是模块的 绝对路径
return content
}
parse(source, parentPath) { // AST (解析 -> 遍历 -> 转换 -> 生成)
const dependencies = [] // 依赖数组
// 解析
const AST = babelParser.parse(source)
// 遍历
babelTraverse(AST, {
CallExpression(p) { // 调用表达式,注意这里参数不能写成path,和node的path冲突了
// 修改
// 主要做两件事情
// 1. require() => __webpack_require__()
// 2. require('./a.js') => require('./src/a.js)
const node = p.node
if (node.callee.name === 'require') { // 找到节点中的callee.name是require的方法,修改名字
node.callee.name = '__webpack_require__' // 替换require的名字
let modulePath = node.arguments[0].value;
modulePath = "./" + path.join(parentPath, modulePath).replace(/\\/g, '/') + (path.extname(modulePath) ? '' : '.js'); // 后缀存在就加空字符串即不做操作,不存在加.js
// 例如:modulePath = './' + '/src' + 'index' + '.js'
// 获取require的参数
dependencies.push(modulePath)
// 转换
node.arguments = [babelTypes.stringLiteral(modulePath)] // 把AST中的argumtns中的Literal修改掉 => 修改成最新的modulePath
}
}
})
// 生成
const sourceCode = babelGenerator(AST).code;
// 返回
return {sourceCode, dependencies}
}
buildModule(moduleAbsolutePath, isEntry) {
// 参数
// moduleAbsolutePath:是模块的绝对路径,通过path.resolve(this.root, this.entry)获得
// isEntry:是否是入口主模块
const source = this.getSource(moduleAbsolutePath)
// 读取模块的源文件内容
let moduleRelativePath = './' + path.relative(this.root, moduleAbsolutePath).replace(/\\/g, '/');
console.log(path.relative(this.root, moduleAbsolutePath))
// path.relative(from, to)
// path.relative(from, to)方法根据当前工作目录返回 from 到 to 的相对路径
// moduleRelativePath
// 表示模块文件的相对路径
// moduleRelativePath = moduleAbsolutePath - this.root
// console.log(source, moduleRelativePath)
if (isEntry) {
this.entryId = moduleRelativePath
// 如果是主入口,把改造后的形如 ./src/index.js 的文件路径赋值给 entryId
}
const fatherPath = path.dirname(moduleRelativePath)
// fatherPath 即获取 ./src/index.js 的最后一段文件或文件夹的父目录 => ./src
const {sourceCode, dependencies} = this.parse(source, fatherPath)
// parse()主要功能
// 1. 对入口文件源码进行改造
// 2. 返回改造后的源码 和 依赖列表
// 参数:
// 改造前的源码
// 和父路径
// 返回值
// 改造后的源码
// 依赖列表
this.modules[moduleRelativePath] = sourceCode;
// this.modules
// 模块的路径 和 模块的源码一一对应
// key => moduleRelativePath
// value => sourceCode
dependencies.forEach(dep => { // 附模块的加载 递归加载
this.buildModule(path.join(this.root, dep), false)
})
// 递归依赖数组,将this.modules对象的所有key,vaue收起到一起
}
emitFile() { // 发射文件
const {path: p, filename} = this.config.output
const main = path.join(p, filename)
// main 表示打包后的文件的路径
const templeteSourceStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 读取模块源文件 main.ejs
const code = ejs.render(templeteSourceStr, {
entryId: this.entryId,
modules: this.modules
})
// 渲染模板
// 模板中有两个参数 entryId 和 modules
this.assets = {}
this.assets[main] = code;
// key:打包后的文件路径
// value: 打包后的文件源码
fs.writeFileSync(main, this.assets[main])
// 写文件按
// fs.writeFileSync(file, data[, options])
}
run() {
// run方法主要做两件事情
// 1. 创建模块的依赖关系
// 2. 发射打包后的文件
this.hooks.run.call()
this.hooks.compile.call()
this.buildModule(path.resolve(this.root, this.entry), true)
// buildModule()的作用:建模块的依赖关系
// 参数:
// 第一个参数:是entry指定路径的绝对路径
// 第二个参数:是否是主模块
this.hooks.afterCompile.call()
console.log(this.modules, this.entryId)
// 发射一个文件,打包后的文件
this.emitFile()
this.hooks.emit.call()
this.hooks.done.call()
}
}
module.exports = Compiler
资料
打包原理: https://www.jianshu.com/p/89bd63d2564d
打包原理2:https://juejin.im/post/6844903949376438285
Webpack Loader:https://juejin.im/post/6844903555673882632