菜單快捷導航:vue
CommoneJS規定每一個文件是一個模塊。將一個JavaScript文件直接經過script標籤引入頁面中,和封裝成CommonJS模塊最大的不一樣在於:前者的頂層做用域是全局做用域,在進行變量及函數聲明時會污染全局環境;然後者會造成一個屬於模塊自身的做用域,全部的變量及函數只有本身能訪問,對外是不可見的。react
導出是一個模塊向外暴露自身的惟一方式。在CommonJS中,經過module.exports能夠導出模塊中的內容,如:webpack
module.exports = { name: 'commonJS_exports.js', add: function(a, b){ return a + b; } }
爲了書寫方便,CommonJS也支持另外一種簡化的導出方式:直接使用exports。效果和上面同樣:git
exports.name = 'commonJS_exports.js'; exports.add = function(a, b){ return a + b; }
注意:導出時不要把module.exports 與 exports混用,下面舉一個錯誤的示例:es6
exports.add = function(a, b){ return a + b; } module.exports = { name: 'commonJS_exports.js' }
上面的代碼先經過exports導出add屬性,而後將module.exports從新賦值爲另一個對象。這會致使本來擁有的add屬性的對象丟失了,最後導出的只有name。所以建議一個模塊中的導出方式要麼使用module.exports,要麼使用exports,不要混着一塊兒用。github
在實際使用中,爲了提升可讀性,應該將module.exports及exports語句放在模塊的末尾。web
在CommonJS中使用require進行模塊導入。如:npm
commonJS_exports.js導出代碼:api
console.log('...hello, 我是commonJS_exports.js....start..') //一、第一種寫法 module.exports = { name: 'commonJS_exports.js', add: function(a, b){ return a + b; } }
PageModule.vue頁面中導入代碼:瀏覽器
//一、測試CommonJS的exports和require var comObj = require('../api/module/commonJS_exports'); console.log('...name: ', comObj.name); try{ console.log('8 + 9 = ', comObj.add(8, 9)); }catch(e){ console.log(e); }
另外,若是在頁面中對同一模塊進行屢次導入,則該模塊只會在第一次導入時執行,後面的導入不會執行,而是直接導出上次執行後獲得的結果。示例以下:
var comObj = require('../api/module/commonJS_exports'); //再調用一次導入,發現導入模塊不會再次執行,而是直接導出上次執行後獲得的結果 require('../api/module/commonJS_exports'); console.log('...name: ', comObj.name); try{ console.log('8 + 9 = ', comObj.add(8, 9)); }catch(e){ console.log(e); }
咱們看到控制檯打印結果以下,導入模塊果真只執行了一次:
....test CommonJS 的導入...
...name: commonJS_exports.js
8 + 9 = 17
在module對象中有一個屬性loaded用於記錄該模塊是否被加載過,它的默認值爲false,當模塊第一次被加載和執行事後會設置爲true,後面再次加載時檢查到module.loaded爲true, 則不會再次執行模塊代碼。
require函數能夠接收表達式,藉助這個特性咱們能夠動態地指定模塊加載路徑
const moduleNames = ['foo.js', 'bar.js']; moduleNames.forEach(name=>{ require('./' + name); })
2015年6月,發佈的ES6才添加了模塊這一特性。ES6 Module也是將每一個文件做爲一個模塊,每一個模塊擁有自身的做用域,不一樣的是導入、導出語句。import和export也做爲保留關鍵字在ES6版本中加入了進來(CommonJS中的module並不屬於關鍵字)。
在ES6 Module中使用export命令來導出模塊。export有兩種導出形式:
//第一種導出方式:命名導出 //1.1 命名導出第一種寫法 export const name = 'es6_export.js'; export const add = function(a, b) { return a + b; } // //1.2 命名導出第二種寫法 // const name = 'es6_export.js' // const add = function(a, b){ return a + b; } // export { name, add };
第一種寫法是將變量的聲明和導出寫在一行;第二種寫法則是先進行變量聲明,而後再用同一個export語句導出。兩種寫法的效果是同樣的。在使用命名導出時,還能夠經過as關鍵字對變量重命名。如:
const name = 'es6_export.js' const add = function(a, b){ return a + b; } export { name, add as getSum }; //在導入時即爲name和getSum
//第二種導出方式:默認導出 export default{ name: 'es6_export', add: function(a, b){ return a + b; } }
咱們能夠將export default理解爲對外輸出了一個名爲default的變量,所以不須要像「命名導出」同樣進行變量聲明,直接導出便可。
//導出字符串 export default 'this is es6_export.js file ' //導出class export default class {...} //導出匿名函數 export default function(){ ... }
ES6 Module中使用import語法導入模塊。
const name = 'es6_export.js' const add = function(a, b){ return a + b; } export { name, add };
// import {name, add } from '../api/module/es6_export.js'; //命名導出第一種導入方式 // import * as esObj from '../api/module/es6_export.js'; //命名導出第二種別名總體導入方式 import {name, add as getSum } from '../api/module/es6_export.js'; //命名導出第三種別名導入方式 // //命名導出第一種導入方式 // console.log('name: ', name); // console.log('12 + 21: ', add(12, 21)); // //命名導出第二種別名導入方式 // console.log('name: ', esObj.name); // console.log('12 + 21: ', esObj.add(12, 21)); //命名導出第三種別名導入方式 console.log('name: ', name); console.log('12 + 21: ', getSum(12, 21));
加載帶有命名導出的模塊時,import後面要跟一對大括號來將導入的變量名包裹起來,而且這些變量須要與導出的變量名徹底一致。導入變量的效果至關於在當前做用域下聲明瞭這些變量(name和add),而且不可對其進行更改,也就是全部導入的變量都是隻讀的。
另外和命名導出相似,咱們能夠經過as關鍵字對到導入的變量重命名。在導入多個變量時,咱們還能夠採用總體導入的方式,這種import * as <myModule>導入方式能夠把全部導入的變量做爲屬性添加到<myModule>對象中,從而減小了對當前做用域的影響。
//第二種導出方式:默認導出 export default{ name: 'es6_export.js', add: function(a, b){ return a + b; } }
import esObj from '../api/module/es6_export.js'; //默認命名導出的導入測試 console.log('name: ', esObj.name); console.log('12 + 21: ', esObj.add(12, 21));
對於默認導出來講,import後面直接跟變量名,而且這個名字能夠自由指定(好比這裏時esObj), 它指代了es6_export.js中默認導出的值。從原理上能夠這樣去理解:
import { default as esObj } from '../api/module/es6_export';
注意:默認導出自定義變量名和 命名導出總體起別名有點像,可是命名導出總體起別名必須是在import 後面是 * as 別名,而默認導出是import後面直接跟自定義變量名。
最後咱們看一下兩種導入方式混合起來的例子:
import React, {Component} from 'react'
這裏的React對應的是該模塊的默認導出,而Component則是其命名導出中的一個變量。注意:這裏的React必須寫在大括號前面,而不能順序顛倒,不然會引發提示語法錯誤。
在工程中,有時須要把某一個模塊導入以後當即導出,好比專門用來集合全部頁面或組件的入口文件。此時能夠採用複合形式的寫法:
export {name, add} from '../api/module/es6_export.js'
不過,上面的複合寫法目前只支持「命名導出」方式暴露出來的變量。
默認導出則沒有對應的複合形式,只能將導入和導出拆開寫:
import esObj from '../api/module/es6_export.js' export default esObj
上面咱們分別介紹CommonJS和ES6 Module兩種形式的模塊定義,在實際開發中咱們常常會將兩者混用,下面對比一下它們的特性:
CommonJS和ES6 Module最本質的區別在於前者對模塊依賴的解決是「動態的」,然後者是「靜態的」。這裏「動態」的含義是, 模塊依賴關係的創建發生在代碼運行階段;而「靜態」則是模塊依賴關係的創建發生在代碼編譯階段。
咱們先看一個CommonJS的例子:
// commonJS_exports.js module.exports = { name: 'commonJS_exports' }
//PageModule.vue const name = require('../api/module/commonJS_exports').name;
當模塊PageModule.vue加載模塊commonJS_exports.js時,會執行commonJS_exports.js中的代碼,並將其module.exports對象做爲require函數的返回值返回。而且require的模塊路徑能夠動態指定,支持傳入一個表達式,咱們甚至能夠經過if語句判斷是否加載某個模塊。所以,在CommonJS模塊被執行前,並無辦法肯定明確的依賴關係,模塊的導入、導出發生在代碼的運行階段。
一樣的例子,咱們再對比看下ES6 Module的寫法:
//es6_export.js export const name = 'es6_export.js';
//PageModule.vue import { name } from '../api/module/es6_export.js'
ES6 Module的導入、導出語句都是聲明式,它不支持導入的路徑是一個表達式,而且導入、導出語句必須位於模塊的頂層做用域(好比不能放在if語句中)。
所以咱們說,ES6 Module是一種靜態的模塊結構,在ES6代碼的編譯階段就能夠分析出模塊的依賴關係。它相比於CommonJS來講具有如下幾點優點:
在導入一個模塊時,對於CommonJS來講獲取的是一份導出值的拷貝;而在ES6 Module中則是值的動態映射,而且這個映射是隻讀的。例子:
//commonJS_exports.js var count = 0; module.exports = { count: count, add: function(a, b){ count+=1; return a + b; } }
//PageModule.vue var count = require('../api/module/commonJS_exports.js').count; var add = require('../api/module/commonJS_exports.js').add; console.log(count); //0 這裏的count是對commonJS_exports.js中count值的拷貝 add(2, 3); console.log(count); //0 commonJS_exports.js中變量值的改變不會對這裏的拷貝值形成影響 count += 1; console.log(count); //1 拷貝的值能夠更改
PageModule.vue中的count是對commonJS_exports.js中count的一份值拷貝,所以在調用函數時,雖然更改了本來calculator.js中count的值,可是並不會對PageModule.vue中導入時建立的副本形成影響。另外一方面,在CommonJS中容許對導入的值進行更改。咱們能夠在PageModule.vue更改count和add, 將其賦予新值。一樣,因爲是值的拷貝,這些操做不會影響calculator.js自己。
下面咱們使用ES6 Module將上面的例子進行改寫:
//es6_export.js let count = 0; const add = function(a, b){ count += 1; return a + b; } export { count, add }
import {name, add, count } from '../api/module/es6_export'; console.log(count); //0, 對es6_export.js中的count值的映射 add(2, 3); console.log(count); //1 實時反映es6_export.js中count值的變化 // count += 1; //不可更改,會拋出ReferenceError: count is not defined
上面的例子展現了ES6 Module中導入的變量實際上是對原有值的動態映射。PageModule.vue中的count是對calculator.js中的count值的實時反映,當咱們經過調用add函數更改了calculator.js中的count值時,PageModule.vue中count的值也隨之變化。
咱們不能夠對ES6 Module導入的變量進行更改,能夠將這種映射關係理解爲一面鏡子,從鏡子裏咱們能夠實時觀察到原有的事物,可是並不能夠操做鏡子中的影像。
循環依賴是指模塊A依賴於B, 同時模塊B依賴於模塊A。通常來講工程中應該儘可能避免循環依賴的產生,由於從軟件設計的角度來講,單向的依賴關係更加清晰,而循環依賴則會帶來必定的複雜度。而在實際開發中,循環依賴有時會在咱們不經意間產生,由於當工程的複雜度上升到足夠規模時,就容易出現隱藏的循環依賴關係。
簡單來講,A和B兩個模塊之間是否存在直接的循環依賴關係是很容易被發現的。但實際狀況每每是A依賴於B,B依賴於C,C依賴於D,最後繞了一圈,D又依賴於A。當中間模塊太多時就很難發現A和B之間存在着隱式的循環依賴。
所以,如何處理循環依賴是開發者必需要面對的問題。
//bar.js const foo = require('./foo.js'); console.log('value of foo: ', foo); module.exports = 'This is bar.js';
//foo.js const bar = require('./bar.js'); console.log('value of bar: ', bar); module.exports = 'This is foo.js';
//PageModule.vue require('../api/module/foo.js'); /* 打印結果: value of foo: {} value of bar: This is bar.js * */
爲何foo的值是一個空對象呢?讓咱們從頭梳理一下代碼的實際執行順尋:
由上面能夠看出,儘管循環依賴的模塊均被執行了,但模塊導入的值並非咱們想要的。咱們再從Webpack的實現角度來看,將上面例子打包後,bundle中有這樣一段代碼很是重要:
//The require function function __webpack_require__(moduleId){ if(installedModules[moduleId]){ return installedModules[moduleId].exports; } //Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} } //... }
當PageModule.vue引用了foo.js以後,至關於執行了這個__webpack_require__函數,初始化了一個module對象並放入installedModules中。當bar.js再次引用foo.js時,又執行了該函數,但此次是直接從installedModules裏面取值,此時它的module.exports是一個空對象。這就解釋了上面再第3步看到的現象。
//bar_es6.js import foo from './foo_es6.js'; console.log('value of foo: ', foo); export default 'This is bar_es6.js';
//foo_es6.js import bar from './bar_es6.js'; console.log('value of bar: ', bar); export default 'This is foo_es6.js';
//PageModule.vue import foo_es6 from '../api/module/foo_es6.js';
/* 打印結果: value of foo: undefined value of bar: This is bar_es6.js * */
很遺憾,在bar_es6.js中一樣沒法獲得foo_es6.js正確的導出值,只不過和CommonJS默認導出一個空對象不一樣,這裏獲取到的是undefined。
上面咱們談到,在導入一個模塊時,CommonJS獲取到的時值的拷貝,ES6 Module則是動態映射,
//bar_es6_2.js import foo from './foo_es6_2.js'; let invoked = false; function bar(invoker){ if (!invoked){ invoked = true; console.log(invoker + ' invokes bar_es6_2.js'); foo('bar_es6_2.js'); } } export default bar;
//foo_es6_2.js import bar from './bar_es6_2.js' function foo(invoker){ console.log(invoker + ' invokes foo_es6_2.js'); bar('foo_es6_2.js'); } export default foo;
import foo_es6_2 from '../api/module/foo_es6_2.js' foo_es6_2('PageModule.vue'); /* 打印結果: PageModule.vue invokes foo_es6_2.js foo_es6_2.js invokes bar_es6_2.js bar_es6_2.js invokes foo_es6_2.js * */
能夠看到,foo_es6_2.js和bar_es6_2.js這一對循環依賴的模塊均獲取到了正確的導出值。下面咱們分析一下代碼的執行過程:
由上面的例子能夠看出,ES6 Module的特性使其能夠更好的支持循環依賴,只是須要由開發者來保證導入的值被使用時已經設置好正確的導出值。
面對工程中成百上千個模塊,webpack究竟時如何將它們有序的組織在一塊兒,並按照咱們預想的順序運行在瀏覽器上的呢?下面咱們將從原理上進行探究。
仍是用前面的例子:
//commonJS_exports.js module.exports = { add: function(a, b){ return a + b; } }
//PageModule.vue const comObj = require('../api/module/commonJS_exports'); const sum = comObj.add(2, 3); console.log('sum: ', sum);
上面的代碼通過Webpack打包後將會成爲以下的形式(爲了易讀性這裏只展現代碼的答題結構):
//當即執行匿名函數 (function(modules){ //模塊緩存 var installedModules = {}; //實現require function __webpack_require__(moduleId){ //... } //執行入口模塊的加載 return __webpack_require__(__webpack__require__.s == 0); })({ //modules: 以key-value的形式存儲全部被打包的模塊 0: function(module, exports, __webpack_require__){ //打包入口 module.exports = __webpack_require__("3qiv"); }, "3qiv": function(module, exports, __webpack_require__){ //PageModule.vue 內容 }, jkzz: function(module, exports){ //commonJS_exports.js 內容 } })
這是一個最簡單的Webpack打包結果(bundle),但已經能夠清晰地展現出它是如何將具備依賴關係的模塊串聯在一塊兒的。上面的bundle分爲如下幾個部分:
接下來咱們看看一個bundle是如何在瀏覽器中執行的:
不難看出,第3步和第4步時一個遞歸的過程,Webpack爲每一個模塊創造了一個能夠導出和導入模塊的環境,但本質上並無修改代碼的執行邏輯,所以代碼執行的順序於模塊加載的順序時徹底一致的,這就時Webpack模塊打包的奧祕。
本文測試截圖:
下載測試DEMO:https://github.com/xiaotanit/tan_vue,若是你以爲能夠,請順手點個星^_^。
npm install , npm run serve 後,在瀏覽器輸入測試地址看效果:
http://localhost:8080/pageModule //端口可能會變化
參考書籍:《Webpack實戰:入門、進階與調優》--- 居玉皓