這裡的編譯前指的是開始觸發主要的事件流this-compilaiton、compilation之前,由於還有一些準備代碼,這一節全部弄出來。 模塊基本上只走構造函數,具體的方法調用的時候再具體講解。 上一節NormalModuleFactory模塊的構造函數中,在處理完rules後,註入兩個事件流就 ...
這裡的編譯前指的是開始觸發主要的事件流this-compilaiton、compilation之前,由於還有一些準備代碼,這一節全部弄出來。
模塊基本上只走構造函數,具體的方法調用的時候再具體講解。
上一節NormalModuleFactory模塊的構造函數中,在處理完rules後,註入兩個事件流就結束了,所以可以回到如下代碼:
createNormalModuleFactory() { // 構造完成 const normalModuleFactory = new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {}); // 該事件流預設是沒有的 this.applyPlugins("normal-module-factory", normalModuleFactory); return normalModuleFactory; }
由於沒有對應的事件流,所以會回到params參數的構建:
newCompilationParams() { const params = { // done normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: [] }; return params; }
這裡的contextModuleFactory模塊並沒有任何初始化好講的,簡單貼一下代碼跳過:
createContextModuleFactory() { const contextModuleFactory = new ContextModuleFactory(this.resolvers, this.inputFileSystem); this.applyPlugins("context-module-factory", contextModuleFactory); return contextModuleFactory; } class ContextModuleFactory extends Tapable { constructor(resolvers) { super(); this.resolvers = resolvers; }; // methods... }
完成了params對象的構建後,會回到compile函數,繼續觸發其餘的事件流:
compile(callback) { const params = this.newCompilationParams(); // 暫無 this.applyPluginsAsync("before-compile", params, err => { if (err) return callback(err); // 暫無 this.applyPlugins("compile", params); const compilation = this.newCompilation(params); this.applyPluginsParallel("make", compilation, err => { /**/ }); }); }
然而在打包中,'before-compile'、'compile'事件流是空的,代碼會繼續下麵一行:
const compilation = this.newCompilation(params);
這行代碼源碼如下:
createCompilation() { return new Compilation(this); } newCompilation(params) { // 生成一個Compilation類 const compilation = this.createCompilation(); // 空對象{} compilation.fileTimestamps = this.fileTimestamps; // 空對象{} compilation.contextTimestamps = this.contextTimestamps; // undefined compilation.name = this.name; // 空對象{} compilation.records = this.records; // 空數組[] compilation.compilationDependencies = params.compilationDependencies; // 編譯 this.applyPlugins("this-compilation", compilation, params); this.applyPlugins("compilation", compilation, params); return compilation; }
除去第一行代碼,其餘的都是進行屬性掛載,在初次打包時都是空的對象或數組,所以作用也看不出,後面再進行詳解。
Compilation
剩餘的就是Compilation對象的實例化,只過構造函數,源碼如下:
class Compilation extends Tapable { constructor(compiler) { super(); // 獲取配置屬性 this.compiler = compiler; this.resolvers = compiler.resolvers; this.inputFileSystem = compiler.inputFileSystem; const options = this.options = compiler.options; this.outputOptions = options && options.output; this.bail = options && options.bail; this.profile = options && options.profile; this.performance = options && options.performance; // 引入模板解析模塊 this.mainTemplate = new MainTemplate(this.outputOptions); this.chunkTemplate = new ChunkTemplate(this.outputOptions); this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(this.outputOptions); this.moduleTemplate = new ModuleTemplate(this.outputOptions); this.semaphore = new Semaphore(options.parallelism || 100); this.entries = []; // ...其餘屬性初始化 }; // methods... }
構造函數中主要有三塊內容:
1、獲取配置文件中的一些屬性
2、引入模板解析的輔助模塊
3、初始化其餘本地屬性
配置文件的屬性獲取與初始化本地屬性跳過,這裡簡單過一下幾個輔助模塊,分別為:MainTemplate、ChunkTemplate、HotUpdateChunkTemplate、ModuleTemplate,從名字應該能看出作用了吧?
MainTemplate
源碼簡化如下:
// require function shortcuts: // __webpack_require__.s = the module id of the entry point // __webpack_require__.c = the module cache // __webpack_require__.m = the module functions // __webpack_require__.p = the bundle public path // __webpack_require__.i = the identity function used for harmony imports // __webpack_require__.e = the chunk ensure function // __webpack_require__.d = the exported propery define getter function // __webpack_require__.o = Object.prototype.hasOwnProperty.call // __webpack_require__.n = compatibility get default export // __webpack_require__.h = the webpack hash // __webpack_require__.oe = the uncatched error handler for the webpack runtime // __webpack_require__.nc = the script nonce class MainTemplate extends Template { constructor(outputOptions) { super(outputOptions); this.plugin("startup", (source, chunk, hash) => { /**/ }); this.plugin("render", (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => { /**/ }); this.plugin("local-vars", (source, chunk, hash) => { /**/ }); this.plugin("require", (source, chunk, hash) => { /**/ }); this.plugin("module-obj", (source, chunk, hash, varModuleId) => { /**/ }); this.plugin("require-extensions", (source, chunk, hash) => { /**/ }); this.requireFn = "__webpack_require__"; }; render(hash, chunk, moduleTemplate, dependencyTemplates) { /**/ } renderRequireFunctionForModule(hash, chunk, varModuleId) { /**/ } renderAddModule(hash, chunk, varModuleId, varModule) { /**/ } renderCurrentHashCode(hash, length) { /**/ } entryPointInChildren(chunk) { /**/ } getPublicPath(options) { /**/ } updateHash(hash) { /**/ } updateHashForChunk(hash, chunk) { /**/ } useChunkHash(chunk) { /**/ } };
註意頭部的註釋,這裡解釋了打包後文件中模塊生成函數__webpack_require__上各個參數的意義,屬於Template的主模塊。
最後的requireFn就是那個函數的名字,如果改了打包後文件的名字也會變哦,演示一下:
執行一下webpack指令,會看到打包文件如下:
還挺好玩的。
ChunkTemplate
class ChunkTemplate extends Template { constructor(outputOptions) { super(outputOptions); }; render(chunk, moduleTemplate, dependencyTemplates) {/**/}; updateHash(hash) {/**/}; updateHashForChunk(hash, chunk) {/**/}; };
HotUpdateChunkTemplate
class HotUpdateChunkTemplate extends Template { constructor(outputOptions) { super(outputOptions); }; render(id, modules, removedModules, hash, moduleTemplate, dependencyTemplates) { /**/ }; updateHash(hash) { /**/ }; };
ModuleTemplate
class ModuleTemplate extends Template { constructor(outputOptions) { super(outputOptions); } render(module, dependencyTemplates, chunk) { /**/ } updateHash(hash) { /**/ } };
這三個模塊都是輔助用,簡單看一下方法有個印象就行了。
可以註意到4個模塊都有一個爹,叫做Template。
這個模塊屬於純工具模塊,內部定義了大量的靜態方法以便操作字元串,這裡講一下得了,不然這節沒啥內容,源碼整理後如下:
"use strict"; const Tapable = require("tapable"); const ConcatSource = require("webpack-sources").ConcatSource; const START_LOWERCASE_ALPHABET_CODE = "a".charCodeAt(0); // ...其餘常量定義 module.exports = class Template extends Tapable { constructor(outputOptions) { super(); this.outputOptions = outputOptions || {}; }; static getFunctionContent(fn) { /**/ }; static toIdentifier(str) { /**/ }; static toPath(str) { /**/ }; static numberToIdentifer(n) { /**/ }; indent(str) { /**/ } prefix(str, prefix) { /**/ } asString(str) { /**/ } getModulesArrayBounds(modules) { /**/ } renderChunkModules(chunk, moduleTemplate, dependencyTemplates, prefix) { /**/ } }; function stringifyIdSortPredicate(a, b) { /**/ } function moduleIdIsNumber(module) { /**/ }
內部主要包括四部分:
1、常量定義
2、靜態方法
3、工具方法
4、內部函數
一個一個來。
常量const
// a的Unicode編碼 => 97 const START_LOWERCASE_ALPHABET_CODE = "a".charCodeAt(0); // A的Unicode編碼 => 65 const START_UPPERCASE_ALPHABET_CODE = "A".charCodeAt(0); // 122 - 97 + 1 = 26 返回字母數量 const DELTA_A_TO_Z = "z".charCodeAt(0) - START_LOWERCASE_ALPHABET_CODE + 1; // 匹配函數內容 const FUNCTION_CONTENT_REGEX = /^function\s?\(\)\s?\{\n?|\n?\}$/g; // 全行匹配tab製表符 const INDENT_MULTILINE_REGEX = /^\t/mg; // 匹配非大小寫字母$_開頭 const IDENTIFIER_NAME_REPLACE_REGEX = /^[^a-zA-Z$_]/; // 匹配非大小寫字母數字$_ const IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX = /[^a-zA-Z0-9$_]/g; // 神奇的字元 已經不知道匹配的是啥了 const PATH_NAME_NORMALIZE_REPLACE_REGEX = /[^a-zA-Z0-9_!§$()=\-^°]+/g; // 匹配以-開始或結尾 const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g;
大部分都非常簡單,有兩個比較特殊,一個是匹配函數內容,這個需要配合靜態方法來講解。
另一個就是PATH_NAME_NORMALIZE_REPLACE_REGEX,裡面有幾個字元可能這輩子在代碼里都不會看到吧……
靜態方法
class Template extends Tapable { constructor(outputOptions) { /**/ }; // 抽取函數內容 static getFunctionContent(fn) { return fn.toString().replace(FUNCTION_CONTENT_REGEX, "").replace(INDENT_MULTILINE_REGEX, ""); }; // 頭部數字、所有的特殊字元置換為'_' static toIdentifier(str) { if (typeof str !== "string") return ""; return str.replace(IDENTIFIER_NAME_REPLACE_REGEX, "_").replace(IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX, "_"); }; // 特殊字元置換為'-' // 去掉頭尾的'-' static toPath(str) { if (typeof str !== "string") return ""; return str.replace(PATH_NAME_NORMALIZE_REPLACE_REGEX, "-").replace(MATCH_PADDED_HYPHENS_REPLACE_REGEX, ""); }; // 數字轉換為字母 static numberToIdentifer(n) { // n ∈ [0,26) 返回 a-z if (n < DELTA_A_TO_Z) return String.fromCharCode(START_LOWERCASE_ALPHABET_CODE + n); // n ∈ [26,52) 返回 A-Z n -= DELTA_A_TO_Z; if (n < DELTA_A_TO_Z) return String.fromCharCode(START_UPPERCASE_ALPHABET_CODE + n); // n ∈ [52,正無窮大) 返回 '_ + (n-52)' n -= DELTA_A_TO_Z; return "_" + n; } }
也非常的簡單,這裡每一個都舉個小例子:
1、getFunctionContent
const fn1 = function() { console.log(1); }; // console.log(1) const result = Template.getFunctionContent(fn1.toString());
這個方法就是抽取出函數內容,註意,必須是函數表達式,使用箭頭函數或者具名函數將失效。
const fn1 = () => { console.log(1); }; /* 失敗 () => { console.log(1); } */ const result = Template.getFunctionContent(fn1.toString());
const fn1 = function fn1() { console.log(1); }; /* 失敗 function fn1() { console.log(1); } */ const result = Template.getFunctionContent(fn1.toString());
2、toIdentifier
// __1_2_3_4_5_6 const result = Template.toIdentifier('0/1.2,3;4[5]6');
3、toPath
// d-workspace-doc const result = Template.toPath('d://workspace//doc//');
4、numberToIdentifer
// a const result = Template.numberToIdentifer(0); // E const result2 = Template.numberToIdentifer(30); // _48 const result3 = Template.numberToIdentifer(100);
簡單易懂,包教包會!
工具方法
class Template { indent(str) { // 數組map處理 if (Array.isArray(str)) { return str.map(this.indent.bind(this)).join("\n"); } else { // 去除右空白 實驗性方法 str = str.trimRight(); if (!str) return ""; var ind = (str[0] === "\n" ? "" : "\t"); // 兩個製表符 return ind + str.replace(/\n([^\n])/g, "\n\t$1"); } } prefix(str, prefix) { // 傳入數組換行拼接 if (Array.isArray(str)) { str = str.join("\n"); } // 去除兩側空白 str = str.trim(); if (!str) return ""; const ind = (str[0] === "\n" ? "" : prefix); // 加首碼 return ind + str.replace(/\n([^\n])/g, "\n" + prefix + "$1"); } asString(str) { // 數組換行拼接或返回原字元串 if (Array.isArray(str)) { return str.join("\n"); } return str; } }
前三個比較簡單,直接看源碼就懂了。
1、indent
這個方法簡單講解就是把每行往後推兩個製表符,如果傳入字元數組則如下所示:
const tmp = new Template(); const str = ['a', 'b', 'c']; /* a b c */ const result = tmp.indent(str);
2、prefix
簡單講就是給字元串加首碼:
const tmp = new Template(); // -a const result = tmp.prefix(`a`, `-`);
3、asString
傳入數組會分別進行換行拼接,非數組直接返回:
const tmp = new Template(); /* a b c */ const result = tmp.asString(['a', 'b', 'c']);
4、getModulesArrayBounds
Template.prototype.getModulesArrayBounds = (modules) => { // typeof module.id === 'number' if (!modules.every(moduleIdIsNumber)) return false; var maxId = -Infinity; var minId = Infinity; // 獲取modules中的最大與最小id // 一個模塊對應一個id modules.forEach(function(module) { if (maxId < module.id) maxId = module.id; if (minId > module.id) minId = module.id; }); // 當最小id小於16 + 位數 置0 if (minId < 16 + ("" + minId).length) { // add minId x ',' instead of 'Array(minId).concat(...)' minId = 0; } // map返回每一個module.id位數+2 // reduce將數組元素相加 起始值為-1 var objectOverhead = modules.map(function(module) { var idLength = (module.id + "").length; return idLength + 2; }).reduce(function(a, b) { return a + b; }, -1); // 這裡的應用到實際調用的時候再看吧 var arrayOverhead = minId === 0 ? maxId : 16 + ("" + minId).length + maxId; return arrayOverhead < objectOverhead ? [minId, maxId] : false; }
這個函數並不複雜,但是不懂是如何應用的。
5、renderChunkModules
這個沒法單獨講,調用的時候做講解。
內部函數
// 這是按字元串排序 // 即 2 > 11 function stringifyIdSortPredicate(a, b) { var aId = a.id + ""; var bId = b.id + ""; if (aId < bId) return -1; if (aId > bId) return 1; return 0; } // 這個工具方法有使用 function moduleIdIsNumber(module) { return typeof module.id === "number"; }
比較簡單。
至此,該模塊內容基本完事,有一個方法需要在用的時候做解析。
接下來幾節就將正式進入編譯階段。