webpack早就已經在前端領域大放異彩,會使用和優化webpack也已經是中、高級工程師必備技能,在此基礎之上再對webpack的原理進行理解和掌握,必定會在未來的開發中事半功倍。若是對於webpack不熟悉可以查看之前的文章進行學習和了解。
由於本人能力一般、水平有限,所以會在本篇文章編寫過程中對一些內容進行又臭又長的贅述,就是為了能讓一些基礎比較薄弱的同學閱讀起來可以更加省心點,接下來即將開始正題了,希望此文章能對你有些許幫助。
構建項目
-
新建一個文件夾
webpack-theory
是之后插件的名字,可以理解為webpack的別名,可以直接
wepack-theory
進行使用。 -
新建
bin
目錄,在此目錄下創建webpack-theory.js
文件, 將打包工具主程序放入其中主程序的頂部應當有:
#!/usr/bin/env node
標識,指定程序執行環境為 node#!/usr/bin/env node // log的內容修改直接,可以直接生效 console.log('當通過npm link鏈接之后,通過webpack-theory指令可以直接打出');
-
在package.json中配置 bin 腳本,與scripts平級
{ "bin": "./bin/webpack-theory.js" }
-
通過
npm link
將本地的項目webpack-theory 鏈接到全局包中,鏈接之后便可以直接在本地使用,供本地測試使用,具體參考 npm link- 成功之后,可以
cd /usr/local/lib/node_modules
查看所有安裝的包
- 成功之后,可以
進入目錄后,可以看到webpack-theory
,webpack-theory就是npm link時,在全局的node_modules中生成一個符號鏈接,指向模塊(webpack-theory)的本地目錄,當本地的文件(bin/webpack-theory)修改時會自動鏈接到全局,因為全局的node_modules只是本地的引用
- 在本地執行
webpack-theory
, 會直接將bin/webpack-theory.js
的console.log內容輸出
>>> ~ » webpack-theory
>>> 當通過npm link鏈接之后,通過webpack-theory指令可以直接打出
分析bundle
在深入接觸webpack
原理之前,需要知道其打包生成的文件結果是什么樣,通過打包生成的文件可以從整體了解webpack在對文件處理過程中做了哪些事情,通過結果反推其原理。
-
自行創建一個簡單的weback項目,創建三個js文件,分別是index.js,parent.js 和 child.js,並將其通過webpack進行打包
- index.js 內容
const parent = require('./parent.js') console.log(parent)
- parent.js 內容
const child = require('./child.js') module.exports = { msg: '我是parent的信息', child: child.msg }
- child.js 內容
module.exports = { msg: '我是child的信息' }
-
通過
npx webpack
進行打包,將打包文件進行簡單的刪除和整理之后
(function (modules) { // 將所有的模塊組成一個modules對象傳遞進來, 鍵就是模塊的路徑,值就是模塊內部的代碼
// 模塊緩存對象, 已經解析過的路徑都會放進來,可以判斷當前需要解析的模塊是否已經解析過
var installedModules = {};
// 定義一個 webpack 自己的的 require polyfill
function __webpack_require__(moduleId) {
// 檢測 moduleId 是否已經存在緩存中了,若是已經存在則不需要在進行依賴解析
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創建一個新的 module, 並將其push至緩存中,方便在后續遞歸遍歷解析依賴時,檢測是否已經解析過
var module = installedModules[moduleId] = {
i: moduleId, // moduleId 是自執行函數的參數 modules 對象的鍵,根本是模塊的路徑
exports: {}
};
// 執行 modules[moduleId] 函數
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 將 exports 返回
return module.exports;
}
// 將 webpack.config.js 配置中的 entry 作為 moduleId 進行傳遞
return __webpack_require__("./src/index.js");
})
/*** 將項目中的幾個模塊作為自執行函數的參數傳遞 ***/
({
// webpack.config.js 配置中 entry 的值,會將其作為遞歸解析依賴的入口
"./src/index.js": (function (module, exports, __webpack_require__) {
eval("const parent = __webpack_require__(/*! ./parent.js */ \"./src/parent.js\")\n\nconsole.log(parent)\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/parent.js": (function (module, exports, __webpack_require__) {
eval("const child = __webpack_require__(/*! ./child.js */ \"./src/child.js\")\n\nmodule.exports = {\n msg: '我是parent的信息',\n child: child.msg\n}\n\n\n\n//# sourceURL=webpack:///./src/parent.js?");
}),
"./src/child.js": (function (module, exports) {
eval("\nmodule.exports = {\n msg: '我是child的信息'\n}\n\n//# sourceURL=webpack:///./src/child.js?");
})
});
根據生成的bundle.js
可以梳理webpack的整體打包思路,就是利用一個自執行函數創建一個閉包,在這個獨立的作用域中,將模塊的路徑作為modules的鍵、模塊的內容放在一個函數中作為值作為自執行函數的形參傳遞進來,通過自定義的函數 __webpack_require__
進行遞歸解析。
簡單分析一下bundle的整體執行過程
- 第一步: 自執行函數第一次執行時,會直接運行內部的
__webpack_require__
函數,並將入口文件的路徑./src/index.js
作為形參moduleId
傳遞 - 第二步: 在函數
__webpack_require__
執行過程中- 會首先判斷當前
moduleId
是否已經存在緩存installedModules
中,若是存在則直接返回,不需要再繼續解析其依賴。若是不存在,則會構造一個對象並將其同時存到installedModules
中和module
中。第一次執行時installedModules
為空對象,moduleId為./src/index.js
。 - 執行
modules[moduleId]
函數,即執行modules['./src/index.js']
,會通過call改變其作用域並傳遞module, module.exports, __webpack_require__
三個形參,執行的內容就是入口文件模塊./src/index.js
中的js代碼。- call傳遞的作用域置為
module.exports
,由於module.exports
此時為空對象,則index.js
中的作用域就是指向它,這也是典型的使用閉包來解決作用域的問題。 module, module.exports
的作用就是用於模塊內拋出對象使用的,作用是一個的,可以參考require.js
進行這塊的理解__webpack_require__
的作用就很巧妙了,此時入口index.js
中使用的require('./parent.js')
已經被替換成__webpack_require__("./src/parent.js\")
,執行modules[moduleId]
函數時便會在此調用__webpack_require__
函數進行遞歸調用,會再次回到第二步,直到child.js
執行完畢,整個bundle才算執行結束。
- call傳遞的作用域置為
- 會首先判斷當前
分析完bundle之后,會發現對於webpack的打包結果,除了形參modules
會跟着代碼的業務邏輯修改而變化之外,自執行函數中的代碼始終是固定不變的,因此想要編寫一個屬於自己的webpack時,重點關注和需要解決的就是modules這個對象是如何生成的。
創建bundle
分析完webpack打包完成之后的bundle文件,以結果為導向反推實現過程便會簡單許多,若是讓我們自己動手實現一個簡單版的webpack,便會有了些思路。
首先需要一個簡單的wbepack配置
const path = require('path')
module.exports = {
entry: './src/index.js',
output:{
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}
簡單版本的webpack實現思路
- 獲取webpack配置文件
- 封裝一個用於解析配置並將其簡單打包的方法
- 利用抽象語法書解析模塊內容
- 遞歸解析模塊依賴
- 使用模版引擎輸出結果
有了思路,接下來就是按部就班的實現
- 獲取webpack配置文件,而需要做的事情就是將這個配置文件進行解析,根據配置文件進行打包生成bundle。首先就是讀取需要打包項目的配置文件
const config = require(path.resolve('webpack.config.js'))
- 獲取配置文件之后,便是如何解析並實現webpack的功能,這些功能全部封裝在Compiler類中,用於解析配置文件的配置,並通過start進行啟動解析
const Compiler = require('../lib/Compiler')
new Compiler(config).start()
- 重點就是如何實現這個方法,定義一個Compiler類,提供一個start方法開始webpack打包,通過depAnalyse便可以獲取到入口文件index的內容
const path = require('path')
const fs = require('fs')
class Compiler {
constructor(config){
this.config = config
const { entry } = config // 配置文件
this.entry = entry // 入口文件
this.root = process.cwd() // 輸入 webpack-theory 的路徑
this.modules = {} // 初始化一個控對象,存放所有的模塊
}
/**
* 開始打包
* 打包最主要的就是依賴的分析
*/
start(){
this.depAnalyse(path.resolve(this.root, this.entry))
}
/**
* 依賴分析
* 需要根據入口entry進行開始分析
*/
depAnalyse(modulePath){
// 獲取 index.js 的內容
let source = this.getSource(modulePath)
}
// 讀取文件
getSource(path){
return fs.readFileSync(path, 'utf-8')
}
}
module.exports = Compiler
- 獲取到
index.js
的文件內容之后,並不能直接使用,需要通過將其解析成抽象語法樹進行處理,需要使用一個插件@babel/parser
將模塊代碼解析成AST,然后插件@babel/traverse
配合着使用,將AST的節點進行替換,替換完成之后,使用插件@babel/generator
將AST轉換成模塊的原有代碼,改變的只是將require
變成__webpack_require__
,需要注意的是需要將路徑處理一下,因為此時的路徑是相對於src
下面的。處理完index
之后需要遞歸調用處理全部的模塊,並聲稱bundle
中自執行函數的參數modules
此時index的模塊代碼經過處理之后,變成了需要的代碼
const parent = __webpack_require__("./src/parent.js");
console.log(parent);
在函數depAnalyse
中添加如下處理
// 獲取 index.js 的內容
let source = this.getSource(modulePath)
// -------
// 准備一個依賴數組,用於存儲當前模塊
let dependenceArr = []
// 將js代碼 解析成AST
let ast = parser.parse(source)
// 將AST中的 require 替換為 __webpack_require__
traverse(ast, {
// p 是抽象語法樹的節點
CallExpression(p) {
if (p.node.callee.name === 'require') {
// 將代碼中的 require 替換為 __webpack_require__
p.node.callee.name = '__webpack_require__'
const oldValue = p.node.arguments[0].value
// 修改路徑,避免windows出現反斜杠 \
p.node.arguments[0].value = ('./' + path.join('src', oldValue)).replace(/\\+/g, '/')
// 每找到一個require調用,就將其中的路徑修改完畢后加入到依賴數組中
dependenceArr.push(p.node.arguments[0].value)
}
}
})
// 構建modules對象
const sourceCode = generator(ast).code
const modulePathRelative = './' + (path.relative(this.root, modulePath)).replace(/\\+/g, '/')
this.modules[modulePathRelative] = sourceCode
// 遞歸調用加載所有依賴
dependenceArr.forEach(dep => this.depAnalyse(path.resolve(this.root, dep)))
至此已經完成了modules
的處理,接下來需要處理的就是怎么生成bundle.js
,分析bundle
時已經指出我們需要關注的地方只有modules
的拼接,至於自執行函數中的內容都是基本都是固定的,不需要額外的處理
- 如何使用模版引擎打包模塊的代碼呢?
-
使用模版引擎ejs創建模版,模版的內容就是
webpack
打包生成的內容,只需要根據Compiler
中modules
進行遍歷即可,還需要關注的是return __webpack_require__(__webpack_require__.s = "<%-entry%>")
,這里傳入的是配置文件的入口,也是自執行函數第一次執行時的參數- 創建
ejs
的模板文件output.ejs
,需要關注的只有兩個地方,其它地方使用默認的代碼
// 第一次執行的參數就是配置的entry return __webpack_require__(__webpack_require__.s = "<%-entry%>"); // 拼接函數需要的形參 modules { <% for (let k in modules) {%> "<%-k%>": (function (module, exports, __webpack_require__) { eval(`<%-modules[k]%>`); }), <%}%> }
- 創建
-
為
Compiler
增加一個emitFile
方法,用於根據模板生成打包的bundle
文件,在start
函數中的depAnalyse
之后進行調用/** * 根據寫好的模板 創建文件 */ emitFile(){ // 已經創建好的 ejs 模版 const template = this.getSource(path.join(__dirname, '../template/output.ejs')) // 使用 ejs 進行編譯 const result = ejs.render(template, { entry: this.entry, modules: this.modules }) // 獲取輸出路徑和文件名 const { path: filePath, filename } = this.output const outputPath = path.join(filePath, filename) // 打包生成bundle 並放在指定的目錄下 fs.writeFile(outputPath, result, (err) => { console.log(err ? err : '打包生成bundle完成'); }) }
到目前為止,已經可以進行簡單的模塊打包,可以將index.js、parent.js和child.js進行簡單的打包,這里僅僅是支持最簡單的webpack用法打包
loader
loader是webpack的重要核心功能之一,也是使用頻率非常高的,主要功能就是將代碼按照預期的結果進行加工處理,生成最終的代碼后輸出,因此掌握loade的基本機制是很有必要的。loader的使用也是非常簡單的,其基本配置和用法這里不再贅述,接下來一起看看如何在自己的webpack-theory中添加解析loader和如何編寫一個自己的loader。
自制loader
在為webpack-theory
添加處理loader的能力之前,先看看如何在webpack
中實現一個自己的loader
-
webpack中loader,主要步驟如下
- 讀取webpack.config.js配置文件的module.rules配置項,進行倒序迭代(rules的每項匹配規則按倒序匹配)
- 根據正則匹配到對應的文件類型,同時再批量導入loader函數
- 默認是倒序迭代調用所有的loader函數(loader從右到左,從下到上),也可以自己來控制這個順序
- 最后返回處理后的代碼
-
當想要在webpack中增加處理cass文件能力的時候,會進行loader的配置
{
test:/\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
sass-loader
其實就是一個函數,根據test
的匹配規則,將以.scss
結束的文件內容讀取出來,然后將匹配到的文件內容作為參數傳遞給一個函數,這個函數將sass
文件的內容按照規則進行加工處理成瀏覽器能夠識別的css
並輸出,所以loader
的本質就是一個函數,接受一個參數,這個參數就是匹配到的文件里面的代碼。同理,css-loader
和style-loader
也是這樣的處理流程,只是內部做的事情不同。
function handlerScss(sourceCode){
// 這里就是將scss文件的內容,按照規則進行加工、處理,結果就是瀏覽器能夠識別解析的css,並將其返回
return newSourceCode
}
- 接下來實現一個自己的簡單loader,將之前的
parent.js
和child.js
中的信息使用loader處理為msg
// 將js文件中的 信息 換成 msg
module.exports = function (source) {
return source.replace(/信息/g, 'msg')
}
在webpack中配置loader
{
test:/\.js/,
use:['./loader/handlerLoader1.js']
}
使用npx webpack
打包之后,可以看到打包的代碼中已經將原有代碼中的信息更換為msg
- 若是想講
handlerLoader1
的loader中替換的內容通過配置自定義處理呢?就像是url-loader
那樣傳遞一個配置選項options
,然后在loader中進行接受並處理。可以通過loader-utils
的getOptions
提取loader中的options
進行處理,老版本是通過thus.query
來進行處理
修改loader文件handlerLoader1
const loaderUtils = require('loader-utils')
// 將js文件中的 信息 換成 通過options傳遞的name
module.exports = function (source) {
const optionsName = loaderUtils.getOptions(this).name || ''
return source.replace(/信息/g, optionsName)
}
修改webpack的loader
{
test:/\.js/,
use:{
loader: './loader/loader1.js',
options:{
name:'新的信息'
}
}
}
使用npx webpack
打包之后,便可以通過options
配置進行替換
- 若是
handlerLoader1
處理完的東西還需要交給下一個loader進行處理之后,這樣就會牽扯到多個同級loader的情況,將handlerLoader1
拷貝兩份,分別命名為handlerLoader11
和handlerLoader12
,內容可保持原有的,只是在原有的函數中分別打印其對應的loader的文件名稱,因為只是為了看看loader的加載。
handlerLoader1
的內容為
// 將js文件中的 信息 換成 msg
module.exports = function (source) {
console.log('我是 handlerLoader1'); // 其余兩loader 的log分別為 handlerLoader2 handlerLoader3
return source.replace(/信息/g, 'msg')
}
webpack配置loader
{
test:/\.js/,
use:[
'./loader/handlerLoader1.js',
'./loader/handlerLoader2.js',
'./loader/handlerLoader3.js'
]
}
執行webpack打包,輸出結果,可以得出loader的默認順序是由右到左
>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1
- 若修改webpack的loader為
{
test:/\.js/,
use:['./loader/loader1.js']
},{
test:/\.js/,
use:['./loader/loader2.js']
},{
test:/\.js/,
use:['./loader/loader3.js']
},
執行webpack打包,輸出結果,可以得出loader的默認順序是由下到上的
>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1
添加loader功能
通過自制一個loader之后,可以總結得到webpack支持loader的功能,主要就是4步
- 讀取配置文件
webpack.config.js
的module.rules
loader配置項,進行倒序迭代 - 根據正則匹配到對應的文件類型,同時再批量導入loader函數
- 倒序迭代調用所有loader函數
- 返回處理后的代碼
在webpack-theory
中增加處理loader
的能力,無非就是在加載每個模塊的時候,根據配置的rules
的正則進行匹配需要的資源,滿足條件之后就會加載並使用對應的loader
進行處理並迭代調用
需要注意的是,在什么時候去執行loader
呢,在每次獲取模塊依賴的時候,都需要進行loader
的test
匹配,若是匹配到就加載對應的loader
進行處理。例如本文的案例代碼存在三個js文件,首先會加載index.js
,在加載解析index的依賴之前就需要對其進行倒序便利全部的loader
,若是匹配到對應的loader
就會加載對應的loader
對index.js
的內容進行處理,因為index引入了parent.js
,接下來便會在遞歸調用depAnalyse
方法解析parnet之前進行判斷和處理,child.js
同理。
在depAnalyse
方法中每次解析以來之前添加如下代碼:
// 內部定義一個處理loader的函數
const _handleLoader = (usePath, _this) => {
const loaderPath = path.join(this.root, usePath)
const loader = require(loaderPath)
source = loader.call(_this, source)
}
// 讀取 rules 規則, 進行倒序遍歷
const rules = this.rules
for (let i = rules.length - 1; i >= 0; i--) {
const {
test,
use
} = rules[i]
// 匹配 modulePath 是否符合規則,若是符合規則就需要倒序遍歷獲取所有的loader
// 獲取每一條規則,和當前的 modulePath 進行匹配
if (test.test(modulePath)) {
// use 可能是 數組、對象、字符串
console.log(use);
if (Array.isArray(use)) {
// array
for (let j = use.length - 1; j >= 0; j--) {
// const loaderPath = path.join(this.root, use[j])
// const loader = require(loaderPath)
// source = loader(source)
_handleLoader(use[j])
}
} else if (typeof use === 'string') {
// string
_handleLoader(use)
} else if (use instanceof Object) {
// object
_handleLoader(use.loader, {
query: use.options
})
}
}
}
loader基礎的相關編寫到此為止,但是還是需要多加練習的思考,這里僅僅是演示了最簡單的,大家可以參考官方文檔進行loader的enforce
、異步loader
等知識點的深入學習和查看babel
、sass-loader
等社區優秀loader
進行深入的理解和練習。
plugin
插件是 webpack 生態系統的重要組成部分,為社區用戶提供了一種強大方式來直接觸及 webpack 的編譯過程(compilation process)。插件能夠 鈎入(hook) 到在每個編譯(compilation)中觸發的所有關鍵事件。在編譯的每一步,插件都具備完全訪問
compiler
對象的能力,如果情況合適,還可以訪問當前compilation
對象。
自定義插件本質就是在webpack的編譯過程的提供的生命周期鈎子中,進行編碼開發實現一些功能,在適當的時間節點做該做的事情,例如clean-webpack-plugin
插件,就是在編譯之前執行插件,將打包目錄清空。
自制plugin
-
在實現自制插件之前,先了解一下webpack插件組成
-
一個JavaScript命名函數
-
在插件函數的prototype上定義一個apply方法
-
指定一個綁定到webpack自身的事件鈎子
-
處理webpack內部實例的特定數據
-
功能完成后調用webpack提供的回調
-
-
webpack的生命周期鈎子 生命周期鈎子
Compiler
模塊是 webpack 的支柱引擎,它通過 CLI 或 Node API 傳遞的所有選項,創建出一個 compilation 實例。它擴展(extend)自Tapable
類,以便注冊和調用插件。大多數面向用戶的插件首,會先在Compiler
上注冊。
hello word
根據官方文檔實現一個hello word插件,可以簡單的了解到plugin
// 1. 一個JavaScript命名函數
// 2. 在插件函數的 prototype 上定義一個apply方法
class HelloWordPlugin {
// 3. apply 中有一個 compiler 形參
apply(compiler){
console.log('插件執行了');
// 4. 通過compiler對象可以注冊對應的事件,全部的鈎子都可以使用
// 注冊一個編譯完成的鈎子, 一般需要將插件名作為事件名即可
compiler.hooks.done.tap('HelloWordPlugin', (stats) => {
console.log('整個webpack打包結束了');
})
compiler.hooks.emit.tap('HelloWordPlugin', (compilation) => {
console.log('觸發emit方法');
})
}
}
module.exports = HelloWordPlugin
在webpack.config.js
引入並使用
const HelloWordPlugin = require('./plugins/HelloWordPlugin')
{
// ...
plugins:[
new HelloWordPlugin()
]
}
npx webpack
打包,可以查看插件的觸發
>>> 插件執行了
>>> 觸發emit方法
>>> 整個webpack打包結束了
HtmlWebpackPlugin
模仿實現HtmlWebpackPlugin插件的功能
html-webpack-plugin 可以將制定的html模板復制一份輸出到dist目錄下,自動引入bundle.js
- 實現步驟
- 編寫一個自定義插件,注冊 afterEmit 鈎子
- 根據創建對象時傳入的 template 屬性來讀取 html 模板
- 使用工具分析HTML,推薦使用 cheerio,可以直接使用jQuery API
- 循環遍歷webpack打包的資源文件列表,如果有多個bundle就都打包進去
- 輸出新生成的HTML字符串到dist目錄中
const path = require('path')
const fs = require('fs')
const cheerio = require('cheerio')
class HTMLPlugin {
constructor(options){
// 插件的參數,filename、template等
this.options = options
}
apply(compiler){
// 1. 注冊 afterEmit 鈎子
// 如果使用done鈎子,則需要使用stats.compilation.assets獲取,而且會比 afterEmit 晚一些
compiler.hooks.afterEmit.tap('HTMLPlugin', (compilation) => {
// 2. 根據模板讀取html文件內容
const result = fs.readFileSync(this.options.template, 'utf-8')
// 3. 使用 cheerio 來分析 HTML
let $ = cheerio.load(result)
// 4. 創建 script 標簽后插入HTML中
Object.keys(compilation.assets).forEach(item => {
$(`<script src="/${item}"></script>`).appendTo('body')
})
// 5. 轉換成新的HTML並寫入到 dist 目錄中
fs.writeFileSync(path.join(process.cwd(), 'dist', this.options.filename), $.html())
})
}
}
module.exports = HTMLPlugin
- 注意 Compiler 和 Compilattion 的區別
- compile: r對象表示不變的webpack環境,是針對webpack的
- compilation: 對象針對的是隨時可變的項目文件,只要文件有改動,compilation就會被重新創建
添加plugin功能
為webpack-theory
添加plugin
功能,只需在Compiler構造時,創建對應的鈎子即可,webpack-theory
只是負責定義鈎子,並在適當的時間節點去觸發,至於鈎子的事件注冊都是各個plugin
自己內部去實現。
// tapable 的構造函數內部定義的鈎子
this.hooks = {
afterPlugins: new SyncHook(),
beforeRun: new SyncHook(),
run: new SyncHook(),
make: new SyncHook(),
afterCompiler: new SyncHook(),
shouldEmit: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(['compilation']),
done: new SyncHook(),
}
// 觸發plugins中所有插件的apply方法, 並傳入Compiler對象
if(Array.isArray(this.config.plugins)){
this.config.plugins.forEach(plugin => {
plugin.apply(this)
});
}
在合適的時機調用對應鈎子的call方法即可,如需要傳入參數,可以在對應的鈎子中定義好需要傳入的參數,call時直接傳入
在 start
中調用定義的鈎子
start() {
this.hooks.compiler.call() // 開始編譯
this.depAnalyse(path.resolve(this.root, this.entry))
this.hooks.afterCompiler.call() //編譯完成了
this.hooks.emit.call() // 開始發射文件
this.emitFile()
this.hooks.done.call() // 完成
}
補充
AST
就是將一行代碼解析成對象的格式,可以使用在線工具生成ast語法樹 astexplorer 進行查看
- 安裝@babel/parser插件
npm i -S @babel/parser
- 使用
const parser = require('@babel/parser')
//source是需要生成ast語法樹的代碼片段
const ast = parser.parse(source)
- 生成效果
源碼
const news = require('./news')
console.log(news.content)
生成的ast語法樹
Node {
type: 'File',
start: 0,
end: 57,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 3, column: 25 } },
program:
Node {
type: 'Program',
start: 0,
end: 57,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'script',
interpreter: null,
body: [ [Node], [Node] ],
directives: [] },
comments: [] }
tabable
在webpack內部實現事件流機制的核心就在於tapable,有了它就可以通過事件流的形式,將各個插件串聯起來,tapable類似於node中的events庫,核心原理就是一個訂閱發布模式
-
基本用法
- 定義鈎子
- 使用者注冊事件
- 在合適的階段調用鈎子,觸發事件
const { SyncHook } = require('tapable') /** * 學習前端 * 學習過程 1.准備 2.學html 3.學css 4.學js 5.學框架 * 學習的每個過程就類似於生命周期 */ class Frontend{ constructor(){ // 1. 定義生命周期鈎子 this.hooks = { beforeStudy: new SyncHook(), afterHtml: new SyncHook(), afterCss: new SyncHook(), afterJs: new SyncHook(), // 需要傳遞的參數 需要在 new SyncHook() 的時候定義好 afterFrame: new SyncHook(['name']), } } study(){ // 3. 在合適的時候 調用 console.log('准備'); this.hooks.beforeStudy.call() console.log('准備學html'); this.hooks.afterHtml.call() console.log('准備學css'); this.hooks.afterCss.call() console.log('准備學js'); this.hooks.afterJs.call() console.log('准備學框架'); this.hooks.afterFrame.call('vue、react') } } const f = new Frontend() // 2. 注冊事件 f.hooks.afterHtml.tap('afterHtml', () => { console.log('學完html,完成了一部分前端知識'); }) f.hooks.afterJs.tap('afterJs', () => { console.log('學完js,完成了一部分前端知識'); }) f.hooks.afterFrame.tap('afterFrame', (name) => { console.log(`學完了${name}框架,天下無敵....`); }) f.study()