
我們常說node并不是一門新的程式設計語言,他隻是javascript的運作時,運作時你可以簡單地了解為運作javascript的環境。在大多數情況下我們會在浏覽器中去運作javascript,有了node的出現,我們可以在node中去運作javascript,這意味着哪裡安裝了node或者浏覽器,我們就可以在哪裡運作javascript。
1.node子產品化的實作
node中是自帶子產品化機制的,每個檔案就是一個單獨的子產品,并且它遵循的是CommonJS規範,也就是使用require的方式導入子產品,通過module.export的方式導出子產品。
node子產品的運作機制也很簡單,其實就是在每一個子產品外層包裹了一層函數,有了函數的包裹就可以實作代碼間的作用域隔離。
你可能會說,我在寫代碼的時候并沒有包裹函數呀,是的的确如此,這一層函數是node自動幫我們實作的,我們可以來測試一下。
我們建立一個js檔案,在第一行列印一個并不存在的變量,比如我們這裡列印window,在node中是沒有window的。
console.log(window);
複制代碼
通過node執行該檔案,會發現報錯資訊如下。(請使用系統預設cmd執行指令)。
(function (exports, require, module, __filename, __dirname) { console.log(window);
ReferenceError: window is not defined
at Object.<anonymous> (/Users/choice/Desktop/node/main.js:1:75)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
at startup (internal/bootstrap/node.js:279:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:752:3)
複制代碼
可以看到報錯的頂層有一個自執行的函數,, 函數中包含exports, require, module, __filename, __dirname這些我們常用的全局變量。
我在之前的《前端子產品化發展曆程》一文中介紹過。自執行函數也是前端子產品化的實作方案之一,在早期前端沒有子產品化系統的時代,自執行函數可以很好的解決命名空間的問題,并且子產品依賴的其他子產品都可以通過參數傳遞進來。cmd和amd規範也都是依賴自執行函數實作的。
在子產品系統中,每個檔案就是一個子產品,每個子產品外面會自動套一個函數,并且定義了導出方式 module.exports或者exports,同時也定義了導入方式require。
let moduleA = (function() {
module.exports = Promise;
return module.exports;
})();
複制代碼
2.require加載子產品
require依賴node中的fs子產品來加載子產品檔案,fs.readFile讀取到的是一個字元串。
在javascrpt中我們可以通過eval或者new Function的方式來将一個字元串轉換成js代碼來運作。
- eval
const name = 'yd';
const str = 'const a = 123; console.log(name)';
eval(str); // yd;
複制代碼
- new Function
new Function接收的是一個要執行的字元串,傳回的是一個新的函數,調用這個新的函數字元串就會執行了。如果這個函數需要傳遞參數,可以在new Function的時候依次傳入參數,最後傳入的是要執行的字元串。比如這裡傳入參數b,要執行的字元串str。
const b = 3;
const str = 'let a = 1; return a + b';
const fun = new Function('b', str);
console.log(fun(b, str)); // 4
複制代碼
可以看到eval和Function執行個體化都可以用來執行javascript字元串,似乎他們都可以來實作require子產品加載。不過在node中并沒有選用他們來實作子產品化,原因也很簡單因為他們都有一個緻命的問題,就是都容易被不屬于他們的變量所影響。
如下str字元串中并沒有定義a,但是确可以使用上面定義的a變量,這顯然是不對的,在子產品化機制中,str字元串應該具有自身獨立的運作空間,自身不存在的變量是不可以直接使用的。
const a = 1;
const str = 'console.log(a)';
eval(str);
const func = new Function(str);
func();
複制代碼
node存在一個vm虛拟環境的概念,用來運作額外的js檔案,他可以保證javascript執行的獨立性,不會被外部所影響。
- vm 内置子產品
雖然我們在外部定義了hello,但是str是一個獨立的子產品,并不在村hello變量,是以會直接報錯。
// 引入vm子產品, 不需要安裝,node 自模組化塊
const vm = require('vm');
const hello = 'yd';
const str = 'console.log(hello)';
wm.runInThisContext(str); // 報錯
複制代碼
是以node執行javascript子產品時可以采用vm來實作。就可以保證子產品的獨立性了。
3.require代碼實作
介紹require代碼實作之前先來回顧兩個node子產品的用法,因為下面會用得到。
- path子產品
用于處理檔案路徑。
basename: 基礎路徑, 有檔案路徑就不是基礎路徑,基礎路勁是
1.js
extname: 擷取擴充名
dirname: 父級路勁
join: 拼接路徑
resolve: 目前檔案夾的絕對路徑,注意使用的時候不要在結尾添加
/
__dirname: 目前檔案所在檔案夾的路徑
__filename: 目前檔案的絕對路徑
const path = require('path', 's');
console.log(path.basename('1.js'));
console.log(path.extname('2.txt'));
console.log(path.dirname('2.txt'));
console.log(path.join('a/b/c', 'd/e/f')); // a/b/c/d/e/
console.log(path.resolve('2.txt'));
複制代碼
- fs子產品
用于操作檔案或者檔案夾,比如檔案的讀寫,新增,删除等。常用方法有readFile和readFileSync,分别是異步讀取檔案和同步讀取檔案。
const fs = require('fs');
const buffer = fs.readFileSync('./name.txt', 'utf8'); // 如果不傳入編碼,出來的是二進制
console.log(buffer);
複制代碼
fs.access: 判斷是否存在,node10提供的,exists方法已經被廢棄, 原因是不符合node規範,是以我們采用access來判斷檔案是否存在。
try {
fs.accessSync('./name.txt');
} catch(e) {
// 檔案不存在
}
複制代碼
4.手動實作require子產品加載器
首先導入依賴的子產品path,fs, vm, 并且建立一個Require函數,這個函數接收一個modulePath參數,表示要導入的檔案路徑。
// 導入依賴
const path = require('path'); // 路徑操作
const fs = require('fs'); // 檔案讀取
const vm = require('vm'); // 檔案執行
// 定義導入類,參數為子產品路徑
function Require(modulePath) {
...
}
複制代碼
在Require中擷取到子產品的絕對路徑,友善使用fs加載子產品,這裡讀取子產品内容我們使用new Module來抽象,使用tryModuleLoad來加載子產品内容,Module和tryModuleLoad我們稍後實作,Require的傳回值應該是子產品的内容,也就是module.exports。
// 定義導入類,參數為子產品路徑
function Require(modulePath) {
// 擷取目前要加載的絕對路徑
let absPathname = path.resolve(__dirname, modulePath);
// 建立子產品,建立Module執行個體
const module = new Module(absPathname);
// 加載目前子產品
tryModuleLoad(module);
// 傳回exports對象
return module.exports;
}
複制代碼
Module的實作很簡單,就是給子產品建立一個exports對象,tryModuleLoad執行的時候将内容加入到exports中,id就是子產品的絕對路徑。
// 定義子產品, 添加檔案id辨別和exports屬性
function Module(id) {
this.id = id;
// 讀取到的檔案内容會放在exports中
this.exports = {};
}
複制代碼
之前我們說過node子產品是運作在一個函數中,這裡我們給Module挂載靜态屬性wrapper,裡面定義一下這個函數的字元串,wrapper是一個數組,數組的第一個元素就是函數的參數部分,其中有exports,module. Require,__dirname, __filename, 都是我們子產品中常用的全局變量。注意這裡傳入的Require參數是我們自己定義的Require。
第二個參數就是函數的結束部分。兩部分都是字元串,使用的時候我們将他們包裹在子產品的字元串外部就可以了。
Module.wrapper = [
"(function(exports, module, Require, __dirname, __filename) {",
"})"
]
複制代碼
_extensions用于針對不同的子產品擴充名使用不同的加載方式,比如JSON和javascript加載方式肯定是不同的。JSON使用JSON.parse來運作。
javascript使用vm.runInThisContext來運作,可以看到fs.readFileSync傳入的是module.id也就是我們Module定義時候id存儲的是子產品的絕對路徑,讀取到的content是一個字元串,我們使用Module.wrapper來包裹一下就相當于在這個子產品外部又包裹了一個函數,也就實作了私有作用域。
使用call來執行fn函數,第一個參數改變運作的this我們傳入module.exports,後面的參數就是函數外面包裹參數exports, module, Require, __dirname, __filename
Module._extensions = {
'.js'(module) {
const content = fs.readFileSync(module.id, 'utf8');
const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
const fn = vm.runInThisContext(fnStr);
fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
},
'.json'(module) {
const json = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(json); // 把檔案的結果放在exports屬性上
}
}
複制代碼
tryModuleLoad函數接收的是子產品對象,通過path.extname來擷取子產品的字尾名,然後使用Module._extensions來加載子產品。
// 定義子產品加載方法
function tryModuleLoad(module) {
// 擷取擴充名
const extension = path.extname(module.id);
// 通過字尾加載目前子產品
Module._extensions[extension](module);
}
複制代碼
至此Require加載機制我們基本就寫完了,我們來重新看一下。Require加載子產品的時候傳入子產品名稱,在Require方法中使用
path.resolve(__dirname, modulePath)
擷取到檔案的絕對路徑。然後通過new Module執行個體化的方式建立module對象,将子產品的絕對路徑存儲在module的id屬性中,在module中建立exports屬性為一個json對象。
使用tryModuleLoad方法去加載子產品,tryModuleLoad中使用
path.extname
擷取到檔案的擴充名,然後根據擴充名來執行對應的子產品加載機制。
最終将加載到的子產品挂載module.exports中。tryModuleLoad執行完畢之後module.exports已經存在了,直接傳回就可以了。
// 導入依賴
const path = require('path'); // 路徑操作
const fs = require('fs'); // 檔案讀取
const vm = require('vm'); // 檔案執行
// 定義導入類,參數為子產品路徑
function Require(modulePath) {
// 擷取目前要加載的絕對路徑
let absPathname = path.resolve(__dirname, modulePath);
// 建立子產品,建立Module執行個體
const module = new Module(absPathname);
// 加載目前子產品
tryModuleLoad(module);
// 傳回exports對象
return module.exports;
}
// 定義子產品, 添加檔案id辨別和exports屬性
function Module(id) {
this.id = id;
// 讀取到的檔案内容會放在exports中
this.exports = {};
}
// 定義包裹子產品内容的函數
Module.wrapper = [
"(function(exports, module, Require, __dirname, __filename) {",
"})"
]
// 定義擴充名,不同的擴充名,加載方式不同,實作js和json
Module._extensions = {
'.js'(module) {
const content = fs.readFileSync(module.id, 'utf8');
const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
const fn = vm.runInThisContext(fnStr);
fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
},
'.json'(module) {
const json = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(json); // 把檔案的結果放在exports屬性上
}
}
// 定義子產品加載方法
function tryModuleLoad(module) {
// 擷取擴充名
const extension = path.extname(module.id);
// 通過字尾加載目前子產品
Module._extensions[extension](module);
}
複制代碼
5.給子產品添加緩存
添加緩存也比較簡單,就是檔案加載的時候将檔案放入緩存在,再去加載子產品時先看緩存中是否存在,如果存在直接使用,如果不存在再去重新嘉愛,加載之後再放入緩存。
// 定義導入類,參數為子產品路徑
function Require(modulePath) {
// 擷取目前要加載的絕對路徑
let absPathname = path.resolve(__dirname, modulePath);
// 從緩存中讀取,如果存在,直接傳回結果
if (Module._cache[absPathname]) {
return Module._cache[absPathname].exports;
}
// 嘗試加載目前子產品
tryModuleLoad(module);
// 建立子產品,建立Module執行個體
const module = new Module(absPathname);
// 添加緩存
Module._cache[absPathname] = module;
// 加載目前子產品
tryModuleLoad(module);
// 傳回exports對象
return module.exports;
}
複制代碼
6.自動補全路徑
自動給子產品添加字尾名,實作省略字尾名加載子產品,其實也就是如果檔案沒有字尾名的時候周遊一下所有的字尾名看一下檔案是否存在。
// 定義導入類,參數為子產品路徑
function Require(modulePath) {
// 擷取目前要加載的絕對路徑
let absPathname = path.resolve(__dirname, modulePath);
// 擷取所有字尾名
const extNames = Object.keys(Module._extensions);
let index = 0;
// 存儲原始檔案路徑
const oldPath = absPathname;
function findExt(absPathname) {
if (index === extNames.length) {
return throw new Error('檔案不存在');
}
try {
fs.accessSync(absPathname);
return absPathname;
} catch(e) {
const ext = extNames[index++];
findExt(oldPath + ext);
}
}
// 遞歸追加字尾名,判斷檔案是否存在
absPathname = findExt(absPathname);
// 從緩存中讀取,如果存在,直接傳回結果
if (Module._cache[absPathname]) {
return Module._cache[absPathname].exports;
}
// 嘗試加載目前子產品
tryModuleLoad(module);
// 建立子產品,建立Module執行個體
const module = new Module(absPathname);
// 添加緩存
Module._cache[absPathname] = module;
// 加載目前子產品
tryModuleLoad(module);
// 傳回exports對象
return module.exports;
}
複制代碼
7.分析實作步驟
- 1.導入相關子產品,建立一個Require方法。
- 2.抽離通過Module._load方法,用于加載子產品。
- 3.Module.resolveFilename 根據相對路徑,轉換成絕對路徑。
- 4.緩存子產品 Module._cache,同一個子產品不要重複加載,提升性能。
- 5.建立子產品 id: 儲存的内容是 exports = {}相當于this。
- 6.利用tryModuleLoad(module, filename) 嘗試加載子產品。
- 7.Module._extensions使用讀取檔案。
- 8.Module.wrap: 把讀取到的js包裹一個函數。
- 9.将拿到的字元串使用runInThisContext運作字元串。
- 10.讓字元串執行并将this改編成exports。