Webpack源碼分析

文章源碼分析基於webpack2,參考文章

細說webpack之流程篇 - sharlly - 博客園

Webpack 概述 - 企業網站欣賞,HTML5模板下載,JQUERY特效,網頁素材下載

webpack作為一種流行的打包工具被廣泛應用在web項目的前端工程化構建中.單配置文件,各種插件形式的loader能很方便的把各種源碼構建成瀏覽器環境能運行的js代碼。webpack的構建流程是如何的?一個個獨立的依賴文件怎麼被整合成目標js的?源碼是何時被轉義成瀏覽器可運行標準的?我自己要定製打包過程從何入手?這篇文章就來解答這幾個問題。

0 Tapable.js與webpack核心路徑的實現

在我們寫好webpack.config.js文件後,terminal輸入webpack命令時,nodejs會調用node_moudule/webpack/webpack.js文件里的webpack方法,一切的構建從這裡開始。基本概念:webpack生成的最終js文件叫chunk,代碼塊的概念,單獨文件對應chunk里包含若干的小邏輯模塊叫module。主要的步驟如下:

compile

開始編譯 根據配置文件不同區分單入口或多入口

make

從入口點分析模塊,建立module對象,他依賴的其他module都寫入對應數組保存

build 構建模塊

(make中的一個步驟)build時調用runloader找到載入每種不同資源的loader進行處理,最後轉換成js後用arcon.js把源碼轉換成AST語法樹,再分析語法樹,提取依賴關係存入供下一步使用

seal 封裝構建結果

根據配置和之前分析的依賴關係分配代碼,把module粒度的代碼加入到對應的chunk(代碼塊)里,每一個chunk最後就對應一個輸出的目標js (提取公共js在這裡完成)

emit 輸出文件

把各個chunk輸出到結果文件,根據chunk類型的不同,使用不同的模版生成目標js

MainTemplate.js , 主文件模版,包括__webpack_require__在瀏覽器的實現

ChunkTemplate.js ,chunk文件模版 對拆分出來的chunk加包裝代碼

ModuleTemplate.js,module文件模版,對所有module加包裝代碼

HotUpdateChunkTemplate.js 熱替換模版,對所有熱替換模塊加包裝代碼

簡敘的流程邏輯就是這些,其中有許多步驟是使用訂閱/發布模型實現的。這種模型可以把像webpack這種細分子流程

非常多且繁瑣的程序拆分成一個個相對簡單的任務來實現。

Tapable.js

webpack的訂閱/發布實現是基於Tapable.js

Tapable這裡不再贅述,具體細節可以搜github,簡單說兩個基本方法:

Tapable.plugin(name,function(){ 實現代碼 })nvoid plugin(names: string|string[], handler: Function)n

plugin方法可以給觀察者系統註冊一個訂閱者函數

Tapable.applyPlugins(name,參數...)nvoid applyPlugins(name: string, args: any...) n

applyPlugins方法可以向觀察者系統發布一個通知,來調用對應的訂閱者來進行處理。

通過Tapable.js的拆分,我們看到webpack源碼中充滿了各種的訂閱和發布,這也十分便於我們理解和下手修改。

1 Compile過程 webpack參數傳入和啟動編譯

查看node_moudule/webpack/webpack.js源碼,

  1. webapck對options的參數進行了格式驗證處理,並摘出
  2. options中的plugin在訂閱者管理器中進行了註冊,以便後續流程調用。把驗證後的options傳給Compiler.js 調用run方法開始編譯。
  3. Compiler.js開始工作後,調用Complation.js中的實現來進行:建立模塊--loader處理--封裝結果--輸出文件幾個步驟

2 Make過程

Make是代碼分析的核心流程。他包括了創建模塊,構建模塊的工作。

而構建模塊步驟又包含了

  1. loader來處理資源
  2. 處理後的js進行AST轉換並分析語句
  3. 語句中發現的依賴關係再處理拆分成dep,即依賴module,形成依賴網

Make步驟的入口在訂閱者"make"的實現中,我們舉單入口構建的例子來看

compiler.plugin("make", (compilation, callback) => {ntt//module工廠函數nt const dep = n SingleEntryPlugin.createDependency(this.entry, this.name); ntt//開始構建!ntcompilation.addEntry(this.context, dep, this.name, callback);n});n

在這裡傳入了單入口構建型模塊的工廠函數給compilation的addEntry方法開始構建模塊。

進入compilation.addEntry方法後,核心步驟是通過

_addModuleChain() 來把處理的代碼加入module鏈中,以便給後續的封裝步驟使用

//根據addEntry傳入的工廠函數類型得到對應的工廠函數實體nconst moduleFactory = nthis.dependencyFactories.get(dependency.constructor);tnif(!moduleFactory) {n throw new Error(`No dependency factory available n for this dependency type: ${dependency.constructor.name}`);n}nn//找到對應的modue構建工廠創建modulenmoduleFactory.create({},(err, module) => {n //核心流程 處理源碼ntthis.buildModule();n //處理module建的依賴關係ntthis.processModuleDependencies();n })n

3 buildModule過程解析

在make過程中,最核心的就是buildModule流程,這個步驟主要有3個核心任務:

1、通過runLoaders函數找到對應的loader(css-loader,vue-loader,babel-loader...)處理源碼

//NormalModule.js里dobuild的實現ndoBuild(options, compilation, resolver, fs, callback) {n ...n //構建loader運行的上下文環境n const loaderContext = n this.createLoaderContext(resolver, options, compilation, fs);ntn n //環境,loader列表,源碼傳給runLoadersn runLoaders({n resource: this.resource,n loaders: this.loaders,n context: loaderContext,n readResource: fs.readFile.bind(fs)n } n

2 & 3、通過parser.parse方法,把源碼解析成AST樹,並且記錄源碼依賴關係

在上一步doBuild後的回調里,對已經轉化成js的文件進行了如下處理

this.parser.parse(this._source.source(), {ntttttcurrent: this,ntttttmodule: this,ntttttcompilation: compilation,ntttttoptions: optionsntttt});n//解析源文件的內容n

文件轉換AST以及語句的遍歷處理都在webpack/lib/parser.js中實現,簡單介紹幾個點,其他處理

讀者可以自行查看對應代碼

ast = acorn.parse(source, {nttttranges: true,nttttlocations: true,nttttecmaVersion: 2017,nttttsourceType: "module",nttttplugins: {ntttttdynamicImport: truentttt},nttttonComment: commentsnttt});n//使用acorn.js將源碼轉化nnnthis.walkStatements(ast.body);n//遍歷ast樹上的所有語句nnnparser.plugin("import", (statement, source) => {nconst dep =nnew HarmonyImportDependency(source, nHarmonyModulesHelpers.getNewModuleVar(parser.state, source), nstatement.range);ntttdep.loc = statement.loc;ntttparser.state.current.addDependency(dep);ntttparser.state.lastHarmonyImport = dep;ntttreturn true;ntt});n//遇到import ,require等語句時根據commonJS,HarmonyImport,AMD,UMD等不同語法對依賴關係進行解析記錄 n

4 seal過程 封裝代碼

當冗長耗時loader處理源碼,遍歷module依賴關係後,我們得到了一個巨大的AST樹結構的module map,

我們就要回到Complation對象中的seal方法來把代碼封成瀏覽器里運行的模塊了。

seal(){ntself.preparedChunks.forEach(preparedChunk => {ntttconst module = preparedChunk.module;ntttconst chunk = self.addChunk(preparedChunk.name, module);ntttconst entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);ntttentrypoint.unshiftChunk(chunk);nntttchunk.addModule(module);nttt//把module加入對應的chunk里,準備最後輸出成一個文件ntttmodule.addChunk(chunk);nttt//記錄最後含有module的chunk列表在一個數組中ntttchunk.entryModule = module;ntttself.assignIndex(module); nttt//給module賦值int型的moduleID 這就是最終源碼里的webpackJsonp([1,2]) 依賴數字的由來ntttself.assignDepth(module);ntttself.processDependenciesBlockForChunk(module, chunk);ntt}nn this.createChunkAssets(); // 生成最終assetsttn}n

再經歷了seal的步驟後,我們的一個個module塊已經被分配到各自的chunk中去了,準備最後寫入文件。但是依賴關係都是ES6的,要想最後瀏覽器運行還需要加個pollify,webpack針對幾種類型的文件有對應的模版來處理

createChunkAssets(){nt//這個方法調用不同的模版來處理源文件用於輸出,我們用MainTemplate.js舉例,最終瀏覽器運行頂部的代碼模版就在這裡定義ntif(chunk.hasRuntime()) {nttttttsource = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);nttttt} else {nttttttsource = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);nttttt}n}nn//這段代碼輸出了runtime函數,既瀏覽器環境實現的webpack模塊載入器ntnthis.plugin("render", (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {ntttconst source = new ConcatSource();ntttsource.add("/******/ (function(modules) { // webpackBootstrapn");ntttsource.add(new PrefixSource("/******/", bootstrapSource));ntttsource.add("/******/ })n");ntttsource.add("/************************************************************************/n");ntttsource.add("/******/ (");ntttconst modules = this.renderChunkModules(chunk, moduleTemplate, dependencyTemplates, "/******/ ");ntttsource.add(this.applyPluginsWaterfall("modules", modules, chunk, hash, moduleTemplate, dependencyTemplates));ntttsource.add(")");ntttreturn source;ntt});n

5 emitAssets 最後輸出我們處理好的結果到本地文件中

經歷了這麼多步驟,終於在Compiler.compile方法完成後的回調中,我們要調用emitAssets方法進行最後輸出工作,把代碼寫到結果文件中去。

Compiler.prototype.emitAssets = function(compilation, callback) {n this.outputFileSystem.writeFile(targetPath, content, callback);n}n

Webpack工作原理總結

基於訂閱/發布模型建立的Webpack打包工具把一個個繁雜耦合的前端源代碼處理工作拆分成了很多個細小的任務。通過Tapable.plugin來註冊一個個訂閱器就可以在webpack工作中的某個具體步驟插入你的處理邏輯。這種插片式的計方便我們低耦合的對前端打包流程進行自定義。

我們研究webpack的目的是因為他在目前的前端項目中比較流行,為了能更好的使用他,通過改進打包流程的過程而不是業務代碼來實現整站的優化。前端工程工具日新月異,但是基本的思想都是在通過對js語言的預處理來實現其他語言/開發模式中的優秀方式,達到規範前端開發提升開發效率的目的。在對webpack優化中,我們也盡量使用plugin系統來實現流程優化而不是再發明新的api,節約開發成本的同時也更好的兼容業務項目向未來構建工具的遷移。


推薦閱讀:

前端初學路線指南
2016 年中國前端界發生的大事件都有哪些?
前端總結
「Luy」CSS盒子模式還是很重要的
為什麼現在很多網站都不用原生的input輸入,而是用div模擬?

TAG:webpack | 前端开发 | 前端框架 |