webpack 編譯流程
- 初始化參數(shù):從配置文件和 Shell 語句中讀取并合并參數(shù),得出最終的配置對象
- 用上一步得到的參數(shù)初始化 Compiler 對象
- 加載所有配置的插件
- 執(zhí)行對象的 run 方法開始執(zhí)行編譯
- 根據(jù)配置中的entry找出入口文件
- 從入口文件出發(fā),調(diào)用所有配置的Loader對模塊進行編譯
- 再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
- 根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk
- 再把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表
- 在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)
在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監(jiān)聽到感興趣的事件后會執(zhí)行特定的邏輯,并且插件可以調(diào)用 Webpack 提供的 API 改變 Webpack 的運行結(jié)果
1.1entry?
srcentry1.js
let title = require("./title")
console.log("entry12", title)
srcentry2.js
let title = require("./title.js")
console.log("entry2", title)
srctitle.js
module.exports = "title"
1.2loader.js?
- loader 的本質(zhì)就是一個函數(shù),一個用于轉(zhuǎn)換或者說翻譯的函數(shù)
- 把那些 webpack 不認識的模塊 less sass baxx 轉(zhuǎn)換為 webpack 能認識的模塊 js json
loaderslogger1-loader.js
function loader1(source) {
//let name= 'entry1';
return source + "//logger1" //let name= 'entry1';//logger1
}
module.exports = loader1
loaderslogger2-loader.js
function loader2(source) {
//let name= 'entry1';
return source + "//logger2" //let name= 'entry1';//logger2
}
module.exports = loader2
1.3 plugin.js?
pluginsdone-plugin.js
class DonePlugin {
Apply(compiler) {
compiler.hooks.done.tap("DonePlugin", () => {
console.log("done:結(jié)束編譯")
})
}
}
module.exports = DonePlugin
pluginsrun1-plugin.js
class RunPlugin {
apply(compiler) {
//在此插件里可以監(jiān)聽run這個鉤子
compiler.hooks.run.tap("Run1Plugin", () => {
console.log("run1:開始編譯")
})
}
}
module.exports = RunPlugin
pluginsrun2-plugin.js
class RunPlugin {
apply(compiler) {
compiler.hooks.run.tap("Run2Plugin", () => {
console.log("run2:開始編譯")
})
}
}
module.exports = RunPlugin
1.4 webpack.config.js?
webpack.config.js
const path = require("path")
const Run1Plugin = require("./plugins/run1-plugin")
const Run2Plugin = require("./plugins/run2-plugin")
const DonePlugin = require("./plugins/done-plugin")
module.exports = {
mode: "development",
devtool: false,
context: process.cwd,
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
resolve: {
extensions: [".js", ".jsx", ".tx", ".tsx"],
},
module: {
rules: [
{
test: /.js$/,
use: [path.resolve(__dirname, "loaders/loader2.js"), path.resolve(__dirname, "loaders/loader1.js")],
},
],
},
plugins: [new DonePlugin(), new Run2Plugin(), new Run1Plugin()],
}
1.5debugger.js?
debugger.js
const fs = require("fs")
const webpack = require("./webpack2")
// const webpack = require("webpack")
const webpackConfig = require("./webpack.config")
debugger
const compiler = webpack(webpackConfig)
//4.執(zhí)行`Compiler`對象的 run 方法開始執(zhí)行編譯
compiler.run((err, stats) => {
if (err) {
console.log(err)
} else {
//stats代表統(tǒng)計結(jié)果對象
const statsJson = JSON.stringify(
stats.toJson({
// files: true, //代表打包后生成的文件
assets: true, //其實是一個代碼塊到文件的對應(yīng)關(guān)系
chunks: true, //從入口模塊出發(fā),找到此入口模塊依賴的模塊,或者依賴的模塊依賴的模塊,合在一起組成一個代碼塊
modules: true, //打包的模塊 每個文件都是一個模塊
})
)
fs.writeFileSync("./statsJson.json", statsJson)
}
})
1.6 webpack.js?
webpack2.js
const Compiler = require("./Compiler")
function webpack(options) {
// 1.初始化參數(shù):從配置文件和 Shell 語句中讀取并合并參數(shù),得出最終的配置對象
//argv[0]是Node程序的絕對路徑 argv[1] 正在運行的腳本
// node debugger --mode=production
const argv = process.argv.slice(2)
const shellOptions = argv.reduce((shellOptions, options) => {
// options = '--mode=development'
const [key, value] = options.split("=")
shellOptions[key.slice(2)] = value
return shellOptions
}, {})
console.log("shellOptions=>", shellOptions)
const finalOptions = { ...options, ...shellOptions }
//2.用上一步得到的參數(shù)初始化 `Compiler` 對象
const compiler = new Compiler(finalOptions)
//3.加載所有配置的插件
const { plugins } = finalOptions
for (let plugin of plugins) {
//訂閱鉤子
plugin.apply(compiler)
}
return compiler
}
module.exports = webpack
1.7 Compilation?
Compiler.js
const { SyncHook } = require("tapable")
const Compilation = require("./Compilation")
const fs = require("fs")
const path = require("path")
// Compiler 模塊是 webpack 的主要引擎
class Compiler {
constructor(options) {
this.options = options
this.hooks = {
run: new SyncHook(), //在開始編譯之前調(diào)用
done: new SyncHook(), //在編譯完成時執(zhí)行
}
}
run(callback) {
this.hooks.run.call() //在編譯開始前觸發(fā)run鉤子執(zhí)行
//在編譯的過程中會收集所有的依賴的模塊或者說文件
//stats指的是統(tǒng)計信息 modules chunks files=bundle assets指的是文件名和文件內(nèi)容的映射關(guān)系
const onCompiled = (err, stats, fileDependencies) => {
// 10.在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)
for (let filename in stats.assets) {
let filePath = path.join(this.options.output.path, filename)
fs.writeFileSync(filePath, stats.assets[filename], "utf8")
}
callback(err, { toJson: () => stats })
for (let fileDependency of fileDependencies) {
//監(jiān)聽依賴的文件變化,如果依賴的文件變化后會開始一次新的編譯
fs.watch(fileDependency, () => this.compile(onCompiled))
}
}
this.hooks.done.call() //在編譯完成時觸發(fā)done鉤子執(zhí)行
//調(diào)用compile方法進行編譯 開始一次新的編譯
this.compile(onCompiled)
}
//開啟一次新的編譯
compile(callback) {
//每次編譯 都會創(chuàng)建一個新的Compilation實例
let compilation = new Compilation(this.options, this)
compilation.build(callback)
}
}
module.exports = Compiler
1.8 Compilation?
Compilation.js
const path = require("path")
const fs = require("fs")
const parser = require("@babel/parser")
const types = require("@babel/types")
const traverse = require("@babel/traverse").default
const generator = require("@babel/generator").default
const baseDir = normalizePath(process.cwd())
function normalizePath(path) {
return path.replace(/\/g, "/")
}
class Compilation {
constructor(options, compiler) {
this.options = options // 配置參數(shù)
this.options.context = this.options.context || normalizePath(process.cwd())
this.compiler = compiler
this.modules = [] //這里放置本次編譯涉及的所有的模塊
this.chunks = [] //本次編譯所組裝出的代碼塊
this.assets = {} // 存放輸出的文件 key是文件名,值是文件內(nèi)容
this.files = [] //代表本次打包出來的文件
this.fileDependencies = new Set() //本次編譯依賴的文件或者說模塊
}
build(callback) {
//5.根據(jù)配置中的entry找出入口文件
let entry = {}
//格式化入口文件
if (typeof this.options.entry === "string") {
entry.main = this.options.entry
} else {
entry = this.options.entry
}
// 對入口進行遍歷
for (let entryName in entry) {
//獲取入口文件的絕對路徑
let entryFilePath = path.posix.join(baseDir, entry[entryName])
//把此入口文件添加到文件依賴列表中
this.fileDependencies.add(entryFilePath)
//6.從入口文件出發(fā),調(diào)用所有配置的Loader對模塊進行編譯
let entryModule = this.buildModule(entryName, entryFilePath)
// this.modules.push(entryModule)
// 8.根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk
let chunk = {
name: entryName, //入口名稱
entryModule, //入口的模塊 ./src/entry.js
modules: this.modules.filter((module) => module.names.includes(entryName)), //此入口對應(yīng)的模塊
}
this.chunks.push(chunk)
}
// 9.再把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表
this.chunks.forEach((chunk) => {
const filename = this.options.output.filename.replace("[name]", chunk.name)
this.files.push(filename)
//組裝chunk
this.assets[filename] = getSource(chunk)
})
callback(
null,
{
modules: this.modules,
chunks: this.chunks,
assets: this.assets,
files: this.files,
},
this.fileDependencies
)
}
/**
* 編譯模塊
* @param {*} name 模塊所屬的代碼塊(chunk)的名稱,也就是entry的name entry1 entry2
* @param {*} modulePath 模塊的絕對路徑
*/
buildModule(entryName, modulePath) {
//1.讀取文件的內(nèi)容
let rawSourceCode = fs.readFileSync(modulePath, "utf8")
//獲取loader的配置規(guī)則
let { rules } = this.options.module
//根據(jù)規(guī)則找到所有的匹配的loader 適用于此模塊的所有l(wèi)oader
let loaders = []
rules.forEach((rule) => {
//用模塊路徑匹配正則表達式
if (modulePath.match(rule.test)) {
loaders.push(...rule.use)
}
})
//調(diào)用所有配置的Loader對模塊進行轉(zhuǎn)換
let transformedSourceCode = loaders.reduceRight((sourceCode, loaderPath) => {
const loaderFn = require(loaderPath)
return loaderFn(sourceCode)
}, rawSourceCode)
//7.再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
//獲取當(dāng)前模塊,也就是 ./src/entry1.js的模塊ID
let moduleId = "./" + path.posix.relative(baseDir, modulePath)
//創(chuàng)建一個模塊ID就是相對于根目錄的相對路徑 dependencies就是此模塊依賴的模塊
//name是模塊所屬的代碼塊的名稱,如果一個模塊屬于多個代碼塊,那么name就是一個數(shù)組
let module = { id: moduleId, dependencies: new Set(), names: [entryName] }
this.modules.push(module)
let ast = parser.parse(transformedSourceCode, { sourceType: "module" })
//Visitor是babel插件中的概念,此處沒有
traverse(ast, {
CallExpression: ({ node }) => {
//如果調(diào)用的方法名是require的話,說明就要依賴一個其它模塊
if (node.callee.name === "require") {
// .代表當(dāng)前的模塊所有的目錄,不是工作目錄
let depModuleName = node.arguments[0].value //"./title"
let depModulePath
//獲取當(dāng)前的模塊所在的目錄
if (depModuleName.startsWith(".")) {
//暫時先不考慮node_modules里的模塊,先只考慮相對路徑
const currentDir = path.posix.dirname(modulePath)
//要找當(dāng)前模塊所有在的目錄下面的相對路徑
depModulePath = path.posix.join(currentDir, depModuleName)
//此絕對路徑可能沒有后續(xù),需要嘗試添加后綴
// 獲取配置的擴展名后綴
const extensions = this.options.resolve.extensions
//嘗試添加后綴 返回最終的路徑
depModulePath = tryExtensions(depModulePath, extensions)
} else {
//如果不是以.開頭的話,就是第三方模塊
depModulePath = require.resolve(depModuleName)
}
//把依賴的模塊路徑添加到文件依賴列表
this.fileDependencies.add(depModulePath)
//獲取此依賴的模塊的ID, 也就是相對于根目錄的相對路徑
let depModuleId = "./" + path.posix.relative(baseDir, depModulePath)
//修改語法樹,把依賴的模塊名換成模塊ID
node.arguments[0] = types.stringLiteral(depModuleId)
//把依賴的模塊ID和依賴的模塊路徑放置到當(dāng)前模塊的依賴數(shù)組中
module.dependencies.add({ depModuleId, depModulePath })
}
},
})
//轉(zhuǎn)換源代碼,把轉(zhuǎn)換后的源碼放在_source屬性,用于后面寫入文件
let { code } = generator(ast)
module._source = code
;[...module.dependencies].forEach(({ depModuleId, depModulePath }) => {
//判斷此依賴的模塊是否已經(jīng)打包過了或者說編譯 過了
let existModule = this.modules.find((module) => module.id === depModuleId)
if (existModule) {
existModule.names.push(entryName)
} else {
let depModule = this.buildModule(entryName, depModulePath)
this.modules.push(depModule)
}
})
return module
}
}
/**
*
* @param {*} modulePath
* @param {*} extensions
* @returns
*/
function tryExtensions(modulePath, extensions) {
if (fs.existsSync(modulePath)) {
return modulePath
}
for (let i = 0; i < extensions.length; i++) {
let filePath = modulePath + extensions[i]
if (fs.existsSync(filePath)) {
return filePath
}
}
throw new Error(`找不到${modulePath}`)
}
function getSource(chunk) {
return `
(() => {
var modules = {
${chunk.modules
.filter((module) => module.id !== chunk.entryModule.id)
.map(
(module) => `
"${module.id}": module => {
${module._source}
}
`
)
.join(",")}
};
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = cache[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
var exports = {};
(() => {
${chunk.entryModule._source}
})();
})();
`
}
module.exports = Compilation
webpack2.js
總結(jié)?
1. 文件作用?
webpack.js 文件?
webpack 方法
- 接收 webpack.config.js 參數(shù),返回 compiler 實例
- 初始化參數(shù)
- 始化 Compiler 對象實例
- 加載所有配置的插件
Compiler文件?
- Compiler 模塊是 webpack 的主要引擎
- constructor 方法: 初始化一些 hooks
- run 方法
- 執(zhí)行插件訂閱的一系列 hooks
- 創(chuàng)建 Compilation 實例并執(zhí)行實例的 build(onCompiled)方法(開啟一次新的編譯)
- onCompiled 回調(diào)在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)執(zhí)行 compiler.run 方法的回調(diào),傳入 info監(jiān)聽依賴的文件變化,如果依賴的文件變化后會開始一次新的編譯
Compilation 文件?
build 方法
- .根據(jù)配置中的 entry 找出入口文件
- 從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進行編譯
- 再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
- 根據(jù)入口和模塊之間的依賴關(guān)系,組裝成一個個包含多個模塊的 Chunk
- 再把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表
- 執(zhí)行成功后的回調(diào)
2. 流程總結(jié)?
初始化參數(shù):?
- 初始化參數(shù):從配置文件和 Shell 語句中讀取并合并參數(shù),得出最終的配置對象(命令行優(yōu)先級高)
開始編譯?
- 用上一步得到的參數(shù)初始化 Compiler 對象
- 初始化 options 參數(shù)和 hooks ( run: new SyncHook(), //在開始編譯之前調(diào)用...)
- 加載所有配置的插件:
- 在配置中找到 plugins 數(shù)組
- 遍歷 plugins 執(zhí)行每個插件的 apply 方法,并把 compiler 實例傳進去(每個插件都有一個 apply 方法)
- 執(zhí)行 compiler.hooks.run.tap等方法注冊事件
- 執(zhí)行compiler實例的 run 方法開始執(zhí)行編譯
- 整個過程伴隨著觸發(fā)插件的注冊個各種鉤子函數(shù) this.hooks.done.call()...
- 開啟一次新的編譯,創(chuàng)建一個新的 Compilation 實例
- 執(zhí)行實例的 build 方法,傳入完成的回調(diào)
編譯模塊?
- 根據(jù)配置中的 entry 找出入口文件
- 格式化入口文件,變成對象形式
- 對入口進行遍歷,獲取入口文件的絕對路徑,添加到文件依賴列表中
- loader 轉(zhuǎn)換:從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進行轉(zhuǎn)換 (最終返回 module 對象)
- 讀取處理文件的內(nèi)容
- 根據(jù)規(guī)則找到所有的匹配的 loader
- 調(diào)用所有配置的 Loader 對模塊進行轉(zhuǎn)換(從上到下,從右向左)
- 獲取當(dāng)前模塊模塊 id,相對于根目錄的相對路徑
- 創(chuàng)建一個 module 對象
- const module = {
id:'./src/entry1.js',//相對于根目錄的相對路徑
dependencies:[{depModuleId:./src/title.js,depModulePath:'xxx'}],//dependencies就是此模塊依賴的模塊
names:['entry1'],// name是模塊所屬的代碼塊的名稱,如果一個模塊屬于多個代碼塊,那么name就是一個數(shù)組
2.
_source:'xxx',//存放對應(yīng)的源碼
}
- 編譯模塊分析依賴,再遞歸遍歷本步驟直到所有入口依賴模塊的文件都經(jīng)過了本步驟的處理
- 將 loader 編譯后的代碼調(diào)用 parse 轉(zhuǎn)換為 ast
- 遍歷語法樹,如果存在 require 或者 import,說明就要依賴一個其它模塊
- 獲取依賴模塊的絕對路徑,添加到文件依賴列表中
- 獲取此依賴的模塊的 ID, 也就是相對于根目錄的相對路徑
- 修改語法樹,把依賴的模塊名換成模塊 ID
- 把依賴的模塊 ID 和依賴的模塊路徑放置到當(dāng)前模塊 module 的依賴數(shù)組中
- 調(diào)用 generator(ast),把轉(zhuǎn)換后的源碼放在 module._source 屬性,用于后面寫入文件
- 遍歷module.dependencies,遞歸構(gòu)建 module,構(gòu)建好的存儲到 this.modules 上,如果第二個入口也依賴該模塊,直接取用,只需要給該模塊的 name 屬性上添加上入口信息
輸出資源?
- 組裝 chuck 對象:
- 組裝
- const chuck = {
name: "entry1", //入口名稱
entryModule, //入口的模塊的module {id,name,dependencies,_source}
modules: [{}], // 入口依賴模塊的集合
}- this.chunks.push(chunk)
生成 bundle 文件?
- 把每個 Chunk 轉(zhuǎn)換成一個單獨的文件加入到輸出列表獲取要生成的文件名稱并把文件名添加到 this.files 中獲取文件內(nèi)容并給 this.assets 對象執(zhí)行 compilation.build 方法的回調(diào)
寫入文件?
- 在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)