java有類文件,Python有import機制,Ruby有require等,而Javascript 經過<script>
標籤引入代碼的機制顯得雜亂無章,語言自身毫無組織能力,人們不得不用命名空間的等方式人爲的組織代碼,以求達到安全易用的目的
《深刻淺出Nodejs》--樸靈
模塊一直以來都是組織大型軟件的必備的要素,就像建築和磚,「磚」的組織規則更是須要最早明確的事情,一直以來JS在語言層面都沒能給模塊機制足夠的重視,知道ES6的module的出現彷彿給出了最終解決的方案,可是畢竟ES6的module還沒能獲得良好的支持,其中所面臨的複雜狀況可想而知,由於業務場景的多樣性致使彷佛哪種模塊機制都感受到了衆口難調,雖然Node8已經對絕大部分的ES6語法提供了很是好的支持,可是要想使用ES6的模塊機制仍是必需要使用相似babel的轉義工具才能作到並非那麼「無畏」的使用。本文從最簡單的模塊開始,而後主要從Node的模塊規範和ES6的模塊機制對模塊進行梳理。javascript
每次在註冊成爲某一個網站或者應用的用戶時最讓人心碎的的就是本身經常使用的用戶名已經存在了,很緊張得換了幾個還能接受的用戶名發現本身的想法老是很受歡迎,因而即使放着《不將就》也無奈的選擇了在本身的用戶名後面加上了本身的生日數字...
這裏也不太方便討論若是加上了生日數字以後,表單校訂仍是提示你「該用戶名已經存在!」的狀況,剪網線就完事了。html
我想表達的意思實際就是,全局環境下的變量的命名衝突,變量太多不免詞窮狀況很常見,因此這必定是模塊化給咱們帶來的好處,有了模塊你就能夠繼續用你喜歡的用戶名,只不過你得介紹清楚,你是「村口第五家.Ray"vue
無需多言,上圖表達了一切。良好的模塊化,是代碼複用與工程解耦的關鍵,"一把梭"確實爽,講究一個我無論你裏面怎麼翻滾,你暴露給我乾淨的接口,我還你一個講究的git star。java
若是一個包依賴另外一個包,你一把梭的時候還要手動先把它依賴的那個包梭進來,過度之,那個它依賴的包有依賴好幾個別的包,甚至有些狀況中你甚至還要很在乎你手動添加依賴的順序,這種梭法,一旦項目複雜,光是對這些「梭法」的管理都讓人心煩了,因此爲了省心,模塊機制也務必要面對解析依賴,管理依賴這個自己就很繁瑣的任務。node
因此進入正題,針對前面提到的幾點,看一看簡單的模塊實現。jquery
const module1 = ()=>{ // dosomething } const module2 = ()=>{ // dosomething }
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } }); // module1.m1 // module1.m2
缺點:每每存在不想讓外部訪問的變量(module1._count),這種方式就不能知足了(不考慮使用Object.defineProperty)ios
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();
經過自執行函數能夠只返回想返回的東西。git
若是此模塊內想繼承使用相似於jquery等庫則就須要顯示的將庫傳入到自執行函數中了es6
var module1 = (function ($, axios) { //... })(jQuery, axios);
1.默認方法npm
經過<script>
標籤加載 JavaScript 腳本,默認是同步加載執行的,渲染引擎若是遇到<script>
會停下來,知道腳本下載執行完成
2.異步方法
<script src="/lib/test.js" defer></script> <script src="/lib/test.js" async></script>
defer 和 async屬性
Node 的模塊系統就是參照着CommonJs規範所實現的
const path = require('path') path.join(__dirname,path.sep)
path.join 必然是依賴於path模塊加載完成才能使用的,對於服務器來講,由於全部的資源都存放在本地,因此各類模塊各類模塊加載進來以後再執行先關邏輯對於速度的要求來講並不會是那麼明顯問題。
特色:
require(同步加載)基本功能:讀取並執行一個JS文件,而後返回該模塊的exports對象,若是沒有發現指定模塊會報錯;
exports:node爲每一個模塊提供一個exports變量,其指向module.exports,至關於在模塊頭部加了這句話:var exports = module.exports,在對外輸出時,能夠給exports對象添加方法(exports.xxx等同於module.exports.xxx),不能直接賦值(由於這樣就切斷了exports和module.exports的聯繫);
module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。
module對象的屬性:
例子:
exports.hello = function() { return 'hello'; }; module.exports = 'Hello world';/
所以一旦module.exports被賦值了,代表這個模塊具備單一出口了
Asynchronous Module Definition異步加載某模塊的規範。試想若是在瀏覽器中(資源再也不本地)採用commonjs這種徹底依賴於先加載再試用方法,那麼若是一個模塊特別大,網速特別慢的狀況下就會出現頁面卡頓的狀況。便有了異步加載模塊的AMD規範。require.js即是基於此規範
require(['module1','module2'....], callback); reqire([jquery],function(jquery){ //do something }) //定義模塊 define(id, [depends], callback); //id是模塊名,可選的依賴別的模塊的數組,callback是用於return出一個給別的模塊用的函數
熟悉的回調函數形式。
Node 對於模塊的實現以commonjs爲基礎的同時也增長了許多自身的特性
Node模塊的引入的三個步驟
文件定位
require
參數中若是不寫後綴名,node會按照.js
,.node
,.json
的順序依次補足並tryfs
模塊同步阻塞式的判斷文件是否存在,所以非js文件最後加上後綴編譯執行
.js
文件會被解析爲 JavaScript 文本文件,.json
文件會被解析爲 JSON 文本文件。 .node
文件會被解析爲經過 dlopen 加載的編譯後的插件模塊.Node的模塊分類
path
,buffer
,http
等,在Node編譯過程當中就加載進內存,所以會省掉文件定位和編譯執行兩個文件加載步驟require('./test.js').message='hello' console.log(require.cache); console.log(require('./test.js').message)//hello
上述代碼說明第二次加載依舊使用了第一次加載進來以後的模塊並無從新加載而是讀取了緩存中的模塊,由於從新加載的某塊中並無message。打印出來的require.cache包含了本模塊的module信息和加載進來的模塊信息。
那麼若是你想要屢次執行某一個模塊,要麼你手動像下面這樣刪除該模塊的緩存記錄以後再從新加載使用,要麼應該在模塊中暴露一個工廠函數,而後調用那個函數屢次執行該模塊,與vue-ssr的建立應用實例的工廠函數意思相近。
require('./test.js').message='hello' delete require.cache['/absolute-path/test.js'] console.log(require('./test.js').message)//undifined
可見當刪除了相關模塊的緩存,再一次加載時則再也不有message了。
// Vue-ssr工廠函數,目的是爲每一個請求創立一個新的應用實例 const Vue = require('vue') module.exports = function createApp (context) { return new Vue({ data: { url: context.url }, template: `<div>訪問的 URL 是: {{ url }}</div>` }) }
Node在加載模塊以後,執行以前則會使用函數包裝器將模塊代碼包裝,從而實現將頂層變量(var
,let
,const
)做用域限制在模塊範圍內,提供每個特定在該模塊的頂層全局變量module
,exports
,__dirname
(所在文件夾的絕對路徑),__filename
(絕對路徑加上文件名)
(function(exports, require, module, __filename, __dirname) { // 模塊的代碼實際上在這裏 });
關於模塊的具體編譯執行過程,此次就不深刻討論了,足夠花心思在好好從新深刻總結重寫一篇了,順便再次安利樸靈大大的《深刻淺出nodejs》
終於,ES6在語言層面上提供了JS一直都沒有的模塊功能,使得在繼Commonjs之於服務端,AMD之於瀏覽器以外提供了一個通用的解決方案。
1.設計思想
儘可能靜態化(靜態加載),使得編譯時就能肯定模塊間的依賴關係以及輸入輸出的變量。
2.關鍵語法
export
export var a = 1
return x + y;
};
`
對於一個模塊來講,它就是一個默認使用了嚴格模式的文件('use strict'),而別的文件要想使用該模塊,就必需要求該模塊內有export主動導出的內容
例子:
export 1 //直接導出一個數字是不能夠的 var a= 2 export a //間接導出數字也是不能夠的! export {a}//正確 export function(){} //錯誤 function sum(){} export sum //錯誤 export {sum}//正確
export我的最爲重要的一點就是能夠取到模塊內的實時的值
例子:
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500);
引用該模塊的文件在定時器時間到的時候則會獲得改變後的值
實質: 導出一個叫作default(默認的)變量,本質是將後面的值,賦給default變量,因此狀況就和export 不一樣了
不一樣點:
// 第一組 export default function crc32() {} import crc32 from 'crc32'; // 輸入 // 第二組 export function crc32() {}; import {crc32} from 'crc32'; // 輸入 export var a = 1;// 正確 var a = 1; export default a;// 正確 export default var a = 1;// 錯誤
export default 每個模塊只容許有一個
與導出export對應,引用則是import
export {a,b} || \/ import { a as A ,b as B} from './test.js';
主要特色:
使用import加載具備提高的效果,即會提到文件頭部進行:
foo(); import { foo } from 'my_module';
該代碼會正常執行。
*加載默認加載所有導出的變量
import * as A from './a.js'
import 加載進來的變量是不容許改變的。
type='module',此時瀏覽器就會知道這是ES6模塊,同時會自動給他加上前文提到的defer屬性,即等到全部的渲染操做都執行完成以後,纔會執行該模塊
<script type="module" src="./test.js"></script>
因爲Node有本身的模塊加載機制,因此在Node8.5以上版本將兩種方式的加載分開來處理,對於加載ES6的模塊,node要求其後綴名得是.mjs
,而後還得加上--experimental-modules
參數,而後兩種機制還不能混用。確實仍是很麻煩的,因此如今Node端想用import主流仍是用babel轉義。
首先看下面一段代碼:
if (x > 2) { import A from './a.js'; }else{ import B from './b.js'; }
這段代碼會報錯,由於JS引擎在處理import是在編譯時期,此時不會去執行條件語句,所以這段代碼會出現句法錯誤,相反,若是換成:
if (x > 2) { const A =require('./a.js'); }else{ const B =require('./b.js'); }
commonjs是在運行時加載模塊,所以上面代碼就會成功運行
因爲動態加載功能的要求,纔會有了import()函數的提案,這裏就不過多贅述。
commonjs模塊在加載以後會把原始類型的值緩存,以後該模塊的內部變化則不會再影響到其輸出的值:
//test.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; ================================== //main.js var test = require('./test'); console.log(test.counter); // 3 test.incCounter(); console.log(test.counter); // 3
ES6的模塊機制,在引擎靜態分析階段會把import當成是一種只讀引用(地址是隻讀的const,所以不能夠在引用該模塊的文件裏給他從新賦值),等到代碼實際運行時,纔會根據引用去取值
// test.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './test'; console.log(counter); // 3 incCounter(); console.log(counter); // 4
循環加載指的是,a文件依賴於b文件,而b文件又依賴於a文件
commonjs是在加載時執行的,他在require的時候就會所有跑一遍,所以他在遇到循環加載的狀況就會只輸出已經執行的部分,而以後的部分則不會輸出,下面是一個例子:
//parent文件 exports.flag = 1; let children = require('./children')//停下來,加載chilren console.log(`parent文件中chilren的flag =${children.flag}`); exports.flag = 2 console.log(`parent文件執行完畢了`); ========================================================= //test2文件 exports.flag = 1; let parent = require('./parent')//停下來,加載parent,此時parent只執行到了第一行,導出結果flag ==1 console.log(`children文件中parent的flag =${parent.flag}`); exports.flag = 2 console.log(`children文件執行完畢了`);
node parent
以後運行結果爲
運行parent以後會在第一行導出flag=1,而後去ruquire
children文件,此時parent進行等待,等待children文件執行結束,children開始執行到第二行的時候出現「循環加載」parent文件,此時系統自動去找parent文件的exports屬性,而parent只執行了一行,可是好在它有exports了flag,因此children文件加再進來了那個flag並繼續執行,第三行不會報錯,最後在第四行children導出了flag=2,此時parent再接着執行到結束。
ES6和commonjs本質上不一樣!由於ES6是引用取值,即動態引用
引用阮一峯老師ES6標準入門的例子
// a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar); export let foo = 'foo'; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo); export let bar = 'bar';
執行後的結果:
執行的過程是當a文件防線import了b文件以後就會去執行b文件,到了b文件這邊看到了他又引用了a文件,並不會又去執行a文件發生「張郎送李郎」的故事,而是倔強得認爲foo這個接口已經存在了,因而就繼續執行下去,直到在要引用foo
的時候發現foo
尚未定義,由於let定義變量會出現"暫時性死區",不能夠還沒定義就使用,其實若是改爲var聲明,有個變量提高做用就不會報錯了。改爲var聲明fooexport let foo = 'foo';
雖然打印的foo是undifined可是並無影響程序執行,但最好的作法是,改爲一樣有提高做用的function來聲明。最後去執行函數來得到值,最後獲得了但願的結果
// a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar()); export function foo() { return 'foo' }; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo()); export function bar() { return 'bar' };
其實關於模塊還有不少東西尚未梳理總結到,好比node模塊的加載過程的細節,和編譯過程,再好比如何本身寫一個npm模塊發佈等等都是很值得去梳理總結的,這一次就先到這吧,總之,第一次在本身的SF正兒八經的寫這麼長的技術總結博客,組織內容上感受比較凌亂,還有不少的不足。但願本身之後多多總結提升吧。最後固然仍是要感謝開源,感謝提供了那麼多優秀資料的前輩們。也歡迎來個人博客網站(https://isliulei.com)指教。
參考文章:
ES6標準入門--阮一峯
Nodejs v8.9.4 官方文檔
《深刻淺出Nodejs》---樸靈
Commonjs規範