這篇文章主要介紹了淺談webpack組織模組的原理,現在分享給大家,也給大家做個參考。
現在前端用Webpack打包JS和其它檔案已經是主流了,加上Node的流行,使得前端的工程方式和後端越來越像。所有的東西都模組化,最後統一編譯。 Webpack因為版本的不斷更新以及各種紛繁複雜的配置選項,在使用中出現一些迷之錯誤常常讓人無所適從。所以了解Webpack究竟是怎麼組織編譯模組的,產生的程式碼到底是怎麼執行的,還是很有好處的,否則它就永遠是個黑盒子。當然了我是前端小白,最近也是剛開始研究Webpack的原理,在這裡做一點記錄。
編譯模組
編譯兩個字聽起來就很黑科技,加上產生的程式碼往往是一大坨不知所雲的東西,所以常常會讓人卻步,但其實裡面的核心原則並沒有什麼難。所謂的Webpack的編譯,其實只是Webpack在分析了你的原始碼後,對其做出一定的修改,然後把所有原始碼統一組織在一個檔案裡而已。最後產生一個大的bundle JS文件,被瀏覽器或者其它Javascript引擎執行並傳回結果。
在這裡用一個簡單的案例來說明Webpack打包模組的原理。例如我們有一個模組mA.js
var aa = 1; function getDate() { return new Date(); } module.exports = { aa: aa, getDate: getDate }
我隨便定義了一個變數aa和一個函式getDate,然後export出來,這裡是用CommonJS的寫法。
然後再定義一個app.js,作為main文件,仍然是CommonJS風格:
var mA = require('./mA.js'); console.log('mA.aa =' + mA.aa); mA.getDate();
現在我們有了兩個模組,使用Webpack來打包,入口文件是app.js,依賴模組mA.js,Webpack要做幾件事:
從入口模組app.js開始,分析所有模組的依賴關係,把所有用到的模組都讀取進來。
每一個模組的原始碼都會被組織在一個立即執行的函數裡。
改寫模組程式碼中和require和export相關的語法,以及它們對應的引用變數。
在最後產生的bundle檔案建立一套模組管理系統,能夠在runtime動態載入用到的模組。
我們可以看上面這個例子,Webpack打包出來的結果。最後的bundle檔案總的來說是一個大的立即執行的函數,組織層次比較複雜,大量的命名也比較晦澀,所以我在這裡做了一定改寫和修飾,把它整理得盡量簡單易懂。
首先是把所有用到的模組都羅列出來,以它們的檔案名稱(一般是完整路徑)為ID,建立一張表:
var modules = { './mA.js': generated_mA, './app.js': generated_app }
關鍵是上面的generated_xxx是什麼?它是一個函數,它把每個模組的原始碼包裹在裡面,使其成為一個局部的作用域,從而不會暴露內部的變量,實際上就把每個模組都變成一個執行函數。它的定義一般是這樣:
function generated_module(module, exports, webpack_require) { // 模块的具体代码。 // ... }
在這裡模組的具體程式碼是指產生程式碼,Webpack稱之為generated code。例如mA,經過改寫得到這樣的結果:
function generated_mA(module, exports, webpack_require) { var aa = 1; function getDate() { return new Date(); } module.exports = { aa: aa, getDate: getDate } }
乍看之下似乎和原始碼一模一樣。的確,mA沒有require或import其它模組,export用的也是傳統的CommonJS風格,所以產生程式碼沒有任何改變。不過值得注意的是最後的module.exports = ...,這裡的module就是外面傳進來的參數module,這實際上是在告訴我們,運行這個函數,模組mA的源代碼就會被執行,並且最後需要export的內容就會被保存到外部,到這裡就標誌著mA載入完成,而那個外部的東西實際上就後面要說的模組管理系統。
接下來看app.js的生成程式碼:
function generated_app(module, exports, webpack_require) { var mA_imported_module = webpack_require('./mA.js'); console.log('mA.aa =' + mA_imported_module['aa']); mA_imported_module['getDate'](); }
可以看到,app.js的原始碼中關於引入的模組mA的部分做了修改,因為無論是require/ exports,或是ES6風格的import/export,都無法被JavaScript解釋器直接執行,它需要依賴模組管理系統,把這些抽象的關鍵字具體化。也就是說,webpack_require就是require的具體實現,它能夠動態地載入模組mA,並且將結果傳回給app。
到這裡你腦海裡可能已經初步逐漸建立了一個模組管理系統的想法,我們來看看webpack_require的實作:
// 加载完毕的所有模块。 var installedModules = {}; function webpack_require(moduleId) { // 如果模块已经加载过了,直接从Cache中读取。 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 创建新模块并添加到installedModules。 var module = installedModules[moduleId] = { id: moduleId, exports: {} }; // 加载模块,即运行模块的生成代码, modules[moduleId].call( module.exports, module, module.exports, webpack_require); return module.exports; }
注意倒數第二句裡的modules就是我們之前定義過的所有模組的generated code:
var modules = { './mA.js': generated_mA, './app.js': generated_app }
webpack_require的邏輯寫得很清楚,首先檢查模組是否已經加載,如果是則直接從Cache中返回模組的exports結果。如果是全新的模組,那麼就建立對應的資料結構module,並且運行這個模組的generated code,這個函數傳入的正是我們建立的module對象,以及它的exports域,這實際上就是CommonJS裡exports和module的由來。當運行完這個函數,模組就被載入完成了,需要export的結果保存到了module物件中。
所以我们看到所谓的模块管理系统,原理其实非常简单,只要耐心将它们抽丝剥茧理清楚了,根本没有什么深奥的东西,就是由这三个部分组成:
// 所有模块的生成代码 var modules; // 所有已经加载的模块,作为缓存表 var installedModules; // 加载模块的函数 function webpack_require(moduleId);
当然以上一切代码,在整个编译后的bundle文件中,都被包在一个大的立即执行的匿名函数中,最后返回的就是这么一句话:
return webpack_require(‘./app.js');
即加载入口模块app.js,后面所有的依赖都会动态地、递归地在runtime加载。当然Webpack真正生成的代码略有不同,它在结构上大致是这样:
(function(modules) { var installedModules = {}; function webpack_require(moduleId) { // ... } return webpack_require('./app.js'); }) ({ './mA.js': generated_mA, './app.js': generated_app });
可以看到它是直接把modules作为立即执行函数的参数传进去的而不是另外定义的,当然这和上面的写法没什么本质不同,我做这样的改写是为了解释起来更清楚。
ES6的import和export
以上的例子里都是用传统的CommonJS的写法,现在更通用的ES6风格是用import和export关键词,在使用上也略有一些不同。不过对于Webpack或者其它模块管理系统而言,这些新特性应该只被视为语法糖,它们本质上还是和require/exports一样的,例如export:
export aa // 等价于: module.exports['aa'] = aa export default bb // 等价于: module.exports['default'] = bb
而对于import:
import {aa} from './mA.js' // 等价于 var aa = require('./mA.js')['aa']
比较特殊的是这样的:
import m from './m.js'
情况会稍微复杂一点,它需要载入模块m的default export,而模块m可能并非是由ES6的export来写的,也可能根本没有export default,所以Webpack在为模块生成generated code的时候,会判断它是不是ES6风格的export,例如我们定义模块mB.js:
let x = 3; let printX = () => { console.log('x = ' + x); } export {printX} export default x
它使用了ES6的export,那么Webpack在mB的generated code就会加上一句话:
function generated_mB(module, exports, webpack_require) { Object.defineProperty(module.exports, '__esModule', {value: true}); // mB的具体代码 // .... }
也就是说,它给mB的export标注了一个__esModule,说明它是ES6风格的export。这样在其它模块中,当一个依赖模块以类似import m from './m.js'这样的方式加载时,会首先判断得到的是不是一个ES6 export出来的模块。如果是,则返回它的default,如果不是,则返回整个export对象。例如上面的mA是传统CommonJS的,mB是ES6风格的:
// mA is CommonJS module import mA from './mA.js' console.log(mA); // mB is ES6 module import mB from './mB.js' console.log(mB);
我们定义get_export_default函数:
function get_export_default(module) { return module && module.__esModule? module['default'] : module; }
这样generated code运行后在mA和mB上会得到不同的结果:
var mA_imported_module = webpack_require('./mA.js'); // 打印完整的 mA_imported_module console.log(get_export_default(mA_imported_module)); var mB_imported_module = webpack_require('./mB.js'); // 打印 mB_imported_module['default'] console.log(get_export_default(mB_imported_module));
这就是在ES6的import上,Webpack需要做一些特殊处理的地方。不过总体而言,ES6的import/export在本质上和CommonJS没有区别,而且Webpack最后生成的generated code也还是基于CommonJS的module/exports这一套机制来实现模块的加载的。
模块管理系统
以上就是Webpack如何打包组织模块,实现runtime模块加载的解读,其实它的原理并不难,核心的思想就是建立模块的管理系统,而这样的做法也是具有普遍性的,如果你读过Node.js的Module部分的源代码,就会发现其实用的是类似的方法。这里有一篇文章可以参考。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是深入講解webpack模組的基本原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!