摘要:本文從最簡單的模塊開始,然后主要從的模塊規(guī)范和的模塊機制對模塊進行梳理。對象的屬性模塊的識別符,通常是帶有絕對路徑的模塊文件名。返回一個對象,表示調(diào)用該模塊的模塊。表示模塊對外輸出的值。可見當刪除了相關(guān)模塊的緩存再一次加載時則不再有了。
前言
java有類文件,Python有import機制,Ruby有require等,而Javascript 通過標簽引入代碼的機制顯得雜亂無章,語言自身毫無組織能力,人們不得不用命名空間的等方式人為的組織代碼,以求達到安全易用的目的
《深入淺出Nodejs》--樸靈
模塊一直以來都是組織大型軟件的必備的要素,就像建筑和磚,“磚”的組織規(guī)則更是需要最先明確的事情,一直以來JS在語言層面都沒能給模塊機制足夠的重視,知道ES6的module的出現(xiàn)仿佛給出了最終解決的方案,但是畢竟ES6的module還沒能得到良好的支持,其中所面臨的復(fù)雜情況可想而知,因為業(yè)務(wù)場景的多樣性導致似乎哪一種模塊機制都感覺到了眾口難調(diào),雖然Node8已經(jīng)對絕大部分的ES6語法提供了非常好的支持,但是要想使用ES6的模塊機制還是必須要使用類似babel的轉(zhuǎn)義工具才能做到并不是那么“無畏”的使用。本文從最簡單的模塊開始,然后主要從Node的模塊規(guī)范和ES6的模塊機制對模塊進行梳理。
“模塊化”的基本實現(xiàn)每次在注冊成為某一個網(wǎng)站或者應(yīng)用的用戶時最讓人心碎的的就是自己常用的用戶名已經(jīng)存在了,很緊張得換了幾個還能接受的用戶名發(fā)現(xiàn)自己的想法總是很受歡迎,于是即便放著《不將就》也無奈的選擇了在自己的用戶名后面加上了自己的生日數(shù)字...
這里也不太方便討論如果加上了生日數(shù)字之后,表單校正還是提示你“該用戶名已經(jīng)存在!”的情況,剪網(wǎng)線就完事了。
我想表達的意思實際就是,全局環(huán)境下的變量的命名沖突,變量太多難免詞窮情況很常見,所以這一定是模塊化給我們帶來的好處,有了模塊你就可以繼續(xù)用你喜歡的用戶名,只不過你得介紹清楚,你是“村口第五家.Ray"
無需多言,上圖表達了一切。良好的模塊化,是代碼復(fù)用與工程解耦的關(guān)鍵,"一把梭"確實爽,講究一個我不管你里面怎么翻滾,你暴露給我干凈的接口,我還你一個講究的git star。
如果一個包依賴另一個包,你一把梭的時候還要手動先把它依賴的那個包梭進來,過分之,那個它依賴的包有依賴好幾個別的包,甚至有些情況中你甚至還要很在意你手動添加依賴的順序,這種梭法,一旦項目復(fù)雜,光是對這些“梭法”的管理都讓人心煩了,所以為了省心,模塊機制也務(wù)必要面對解析依賴,管理依賴這個本身就很繁瑣的任務(wù)。
所以進入正題,針對前面提到的幾點,看一看簡單的模塊實現(xiàn)。
最簡單的模塊化可以理解成一個一個的封裝函數(shù),每一個封裝的函數(shù)去完成特定的功能,調(diào)用函數(shù)的方式進行復(fù)用。但是存在著類似于a,b污染了全局變量的缺點
const module1 = ()=>{ // dosomething } const module2 = ()=>{ // dosomething }
使用對象封裝
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } }); // module1.m1 // module1.m2
缺點:往往存在不想讓外部訪問的變量(module1._count),這種方式就不能滿足了(不考慮使用Object.defineProperty)
立即執(zhí)行函數(shù)的方式
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();
通過自執(zhí)行函數(shù)可以只返回想返回的東西。
如果此模塊內(nèi)想繼承使用類似于jquery等庫則就需要顯示的將庫傳入到自執(zhí)行函數(shù)中了
var module1 = (function ($, axios) { //... })(jQuery, axios);瀏覽器傳統(tǒng)加載模塊規(guī)則
1.默認方法
通過標簽加載 JavaScript 腳本,默認是同步加載執(zhí)行的,渲染引擎如果遇到會停下來,知道腳本下載執(zhí)行完成
2.異步方法
defer 和 async屬性
defer 會讓該標簽引用的腳本在DOM完全解析之后,并且引用的其他腳本執(zhí)行完成之后,才會執(zhí)行;多個defer會按照在頁面上出現(xiàn)的順序依次執(zhí)行
async 類似于異步回調(diào)函數(shù),加載完成或,渲染引擎就會立即停下來去執(zhí)行該腳本,多個async腳本不能后保證執(zhí)行的順序
CommonJsNode 的模塊系統(tǒng)就是參照著CommonJs規(guī)范所實現(xiàn)的
const path = require("path") path.join(__dirname,path.sep)
path.join 必然是依賴于path模塊加載完成才能使用的,對于服務(wù)器來說,因為所有的資源都存放在本地,所以各種模塊各種模塊加載進來之后再執(zhí)行先關(guān)邏輯對于速度的要求來說并不會是那么明顯問題。
特點:
一個文件就是一個模塊,擁有多帶帶的作用域;
普通方式定義的變量、函數(shù)、對象都屬于該模塊內(nèi);
通過require來加載模塊;
通過exports和modul.exports來暴露模塊中的內(nèi)容;
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
模塊可以多次加載,但只會在第一次加載的時候運行一次,然后運行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果;模塊的加載順序,按照代碼的出現(xiàn)順序是同步加載的;
require(同步加載)基本功能:讀取并執(zhí)行一個JS文件,然后返回該模塊的exports對象,如果沒有發(fā)現(xiàn)指定模塊會報錯;
exports:node為每個模塊提供一個exports變量,其指向module.exports,相當于在模塊頭部加了這句話:var exports = module.exports,在對外輸出時,可以給exports對象添加方法(exports.xxx等同于module.exports.xxx),不能直接賦值(因為這樣就切斷了exports和module.exports的聯(lián)系);
module變量代表當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。
module對象的屬性:
module.id模塊的識別符,通常是帶有絕對路徑的模塊文件名。
module.filename 模塊的文件名,帶有絕對路徑。
module.loaded 返回一個布爾值,表示模塊是否已經(jīng)完成加載。
module.parent 返回一個對象,表示調(diào)用該模塊的模塊。
module.children 返回一個數(shù)組,表示該模塊要用到的其他模塊。
module.exports 表示模塊對外輸出的值。
例子:
注意在這種方式下module.exports被重新賦值了,所以之前使用exports導出的hello不再有效(模塊頭部var exports = module.exports)
exports.hello = function() { return "hello"; }; module.exports = "Hello world";/
因此一旦module.exports被賦值了,表明這個模塊具有單一出口了
AMDAsynchronous Module Definition異步加載某模塊的規(guī)范。試想如果在瀏覽器中(資源不再本地)采用commonjs這種完全依賴于先加載再試用方法,那么如果一個模塊特別大,網(wǎng)速特別慢的情況下就會出現(xiàn)頁面卡頓的情況。便有了異步加載模塊的AMD規(guī)范。require.js便是基于此規(guī)范
require(["module1","module2"....], callback); reqire([jquery],function(jquery){ //do something }) //定義模塊 define(id, [depends], callback); //id是模塊名,可選的依賴別的模塊的數(shù)組,callback是用于return出一個給別的模塊用的函數(shù)
熟悉的回調(diào)函數(shù)形式。
Node的模塊實現(xiàn)Node 對于模塊的實現(xiàn)以commonjs為基礎(chǔ)的同時也增加了許多自身的特性
Node模塊的引入的三個步驟
路徑分析
文件定位
在require參數(shù)中如果不寫后綴名,node會按照.js,.node,.json的順序依次補足并try
此過程會調(diào)用fs模塊同步阻塞式的判斷文件是否存在,因此非js文件最后加上后綴
編譯執(zhí)行
.js 文件會被解析為 JavaScript 文本文件,.json 文件會被解析為 JSON 文本文件。 .node 文件會被解析為通過 dlopen 加載的編譯后的插件模塊.
Node的模塊分類
核心模塊 Node本身提供的模塊,比如path,buffer,http等,在Node編譯過程中就加載進內(nèi)存,因此會省掉文件定位和編譯執(zhí)行兩個文件加載步驟
文件模塊 開發(fā)人員自己寫的模塊,會經(jīng)歷完整的模塊引入步驟
Node也會優(yōu)先從緩存中加載引入過的文件模塊,在Node中第一次加載某一個模塊的時候,Node就會緩存下該模塊,之后再加載模塊就會直接從緩存中取了。這個“潛規(guī)則”核心模塊和文件模塊都會有。
require("./test.js").message="hello" console.log(require.cache); console.log(require("./test.js").message)//hello
上述代碼說明第二次加載依舊使用了第一次加載進來之后的模塊并沒有重新加載而是讀取了緩存中的模塊,因為重新加載的某塊中并沒有message。打印出來的require.cache包含了本模塊的module信息和加載進來的模塊信息。
那么如果你想要多次執(zhí)行某一個模塊,要么你手動像下面這樣刪除該模塊的緩存記錄之后再重新加載使用,要么應(yīng)該在模塊中暴露一個工廠函數(shù),然后調(diào)用那個函數(shù)多次執(zhí)行該模塊,與vue-ssr的創(chuàng)建應(yīng)用實例的工廠函數(shù)意思相近。
require("./test.js").message="hello" delete require.cache["/absolute-path/test.js"] console.log(require("./test.js").message)//undifined
可見當刪除了相關(guān)模塊的緩存,再一次加載時則不再有message了。
// Vue-ssr工廠函數(shù),目的是為每個請求創(chuàng)立一個新的應(yīng)用實例 const Vue = require("vue") module.exports = function createApp (context) { return new Vue({ data: { url: context.url }, template: `訪問的 URL 是: {{ url }}` }) }
模塊包裝器
Node在加載模塊之后,執(zhí)行之前則會使用函數(shù)包裝器將模塊代碼包裝,從而實現(xiàn)將頂層變量(var,let,const)作用域限制在模塊范圍內(nèi),提供每一個特定在該模塊的頂層全局變量module,exports,__dirname(所在文件夾的絕對路徑),__filename(絕對路徑加上文件名)
(function(exports, require, module, __filename, __dirname) { // 模塊的代碼實際上在這里 });
關(guān)于模塊的具體編譯執(zhí)行過程,這次就不深入討論了,足夠花心思在好好重新深入總結(jié)重寫一篇了,順便再次安利樸靈大大的《深入淺出nodejs》
ES6中模塊的解決方案終于,ES6在語言層面上提供了JS一直都沒有的模塊功能,使得在繼Commonjs之于服務(wù)端,AMD之于瀏覽器之外提供了一個通用的解決方案。
1.設(shè)計思想
盡量靜態(tài)化(靜態(tài)加載),使得編譯時就能確定模塊間的依賴關(guān)系以及輸入輸出的變量。
2.關(guān)鍵語法
export
export可以輸出變量:export var a = 1
輸出函數(shù):`export function sum(x, y) {
return x + y;
};
`
輸出類:export class A{}
結(jié)尾大括號寫法:export {a , sum , A}
尤為注意的一點就是export所導出的接口一定要和模塊內(nèi)部的變量建立一一對應(yīng)的關(guān)系
對于一個模塊來說,它就是一個默認使用了嚴格模式的文件("use strict"),而別的文件要想使用該模塊,就必須要求該模塊內(nèi)有export主動導出的內(nèi)容
例子:
export 1 //直接導出一個數(shù)字是不可以的 var a= 2 export a //間接導出數(shù)字也是不可以的! export {a}//正確 export function(){} //錯誤 function sum(){} export sum //錯誤 export {sum}//正確
export個人最為重要的一點就是可以取到模塊內(nèi)的實時的值
例子:
export var foo = "bar"; setTimeout(() => foo = "baz", 500);
引用該模塊的文件在定時器時間到的時候則會得到改變后的值
export default
實質(zhì): 導出一個叫做default(默認的)變量,本質(zhì)是將后面的值,賦給default變量,所以情況就和export 不同了
不同點:
export 導出的變量,在import的時候必須要知道變量名,否則無法加載,export default就允許隨意取名直接加載,并且不用使用大括號;
export default 后面不能跟變量聲明語句
// 第一組 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 每一個模塊只允許有一個
import
與導出export對應(yīng),引用則是import
export {a,b} || / import { a as A ,b as B} from "./test.js";
主要特點:
使用import加載具有提升的效果,即會提到文件頭部進行:
foo(); import { foo } from "my_module";
該代碼會正常執(zhí)行。
*加載默認加載全部導出的變量
import * as A from "./a.js"
import 加載進來的變量是不允許改變的。
瀏覽器對ES6模塊的加載type="module",此時瀏覽器就會知道這是ES6模塊,同時會自動給他加上前文提到的defer屬性,即等到所有的渲染操作都執(zhí)行完成之后,才會執(zhí)行該模塊
Node 對ES6模塊的加載
由于Node有自己的模塊加載機制,所以在Node8.5以上版本將兩種方式的加載分開來處理,對于加載ES6的模塊,node要求其后綴名得是.mjs,然后還得加上--experimental-modules參數(shù),然后兩種機制還不能混用。確實還是很麻煩的,所以現(xiàn)在Node端想用import主流還是用babel轉(zhuǎn)義。
對比ES6 module和Node的commonjs 差異:靜態(tài)加載VS運行時加載
首先看下面一段代碼:
if (x > 2) { import A from "./a.js"; }else{ import B from "./b.js"; }
這段代碼會報錯,因為JS引擎在處理import是在編譯時期,此時不會去執(zhí)行條件語句,因此這段代碼會出現(xiàn)句法錯誤,相反,如果換成:
if (x > 2) { const A =require("./a.js"); }else{ const B =require("./b.js"); }
commonjs是在運行時加載模塊,因此上面代碼就會成功運行
由于動態(tài)加載功能的要求,才會有了import()函數(shù)的提案,這里就不過多贅述。
值的引用VS值的拷貝
commonjs模塊在加載之后會把原始類型的值緩存,之后該模塊的內(nèi)部變化則不會再影響到其輸出的值:
//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的模塊機制,在引擎靜態(tài)分析階段會把import當成是一種只讀引用(地址是只讀的const,因此不可以在引用該模塊的文件里給他重新賦值),等到代碼實際運行時,才會根據(jù)引用去取值
// 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循環(huán)加載問題
循環(huán)加載指的是,a文件依賴于b文件,而b文件又依賴于a文件
commonjs的循環(huán)加載問題
commonjs是在加載時執(zhí)行的,他在require的時候就會全部跑一遍,因此他在遇到循環(huán)加載的情況就會只輸出已經(jīng)執(zhí)行的部分,而之后的部分則不會輸出,下面是一個例子:
//parent文件 exports.flag = 1; let children = require("./children")//停下來,加載chilren console.log(`parent文件中chilren的flag =${children.flag}`); exports.flag = 2 console.log(`parent文件執(zhí)行完畢了`); ========================================================= //test2文件 exports.flag = 1; let parent = require("./parent")//停下來,加載parent,此時parent只執(zhí)行到了第一行,導出結(jié)果flag ==1 console.log(`children文件中parent的flag =${parent.flag}`); exports.flag = 2 console.log(`children文件執(zhí)行完畢了`);
node parent之后運行結(jié)果為
運行parent之后會在第一行導出flag=1,然后去ruquirechildren文件,此時parent進行等待,等待children文件執(zhí)行結(jié)束,children開始執(zhí)行到第二行的時候出現(xiàn)“循環(huán)加載”parent文件,此時系統(tǒng)自動去找parent文件的exports屬性,而parent只執(zhí)行了一行,但是好在它有exports了flag,所以children文件加再進來了那個flag并繼續(xù)執(zhí)行,第三行不會報錯,最后在第四行children導出了flag=2,此時parent再接著執(zhí)行到結(jié)束。
ES6中的循環(huán)加載問題
ES6和commonjs本質(zhì)上不同!因為ES6是引用取值,即動態(tài)引用
引用阮一峰老師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";
執(zhí)行后的結(jié)果:
執(zhí)行的過程是當a文件防線import了b文件之后就會去執(zhí)行b文件,到了b文件這邊看到了他又引用了a文件,并不會又去執(zhí)行a文件發(fā)生“張郎送李郎”的故事,而是倔強得認為foo這個接口已經(jīng)存在了,于是就繼續(xù)執(zhí)行下去,直到在要引用foo的時候發(fā)現(xiàn)foo還沒有定義,因為let定義變量會出現(xiàn)"暫時性死區(qū)",不可以還沒定義就使用,其實如果改成var聲明,有個變量提升作用就不會報錯了。改成var聲明fooexport let foo = "foo";
雖然打印的foo是undifined但是并沒有影響程序執(zhí)行,但最好的做法是,改成同樣有提升作用的function來聲明。最后去執(zhí)行函數(shù)來獲得值,最后得到了希望的結(jié)果
// 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" };結(jié)束語
其實關(guān)于模塊還有很多東西還沒有梳理總結(jié)到,比如node模塊的加載過程的細節(jié),和編譯過程,再比如如何自己寫一個npm模塊發(fā)布等等都是很值得去梳理總結(jié)的,這一次就先到這吧,總之,第一次在自己的SF正兒八經(jīng)的寫這么長的技術(shù)總結(jié)博客,組織內(nèi)容上感覺比較凌亂,還有很多的不足。希望自己以后多多總結(jié)提高吧。最后當然還是要感謝開源,感謝提供了那么多優(yōu)秀資料的前輩們。也歡迎來我的博客網(wǎng)站(https://isliulei.com)指教。
參考文章:
ES6標準入門--阮一峰
Nodejs v8.9.4 官方文檔
《深入淺出Nodejs》---樸靈
Commonjs規(guī)范
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/92818.html
摘要:插件開發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實現(xiàn)文件分片斷點續(xù)傳。 Vue.js 插件開發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強大而且簡單的方式。插....
摘要:一概述集合是引入的新的內(nèi)置對象類型,其特點同數(shù)學意義的集合,即集合內(nèi)所有元素不重復(fù)元素唯一。數(shù)組集合對比數(shù)組和集合,數(shù)組可以加入重復(fù)數(shù)據(jù),而集合的所有元素是唯一的不允許重復(fù)。因此,適合臨時存放一組對象,以及存放跟對象綁定的信息。 本文同步帶你入門 帶你入門 JavaScript ES6 (五) 集合,轉(zhuǎn)載請注明出處。 前面我們學習了: for of 變量和擴展語法 塊作用域變量和解構(gòu)...
摘要:引擎對堆內(nèi)存中的對象進行分代管理新生代存活周期較短的對象,如臨時變量字符串等。內(nèi)存泄漏對于持續(xù)運行的服務(wù)進程,必須及時釋放不再用到的內(nèi)存。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導) 本周正式開始前端進階的第一期,本周的主題是調(diào)用堆棧,今天是第4天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃...
摘要:模塊化是隨著前端技術(shù)的發(fā)展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調(diào)也不等同于異步。將會討論安全的類型檢測惰性載入函數(shù)凍結(jié)對象定時器等話題。 Vue.js 前后端同構(gòu)方案之準備篇——代碼優(yōu)化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術(shù)。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
閱讀 1991·2021-09-26 10:19
閱讀 3267·2021-09-24 10:25
閱讀 1654·2019-12-27 11:39
閱讀 1937·2019-08-30 15:43
閱讀 683·2019-08-29 16:08
閱讀 3515·2019-08-29 16:07
閱讀 915·2019-08-26 11:30
閱讀 1279·2019-08-26 10:41