摘要:每一個模塊的源代碼都會被組織在一個立即執行的函數里。接下來看的生成代碼可以看到,的源代碼中關于引入的模塊的部分做了修改,因為無論是,或是風格的,都無法被解釋器直接執行,它需要依賴模塊管理系統,把這些抽象的關鍵詞具體化。
現在前端用Webpack打包JS和其它文件已經是主流了,加上Node的流行,使得前端的工程方式和后端越來越像。所有的東西都模塊化,最后統一編譯。Webpack因為版本的不斷更新以及各種各樣紛繁復雜的配置選項,在使用中出現一些迷之錯誤常常讓人無所適從。所以了解一下Webpack究竟是怎么組織編譯模塊的,生成的代碼到底是怎么執行的,還是很有好處的,否則它就永遠是個黑箱。當然了我是前端小白,最近也是剛開始研究Webpack的原理,在這里做一點記錄。
編譯模塊編譯兩個字聽起來就很黑科技,加上生成的代碼往往是一大坨不知所云的東西,所以常常會讓人卻步,但其實里面的核心原理并沒有什么難。所謂的Webpack的編譯,其實只是Webpack在分析了你的源代碼后,對其作出一定的修改,然后把所有源代碼統一組織在一個文件里而已。最后生成一個大的bundle JS文件,被瀏覽器或者其它Javascript引擎執行并返回結果。
在這里用一個簡單的案例來說明Webpack打包模塊的原理。例如我們有一個模塊mA.js
var aa = 1; function inc() { aa++; } module.exports = { aa: aa, inc: inc }
我隨便定義了一個變量aa和一個函數inc,然后export出來,這里是用CommonJS的寫法。
然后再定義一個app.js,作為main文件,仍然是CommonJS風格:
var mA = require("./mA.js"); console.log("mA.aa =" + mA.aa); mA.inc();
現在我們有了兩個模塊,使用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 inc() { aa++; } module.exports = { aa: aa, inc: inc } }
乍一看似乎和源代碼一模一樣。的確,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["inc"](); }
可以看到,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,當運行它時,就會運行generated_app;而它需要載入模塊mA,于是就會運行webpack_require("./mA.js"),進而運行generated_mA。也就是說,所有的依賴的模塊就是這樣動態地、遞歸地在runtime完成加載,并被放入InstalledModules緩存。
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關鍵詞,它們看上去似乎只是語法糖,但實際上根據ES6的標準,它們和CommonJS在關于模塊加載的使用和行為上會有一些微妙的不同。例如當CommonJS輸出原始類型(非對象)變量時,輸出的是這個變量的拷貝,這樣一旦模塊加載后,再去修改這個內部變量的值,是不會影響到輸出的變量的;而ES6輸出的則是引用,這樣無論模塊內部出現什么修改,都會反映在已經加載的模塊上。關于ES6和CommonJS在模塊管理上的區別,如果你還不熟悉的話,建議先讀一下阮一峰大神的這篇文章。
對于Webpack或者其它模塊管理系統而言,要實現ES6特性的import/export,本質上還是和require/exports類似的,也就是說仍然使用module.export這一套機制,但是情況會變得比較復雜,因為可能存在CommonJS和ES6模塊之間的相互引用。為了保持兼容,并且符合ES6的相應標準,Webpack在生成相應語句的generated code時,就要做很多特殊處理,關于這一塊內容很多,深究起來可以多帶帶寫一篇文章,在這里我只是把我理解的部分寫出來。
對于CommonJS而言,export的是很直接的,因為源代碼里module.exports輸出什么,生成代碼里的輸出也原樣不變,例如我們之前定義的模塊mA.js:
var aa = 1; function inc() { aa++; } function get_aa() { return aa; } module.exports = { aa: aa, inc: inc, get_aa: get_aa; }
生成代碼里,module.exports也會像源代碼里這樣寫,注意這里輸出的時候,aa作為一個原始類型,輸出到module.exports里的是一個拷貝,這樣一旦模塊mA加載后,再去調用inc(),修改的是模塊內部的aa,而不會影響輸出后的aa:
var mA = require("./mA.js"); console.log("mA.aa = " + mA.aa); // 輸出1 mA.inc(); console.log("mA.aa = " + mA.aa); // 仍然是1 // 這里會輸出2,因為get_aa()拿到的是模塊內部的aa原始引用。 console.log("mA.get_aa() = " + mA.get_aa());
然而ES6就完全不是這么一回事兒了,假如上面的模塊mA,我們用ES6輸出:
export {aa, inc, get_aa}
然后在別的模塊里加載mA:
import {aa, inc} from "./mA.js" console.log("aa = " + aa); // 輸出1 inc(); console.log("aa = " + aa); // 輸出2
這里不管mA輸出的是什么類型的數據,輸出的都是它的引用,當別的模塊載入mA時,得到的也是mA模塊內部變量的引用。要實現這個規則,mA的generated code就不能簡單地直接給module.exports設置aa這個原始變量類型了,而是像上面的get_aa那樣,給它定義getter。例如當我們export aa,Webpack會生成類似于這樣的代碼:
var aa = 1; defineGetter(module.exports, “aa”, function(){ return aa; });
defineGetter的定義如下:
function defineGetter(exports, name, getter) { if (!Object.prototype.hasOwnProperty.call(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter, }); } }
這樣就實現了我們需要的引用功能,也就是說,在module.exports上,我們并不是定義aa這個原始類型,而是定義aa的getter,使之指向其原模塊內部aa的引用。
不過對于export default,當輸出原始類型時,它又回到了拷貝,而不是getter引用的方式,即對于這樣的輸出:
export default aa;
Webpack會生成這樣的代碼:
module.exports["default"] = aa;
我還沒完全弄清楚這樣做是否符合ES6標準,懂的童鞋可以留下評論。
當然話說回來,模塊中直接輸出aa這樣的原始類型的變量還是挺少見的,但并非不可能。源代碼一旦有這樣的行為,ES6和CommonJS就會表現出完全不同的特性,所以Webpack也必須實現這種區別。
Webpack對ES6模塊輸出的另一個特殊處理是__esModule,例如是我們定義ES6模塊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。為什么要這樣做?因為當別人引用一個模塊時,它并不知道這個模塊是以CommonJS還是ES6風格輸出的,所以__esModule的意義就在于告訴別人,這是一個ES6模塊。關于它的具體作用我們繼續看下面的import部分。
這是一種比較簡單的import方式:
import {aa} from "./mA.js" // 基本等價于 var aa = require("./mA.js")["aa"]
但是當別人這樣引用時:
import m from "./m.js"
情況會稍微復雜一點,它需要載入模塊m的export default部分,而模塊m可能并非是由ES6的export來寫的,也可能根本沒有export default。這樣在其它模塊中,當一個依賴模塊以類似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);
這就用到了之前export部分的__esModule了。我們定義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/export上,Webpack需要做很多特殊處理的地方。我的分析還并不完整,建議感興趣的童鞋自己去敲一下并且閱讀編譯后的代碼,仔細比較CommonJS和ES6的不同。不過就實現而言,它們并沒有本質區別,而且Webpack最后生成的generated code也還是基于CommonJS的module/exports這一套機制來實現模塊的加載的。
模塊管理系統以上就是Webpack如何打包組織模塊,實現runtime模塊加載的解讀,其實它的原理并不難,核心的思想就是建立模塊的管理系統,而這樣的做法也是具有普遍性的,如果你讀過Node.js的Module部分的源代碼,就會發現其實用的是類似的方法。這里有一篇文章可以參考。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93258.html
摘要:環境搭建今天給大家介紹種環境搭建的方法。官方的地址步驟安裝種子文件沒有的,可以自己下來,然后打開,執行。使用版本為版本。存放表單相關內置組件與指令。存放網絡請求相關的服務等。等待加載完畢即可。從而實現了頁面的顯示 1:環境搭建 今天給大家介紹4種環境搭建的方法。 一:Angular-cli的安裝 官方指導文檔:www.angular.cn/guide/quickstart 請使用cn...
摘要:所以通常情況下當你的庫需要依賴到例如,這樣的通用模塊時,我們可以不將它打包進,而是在的配置中聲明這就是在告訴請不要將這個模塊注入編譯后的文件里,對于我源代碼里出現的任何這個模塊的語句,請將它保留。 這篇文章討論Webpack打包library時經常需要用到的一個選項external,它用于避免將一些很通用的模塊打包進你發布的library里,而是選擇把它們聲明成external的模塊,...
摘要:系列文章系列第一篇基礎雜記系列第二篇插件機制雜記系列第三篇流程雜記前言公司的前端項目基本都是用來做工程化的,而雖然只是一個工具,但內部涉及到非常多的知識,之前一直靠來解決問題,之知其然不知其所以然,希望這次能整理一下相關的知識點。 系列文章 Webpack系列-第一篇基礎雜記 Webpack系列-第二篇插件機制雜記 Webpack系列-第三篇流程雜記 前言 公司的前端項目基本都是用...
摘要:前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。背后的故事本文是對于年之間世界發生的大事件的詳細介紹,闡述了從提出到角力到流產的前世今生。 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為新聞熱點、開發教程、工程實踐、深度閱讀、開源項目、巔峰人生等欄目。歡迎...
摘要:所以你編譯后的文件實際上應當只輸出,這就需要在配置里用來控制這樣上面的模塊加載函數會在返回值后面加一個,這樣就只返回的部分。 之前一篇文章分析了Webpack打包JS模塊的基本原理,所介紹的案例是最常見的一種情況,即多個JS模塊和一個入口模塊,打包成一個bundle文件,可以直接被瀏覽器或者其它JavaScript引擎執行,相當于直接編譯生成一個完整的可執行的文件。不過還有一種很常見的...
閱讀 1170·2021-11-16 11:45
閱讀 1034·2021-09-04 16:41
閱讀 3085·2019-08-29 16:40
閱讀 2863·2019-08-29 15:34
閱讀 2680·2019-08-29 13:11
閱讀 1742·2019-08-29 12:58
閱讀 1735·2019-08-28 18:00
閱讀 1783·2019-08-26 18:26