本篇文章給大家整分享一些關(guān)于node模塊的面試題,希望幫助你快速了解常見(jiàn)的模塊問(wèn)題的坑點(diǎn),順利通過(guò)面試。
(資料圖)
node.js緩存和熱更新密切相關(guān),我們先簡(jiǎn)單看下Node.js的模塊機(jī)制(下圖來(lái)源于hyj1991大神)。
簡(jiǎn)單的說(shuō)就是,require A模塊之后,會(huì)把A模塊放入到緩存里,第二次取的時(shí)候就取緩存了,所以你僅僅改變了文件并不會(huì)讓這個(gè)文件重新加載,所以我們就需要把緩存去掉,讓這個(gè)文件能重新加載?!鞠嚓P(guān)教程推薦:nodejs視頻教程、編程教學(xué)】
簡(jiǎn)單地說(shuō)父模塊 A 引入子模塊 B 的步驟如下:
判斷子模塊 B 緩存是否存在如果不存在則對(duì) B 進(jìn)行編譯解析添加 B 模塊緩存至require.cache(其中 key 為模塊 B 的全路徑)添加 B 模塊引用至父模塊 A 的 children數(shù)組中如果存在,判斷父模塊 A 的 children數(shù)組中是否存在 B,如不存在則添加 B 模塊引用。所以在node.js做熱更新是十分麻煩的,一些庫(kù)做的也不夠好。這個(gè)問(wèn)題的終極解決方案是借助一些第三方工具,例如k8s,k8s可以輕松實(shí)現(xiàn)滾動(dòng)升級(jí),也就是如果要做熱更新,k8s會(huì)把新的服務(wù)起起來(lái),然后把流量切換到新的服務(wù)上(pod上),然后老服務(wù)再關(guān)閉。
不過(guò)熱更新 json 之類的配置文件的話, 還是可以簡(jiǎn)單的實(shí)現(xiàn)的, 可以直接存到后端的數(shù)據(jù)庫(kù)里,這樣就避免node.js的緩存問(wèn)題了。
模塊機(jī)制太常見(jiàn)的問(wèn)題了,下面我們需要搞清楚什么是commonjs的實(shí)現(xiàn)原理。
模塊機(jī)制詳細(xì)分析文章可以參考我之前寫的一篇文章NodeJS有難度的面試題,你能答對(duì)幾個(gè)?,最開(kāi)始就討論了commonjs的模塊機(jī)制。
我們這里粘貼一下我之前對(duì)于模塊機(jī)制的討論:
Node中,每個(gè)文件模塊都是一個(gè)對(duì)象,它的定義如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; this.filename = null; this.loaded = false; this.children = [];}module.exports = Module;var module = new Module(filename, parent);
所有的模塊都是 Module 的實(shí)例??梢钥吹剑?dāng)前模塊(module.js)也是 Module 的一個(gè)實(shí)例。
這道題基本上就可以了解到面試者對(duì)Node模塊機(jī)制的了解程度 基本上面試提到
1、先計(jì)算模塊路徑2、如果模塊在緩存里面,取出緩存3、加載模塊4、的輸出模塊的exports屬性即可
// require 其實(shí)內(nèi)部調(diào)用 Module._load 方法Module._load = function(request, parent, isMain) { // 計(jì)算絕對(duì)路徑 var filename = Module._resolveFilename(request, parent); // 第一步:如果有緩存,取出緩存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否為內(nèi)置模塊 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } /********************************這里注意了**************************/ // 第三步:生成模塊實(shí)例,存入緩存 // 這里的Module就是我們上面的1.1定義的Module var module = new Module(filename, parent); Module._cache[filename] = module; /********************************這里注意了**************************/ // 第四步:加載模塊 // 下面的module.load實(shí)際上是Module原型上有一個(gè)方法叫Module.prototype.load try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:輸出模塊的exports屬性 return module.exports;};接著上一題繼續(xù)發(fā)問(wèn)
// 上面(1.2部分)的第四步module.load(filename)// 這一步,module模塊相當(dāng)于被包裝了,包裝形式如下// 加載js模塊,相當(dāng)于下面的代碼(加載node模塊和json模塊邏輯不一樣)(function (exports, require, module, __filename, __dirname) { // 模塊源碼 // 假如模塊代碼如下 var math = require("math"); exports.area = function(radius){ return Math.PI * radius * radius }});也就是說(shuō),每個(gè)module里面都會(huì)傳入__filename, __dirname參數(shù),這兩個(gè)參數(shù)并不是module本身就有的,是外界傳入的
exports其實(shí)就是module.exports其實(shí)1.3問(wèn)題的代碼已經(jīng)說(shuō)明問(wèn)題了,接著我引用廖雪峰大神的講解,希望能講的更清楚
很多時(shí)候,你會(huì)看到,在Node環(huán)境中,有兩種方法可以在一個(gè)模塊中輸出變量:
方法一:對(duì)module.exports賦值:
// hello.jsfunction hello() { console.log("Hello, world!");}function greet(name) { console.log("Hello, " + name + "!");}module.exports = { hello: hello, greet: greet};方法二:直接使用exports:
// hello.jsfunction hello() { console.log("Hello, world!");}function greet(name) { console.log("Hello, " + name + "!");}function hello() { console.log("Hello, world!");}exports.hello = hello;exports.greet = greet;但是你不可以直接對(duì)exports賦值:
// 代碼可以執(zhí)行,但是模塊并沒(méi)有輸出任何變量:exports = { hello: hello, greet: greet};如果你對(duì)上面的寫法感到十分困惑,不要著急,我們來(lái)分析Node的加載機(jī)制:
首先,Node會(huì)把整個(gè)待加載的hello.js文件放入一個(gè)包裝函數(shù)load中執(zhí)行。在執(zhí)行這個(gè)load()函數(shù)前,Node準(zhǔn)備好了module變量:
var module = { id: "hello", exports: {}};load()函數(shù)最終返回module.exports:var load = function (exports, module) { // hello.js的文件內(nèi)容 ... // load函數(shù)返回: return module.exports;};var exportes = load(module.exports, module);也就是說(shuō),默認(rèn)情況下,Node準(zhǔn)備的exports變量和module.exports變量實(shí)際上是同一個(gè)變量,并且初始化為空對(duì)象{},于是,我們可以寫:
exports.foo = function () { return "foo"; };exports.bar = function () { return "bar"; };也可以寫:
module.exports.foo = function () { return "foo"; };module.exports.bar = function () { return "bar"; };換句話說(shuō),Node默認(rèn)給你準(zhǔn)備了一個(gè)空對(duì)象{},這樣你可以直接往里面加?xùn)|西。
但是,如果我們要輸出的是一個(gè)函數(shù)或數(shù)組,那么,只能給module.exports賦值:
module.exports = function () { return "foo"; };給exports賦值是無(wú)效的,因?yàn)橘x值后,module.exports仍然是空對(duì)象{}。
結(jié)論如果要輸出一個(gè)鍵值對(duì)象{},可以利用exports這個(gè)已存在的空對(duì)象{},并繼續(xù)在上面添加新的鍵值;
如果要輸出一個(gè)函數(shù)或數(shù)組,必須直接對(duì)module.exports對(duì)象賦值。
所以我們可以得出結(jié)論:直接對(duì)module.exports賦值,可以應(yīng)對(duì)任何情況:
module.exports = { foo: function () { return "foo"; }};或者:
module.exports = function () { return "foo"; };最終,我們強(qiáng)烈建議使用module.exports = xxx的方式來(lái)輸出模塊變量,這樣,你只需要記憶一種方法。
通過(guò)上面的問(wèn)題,面試官又拋出一個(gè)問(wèn)題,每個(gè)require的js文件,作用域如何保證獨(dú)立呢?
其實(shí)每一個(gè)require的js文件,本身就是一個(gè)字符串, 文件是不是字符串嘛,所以我們需要一種機(jī)制能夠把字符串編譯為可以運(yùn)行的javascript語(yǔ)言。
實(shí)際上從上面的討論我們知道,require會(huì)把引入的js包裹在function中,所以它的作用域天然就是獨(dú)立的。
接著講本章的vm模塊,vm模塊和function都可以建立自己獨(dú)立的作用域,并且vm、function、eval還可以把字符串當(dāng)做目標(biāo)代碼執(zhí)行。所以這三者的區(qū)別就需要面試者了解。
evalFunctionvmeval、Function,在執(zhí)行目標(biāo)代碼時(shí),會(huì)有一個(gè)最大的問(wèn)題就是安全性,無(wú)論如何目標(biāo)代碼不能影響我正常的服務(wù),也就是說(shuō),這個(gè)執(zhí)行環(huán)境得是一個(gè)沙盒環(huán)境,而eval顯然并不具備這個(gè)能力。如果需要一段不信任的代碼放任它執(zhí)行,那么不光服務(wù),整個(gè)服務(wù)器的文件系統(tǒng)、數(shù)據(jù)庫(kù)都暴露了。甚至目標(biāo)代碼會(huì)修改eval函數(shù)原型,埋入陷阱等等。
function也有一個(gè)安全問(wèn)題就是可以修改全局變量,所有這種new Function的代碼執(zhí)行時(shí)的作用域?yàn)槿肿饔糜?,不論它的在哪個(gè)地方調(diào)用的,它訪問(wèn)的都是全局變量。
所以也有一定的安全隱患,接下來(lái)我們的主角vm模塊登場(chǎng)。
使用vm的模塊會(huì)比eval更為安全,因?yàn)関m模塊運(yùn)行的腳本完全無(wú)權(quán)訪問(wèn)外部作用域(或自行設(shè)置一個(gè)有限的作用域)。 腳本仍在同一進(jìn)程中運(yùn)行,因此為了獲得最佳安全性。當(dāng)然你可以給上下文傳入一些通用的API方便開(kāi)發(fā):
vm.runInNewContext(` const util = require(‘util’); console.log(util);`, { require: require, console: console});此外,另一個(gè)開(kāi)源庫(kù)vm2針對(duì)vm的安全性等方面做了更多的提升,vm2。避免了一些運(yùn)行腳本有可能“逃出”沙盒運(yùn)行的邊緣情況,語(yǔ)法也跟易于上手,很推薦使用。
npm的包管理機(jī)制你一定要了解,不僅僅是node需要,我們前端瀏覽器項(xiàng)目本身也會(huì)引用很多第三方模塊。面試必備知識(shí)點(diǎn)。
下圖摘自抖音前端團(tuán)隊(duì)的npm包管理機(jī)制
本圖如果你理解的話,后面的內(nèi)容就不用看了。
講npm install 要從嵌套結(jié)構(gòu)講起
在 npm 的早期版本中,npm 處理依賴的方式簡(jiǎn)單粗暴,以遞歸的方式,嚴(yán)格按照 package.json 結(jié)構(gòu)以及子依賴包的 package.json 結(jié)構(gòu)將依賴安裝到他們各自的 node_modules 中。
如下圖:這樣的方式優(yōu)點(diǎn)很明顯, node_modules 的結(jié)構(gòu)和 package.json 結(jié)構(gòu)一一對(duì)應(yīng),層級(jí)結(jié)構(gòu)明顯,并且保證了每次安裝目錄結(jié)構(gòu)都是相同的。
從上圖這種情況,我們不難得出嵌套結(jié)構(gòu)擁有以下缺點(diǎn):
在不同層級(jí)的依賴中,可能引用了同一個(gè)模塊,導(dǎo)致大量冗余在 Windows 系統(tǒng)中,文件路徑最大長(zhǎng)度為260個(gè)字符,嵌套層級(jí)過(guò)深可能導(dǎo)致不可預(yù)知的問(wèn)題2016 年,yarn 誕生了。yarn 解決了 npm 幾個(gè)最為迫在眉睫的問(wèn)題:
安裝太慢(加緩存、多線程)嵌套結(jié)構(gòu)(扁平化)無(wú)依賴鎖(yarn.lock)yarn 帶來(lái)對(duì)的扁平化結(jié)構(gòu):如下圖,我們簡(jiǎn)單看下什么是扁平化的結(jié)構(gòu):
沒(méi)錯(cuò),這就是扁平化依賴管理的結(jié)果。相比之前的嵌套結(jié)構(gòu),現(xiàn)在的目錄結(jié)構(gòu)類似下面這樣:假如之前嵌套的結(jié)構(gòu)如下:
node_modules├─ a| ├─ index.js| |- node_modules -└─ b| | ├─ index.js| | └─ package.json| └─ package.json
那么扁平化處理以后,就編程下面這樣,被拍平了
node_modules├─ a| ├─ index.js| └─ package.json└─ b ├─ index.js └─ package.json
但是扁平化的結(jié)構(gòu)又會(huì)引出新的問(wèn)題:
最主要的就是依賴結(jié)構(gòu)的不確定性!
啥意思,我就懶得畫(huà)圖了,拿網(wǎng)上的一個(gè)例子來(lái)說(shuō):
想象一下有一個(gè) library-a,它同時(shí)依賴了 library-b、c、d、e:
而 b 和 c 依賴了 f@1.0.0,d 和 e 依賴了 f@2.0.0:
這時(shí)候,node_modules 樹(shù)需要做出選擇了,到底是將 f@1.0.0 還是 f@2.0.0 扁平化,然后將另一個(gè)放到嵌套的 node_modules 中?
答案是:具體做那種選擇將是不確定的,取決于哪一個(gè) f 出現(xiàn)得更靠前,靠前的那個(gè)將被扁平化。
還有一個(gè)問(wèn)題就是幽靈依賴,明明只安裝a包,你卻可以引用b包,因?yàn)閍引用了b,并且扁平化處理了。
這就是為啥要有l(wèi)ock文件的原因,lock文件可以保證安裝包的扁平化結(jié)構(gòu)的穩(wěn)定。
pnpm? 可以簡(jiǎn)單介紹一下為啥它能解決上面扁平化結(jié)構(gòu)和幽靈依賴的問(wèn)題。
答:不會(huì), 先執(zhí)行的導(dǎo)出其 未完成的副本, 通過(guò)導(dǎo)出工廠函數(shù)讓對(duì)方從函數(shù)去拿比較好避免. 模塊在導(dǎo)出的只是 var module = { exports: {...} }; 中的 exports。以下摘自阮一峰老師的博客:
CommonJS模塊的重要特性是加載時(shí)執(zhí)行,即腳本代碼在require的時(shí)候,就會(huì)全部執(zhí)行。CommonJS的做法是,一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會(huì)輸出。
讓我們來(lái)看,官方文檔里面的例子。腳本文件a.js代碼如下。
exports.done = false;var b = require("./b.js");console.log("在 a.js 之中,b.done = %j", b.done);exports.done = true;console.log("a.js 執(zhí)行完畢");上面代碼之中,a.js腳本先輸出一個(gè)done變量,然后加載另一個(gè)腳本文件b.js。注意,此時(shí)a.js代碼就停在這里,等待b.js執(zhí)行完畢,再往下執(zhí)行。
再看b.js的代碼。
exports.done = false;var a = require("./a.js");console.log("在 b.js 之中,a.done = %j", a.done);exports.done = true;console.log("b.js 執(zhí)行完畢");上面代碼之中,b.js執(zhí)行到第二行,就會(huì)去加載a.js,這時(shí),就發(fā)生了"循環(huán)加載"。系統(tǒng)會(huì)去a.js模塊對(duì)應(yīng)對(duì)象的exports屬性取值,可是因?yàn)閍.js還沒(méi)有執(zhí)行完,從exports屬性只能取回已經(jīng)執(zhí)行的部分,而不是最后的值。
a.js已經(jīng)執(zhí)行的部分,只有一行。
exports.done = false;因此,對(duì)于b.js來(lái)說(shuō),它從a.js只輸入一個(gè)變量done,值為false。
然后,b.js接著往下執(zhí)行,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給a.js。于是,a.js接著往下執(zhí)行,直到執(zhí)行完畢。我們寫一個(gè)腳本main.js,驗(yàn)證這個(gè)過(guò)程。
var a = require("./a.js");var b = require("./b.js");console.log("在 main.js 之中, a.done=%j, b.done=%j", a.done, b.done);執(zhí)行main.js,運(yùn)行結(jié)果如下。
$ node main.js
在 b.js 之中,a.done = falseb.js 執(zhí)行完畢在 a.js 之中,b.done = truea.js 執(zhí)行完畢在 main.js 之中, a.done=true, b.done=true
上面的代碼證明了兩件事。一是,在b.js之中,a.js沒(méi)有執(zhí)行完畢,只執(zhí)行了第一行。二是,main.js執(zhí)行到第二行時(shí),不會(huì)再次執(zhí)行b.js,而是輸出緩存的b.js的執(zhí)行結(jié)果,即它的第四行。
exports.done = true;
ES6模塊的運(yùn)行機(jī)制與CommonJS不一樣,它遇到模塊加載命令import時(shí),不會(huì)去執(zhí)行模塊,而是只生成一個(gè)引用。等到真的需要用到時(shí),再到模塊里面去取值。
因此,ES6模塊是動(dòng)態(tài)引用,不存在緩存值的問(wèn)題,而且模塊里面的變量,綁定其所在的模塊。請(qǐng)看下面的例子。
// m1.jsexport var foo = "bar";setTimeout(() => foo = "baz", 500);
// m2.jsimport {foo} from "./m1.js";console.log(foo);setTimeout(() => console.log(foo), 500);上面代碼中,m1.js的變量foo,在剛加載時(shí)等于bar,過(guò)了500毫秒,又變?yōu)榈扔赽az。
讓我們看看,m2.js能否正確讀取這個(gè)變化。
$ babel-node m2.jsbarbaz
上面代碼表明,ES6模塊不會(huì)緩存運(yùn)行結(jié)果,而是動(dòng)態(tài)地去被加載的模塊取值,以及變量總是綁定其所在的模塊。
這導(dǎo)致ES6處理"循環(huán)加載"與CommonJS有本質(zhì)的不同。ES6根本不會(huì)關(guān)心是否發(fā)生了"循環(huán)加載",只是生成一個(gè)指向被加載模塊的引用,需要開(kāi)發(fā)者自己保證,真正取值的時(shí)候能夠取到值。
請(qǐng)看下面的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)。
// a.jsimport {bar} from "./b.js";export function foo() { bar(); console.log("執(zhí)行完畢");}foo();// b.jsimport {foo} from "./a.js";export function bar() { if (Math.random() > 0.5) { foo(); }}按照CommonJS規(guī)范,上面的代碼是沒(méi)法執(zhí)行的。a先加載b,然后b又加載a,這時(shí)a還沒(méi)有任何執(zhí)行結(jié)果,所以輸出結(jié)果為null,即對(duì)于b.js來(lái)說(shuō),變量foo的值等于null,后面的foo()就會(huì)報(bào)錯(cuò)。
但是,ES6可以執(zhí)行上面的代碼。
$ babel-node a.js執(zhí)行完畢
a.js之所以能夠執(zhí)行,原因就在于ES6加載的變量,都是動(dòng)態(tài)引用其所在的模塊。只要引用是存在的,代碼就能執(zhí)行。
如果 a.js require 了 b.js, 那么在 b 中定義全局變量 t = 111 能否在 a 中直接打印出來(lái)?會(huì),作用域鏈的嘛。。。。
更多node相關(guān)知識(shí),請(qǐng)?jiān)L問(wèn):nodejs 教程!
以上就是【整理分享】一些node模塊相關(guān)的面試題及答案(收藏)的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!
關(guān)鍵詞: