首頁 >web前端 >js教程 >深入講解webpack模組的基本原理

深入講解webpack模組的基本原理

亚连
亚连原創
2018-05-31 13:58:491276瀏覽

這篇文章主要介紹了淺談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要做幾件事:

  1. 從入口模組app.js開始,分析所有模組的依賴關係,把所有用到的模組都讀取進來。

  2. 每一個模組的原始碼都會被組織在一個立即執行的函數裡。

  3. 改寫模組程式碼中和require和export相關的語法,以及它們對應的引用變數。

  4. 在最後產生的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部分的源代码,就会发现其实用的是类似的方法。这里有一篇文章可以参考。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

vue iview组件表格 render函数的使用方法详解

微信小程序实现换肤功能

nodejs实现解析xml字符串为对象的方法示例

以上是深入講解webpack模組的基本原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn