因為換了個工作,所以博客停了一段時間。 這是上個月留下來的坑,webpack的源碼已經不太想看了,又臭又長,噁心的要死,想去看node的源碼……總之先補完這個 上一節完成了babel-loader對JS文件字元串的轉換,最後返回後進入如下代碼: 在看這個parse方法之前,需要過一下參數,首先是這個 ...
因為換了個工作,所以博客停了一段時間。
這是上個月留下來的坑,webpack的源碼已經不太想看了,又臭又長,噁心的要死,想去看node的源碼……總之先補完這個
上一節完成了babel-loader對JS文件字元串的轉換,最後返回後進入如下代碼:
// NormalModule.js build(options, compilation, resolver, fs, callback) { // ... return this.doBuild(options, compilation, resolver, fs, (err) => { // ... // 進入這裡 try { this.parser.parse(this._source.source(), { current: this, module: this, compilation: compilation, options: options }); } catch (e) { const source = this._source.source(); const error = new ModuleParseError(this, source, e); this.markModuleAsErrored(error); return callback(); } return callback(); }); }
在看這個parse方法之前,需要過一下參數,首先是這個source方法。
這個_source並不是轉換後的字元串,而是進行一層封裝的對象,source是其原型方法,源碼如下:
class OriginalSource extends Source { // value => babel-loader轉換後的字元串 // name => 'D:\\workspace\\node_modules\\babel-loader\\lib\\index.js!D:\\workspace\\doc\\input.js' constructor(value, name) { super(); this._value = value; this._name = name; } source() { return this._value; } //... }
有病啊!還是返回了轉換後的字元串。
parser.parse
這個parser的parse方法有兩部分,如下:
parse(source, initialState) { let ast; const comments = []; // 這一部分負責解析源代碼字元串 for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) { // ... } // 這裡再次進行嘗試 if (!ast) { // ... } if (!ast || typeof ast !== "object") throw new Error("Source couldn't be parsed"); // code... // 這裡傳入parse後的ast觸發parse的事件流 if (this.applyPluginsBailResult("program", ast, comments) === undefined) { this.prewalkStatements(ast.body); this.walkStatements(ast.body); } // code... return state; }
先不用管這裡POSSIBLE_AST_OPTIONS是啥,總之這裡做了兩件事情
1、對返回的字元串再做一次parse
2、將得到的ast作為參數觸發program事件流
一個一個來,首先是parse代碼塊,如下:
/* const POSSIBLE_AST_OPTIONS = [ { ranges: true, locations: true, ecmaVersion: ECMA_VERSION, sourceType: "module", plugins: { dynamicImport: true } }, { ranges: true, locations: true, ecmaVersion: ECMA_VERSION, sourceType: "script", plugins: { dynamicImport: true } } ]; */ // 抽象語法樹 let ast; // 註釋數組 const comments = []; for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) { if (!ast) { try { comments.length = 0; POSSIBLE_AST_OPTIONS[i].onComment = comments; // 傳入JS字元串與本地預設配置參數 ast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]); } catch (e) { // ignore the error } } }
這裡引入了別的模塊acorn來解析字元串,負責將JS字元串解析成抽象語法樹。
這裡不關心解析的過程,假設解析完成,簡單看一下一個JS文件源碼與解析後的樹結構:
源碼
const t = require('./module.js');
t.fn();
抽象語法樹
{ "type": "Program", "start": 0, "end": 41, "body": [{ "type": "VariableDeclaration", "start": 0, "end": 33, "declarations": [{ "type": "VariableDeclarator", "start": 6, "end": 32, "id": { "type": "Identifier", "start": 6, "end": 7, "name": "t" }, "init": { "type": "CallExpression", "start": 10, "end": 32, "callee": { "type": "Identifier", "start": 10, "end": 17, "name": "require" }, "arguments": [{ "type": "Literal", "start": 18, "end": 31, "value": "./module.js", "raw": "'./module.js'" }] } }], "kind": "const" }, { "type": "ExpressionStatement", "start": 34, "end": 41, "expression": { "type": "CallExpression", "start": 34, "end": 40, "callee": { "type": "MemberExpression", "start": 34, "end": 38, "object": { "type": "Identifier", "start": 34, "end": 35, "name": "t" }, "property": { "type": "Identifier", "start": 36, "end": 38, "name": "fn" }, "computed": false }, "arguments": [] } }], "sourceType": "script" }
這裡涉及到一個抽象語法樹的規則,詳情可見https://github.com/estree/estree
接下來會調用Parser上的program事件流,定義地點如下:
// WebpackOptionsApply.js compiler.apply( // 1 new HarmonyModulesPlugin(options.module), // 2 new UseStrictPlugin(), );
地方不好找,總之一個一個過:
HarmonyModulesPlugin
// HarmonyModulesPlugin.js => HarmonyDetectionParserPlugin.js parser.plugin("program", (ast) => { // 這裡對Import/Export的表達式進行檢索 const isHarmony = ast.body.some(statement => { return /^(Import|Export).*Declaration$/.test(statement.type); }); if(isHarmony) { const module = parser.state.module; const dep = new HarmonyCompatibilityDependency(module); dep.loc = { start: { line: -1, column: 0 }, end: { line: -1, column: 0 }, index: -2 }; // 如果存在就對該模塊進行特殊標記處理 module.addDependency(dep); module.meta.harmonyModule = true; module.strict = true; module.exportsArgument = "__webpack_exports__"; } });
這裡的正則可以參考https://github.com/estree/estree/blob/master/es2015.md的Modules部分說明,簡單講就是檢索JS中是否出現過Import * from *、Export default *等等。
如果存在會對該模塊進行標記。
UseStrictPlugin
// UseStrictPlugin.js parser.plugin("program", (ast) => { const firstNode = ast.body[0]; // 檢測頭部是否有'use strict'字元串 if(firstNode && firstNode.type === "ExpressionStatement" && firstNode.expression.type === "Literal" && firstNode.expression.value === "use strict") { // Remove "use strict" expression. It will be added later by the renderer again. // This is necessary in order to not break the strict mode when webpack prepends code. // @see https://github.com/webpack/webpack/issues/1970 const dep = new ConstDependency("", firstNode.range); dep.loc = firstNode.loc; parserInstance.state.current.addDependency(dep); parserInstance.state.module.strict = true; } });
這個就比較簡單了,判斷JS是否是嚴格模式,然後做個標記。
事件流走完,parse方法也就調用完畢,接下來調用build方法的callback,一路回到了Compilation類的buildModule方法。
// Compilation.js buildModule(module, optional, origin, dependencies, thisCallback) { this.applyPlugins1("build-module", module); if(module.building) return module.building.push(thisCallback); const building = module.building = [thisCallback]; function callback(err) { module.building = undefined; building.forEach(cb => cb(err)); } module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, (error) => { // 處理錯誤與警告 const errors = module.errors; for(let indexError = 0; indexError < errors.length; indexError++) { // ... } const warnings = module.warnings; for(let indexWarning = 0; indexWarning < warnings.length; indexWarning++) { // ... } module.dependencies.sort(Dependency.compare); // 事件流不存在 if(error) { this.applyPlugins2("failed-module", module, error); return callback(error); } this.applyPlugins1("succeed-module", module); return callback(); });
這裡對模塊解析後的警告與錯誤進行處理,根據是否有錯誤走兩個不同的事件流,然後觸發callback。
這裡神神秘秘的搞個callback函數,還forEach,往上面一看,傻了吧唧的就是強行給外部callback參數弄成數組,實際上就是調用了thisCallback。
現在總算摸清了callback的套路,就跟this一樣,誰調用就找誰,於是這個callback回到了Compilation的_addModuleChain函數的尾部:
// Compilation.js _addModuleChain(context, dependency, onModule, callback) { // ... this.semaphore.acquire(() => { moduleFactory.create({ // ... }, (err, module) => { // ... // 從這裡出來 this.buildModule(module, false, null, null, (err) => { if(err) { this.semaphore.release(); return errorAndCallback(err); } // 這屬性就是個計時器 // 計算從讀取模塊內容到構建完模塊的時間 if(this.profile) { const afterBuilding = Date.now(); module.profile.building = afterBuilding - afterFactory; } moduleReady.call(this); }); function moduleReady() { this.semaphore.release(); // 跳入下一個階段 this.processModuleDependencies(module, err => { if(err) { return callback(err); } return callback(null, module); }); } }); }); }
至此,模塊的構建基本完成,先到這裡吧……