NODE基礎總結(2) —— Module

Desmonddai583發表於2018-03-22

Module發展歷程

一開始瀏覽器是不支援模組的概念的,所以就提出了名稱空間的方法,但是這種方式存在著一個很大的問題就是很難保證名字永遠不會重複。另外,現在也很多人使用閉包的方式來實現,但是由於閉包會使得函式中的變數都被儲存在記憶體中,所以使用不當的話就會造成效能問題。

在這之後陸續出現了實現CMD規範的Seajs和AMD規範的RequireJS,如果有興趣的可以去了解一下,但是目前來說它們都已經被deprecate了。

在Node出現之後,它實現了CommonJS的規範,CommonJS的規範採用的是同步的載入方式(也稱動態載入),但是這套規範在瀏覽器中並不能使用

之後就出現了umd規範,其實它就是統一了上述3種規範

在es6出現之後,又衍生出了esmodule規範,在node的新版本中已經開始嘗試去支援使用這套規範了,在Node中就叫mjs

CommonJS簡介

CommonJS背後其實使用的就是閉包的原理來保證封閉作用域與封裝功能,同時它也解決了模組間依賴的問題,在CommonJS規範中規定了一個檔案就是一個模組。

模組常見有兩種形式,第一種就是系統模組,就是Node中提供的如fs,http等,還有一種就是檔案模組,其實也就是我們自己實現的模組

檔案模組的用法上其實就是在我們實現的模組檔案中加入module.exports

// school.js
module.exports = 'test'

// use school.js
let school = require('./school');
console.log(school)
複製程式碼

CommonJS原理

接下來我們就自己來實現一個簡單版本的模組載入來理解CommonJS背後的一些主要原理

首先我們先定義好一個基本的框架和測試程式碼,這裡filename是檔名,而且要注意檔名可能沒有字尾,在Node使用模組時如果是js或者json或者node結尾的檔案可以省略字尾

function req(filename) {
}

let result = req('./school');
console.log(result);
複製程式碼

在呼叫模組時,為了節約效能,node會實現快取的機制,將module.exports後面的結果進行快取,require時如果有直接把快取返回回去,從而避免重複讀取呼叫問題,那麼在不同的路徑下可能存在同樣的檔名,所以我們快取需要根據絕對路徑來儲存,我們可以先定義一個Module的建構函式來建立一個模組的例項,然後它會存放著一個exports屬性用來存模組中要export的資料

function Module(filename) { // 建構函式
    this.filename = filename;
    this.exports = {};
}
Module._cache = {}; //快取
複製程式碼

同時我們也需要定義一個用來存放各種字尾名的陣列,用於查詢處理不同的檔案引入

Module._extentions = ['.js','.json','.node'];
複製程式碼

接下來我們再定義一個函式用來將filename解析為絕對路徑

let path = require('path');
let fs = require('fs');

Module._resolvePathname = function(filename) {
    let p = path.resolve(__dirname, filename);
    if (!path.extname(p)) {
        for(var i = 0;i < Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            try{ // 如果訪問的檔案不存在 就會發生異常
                fs.accessSync(newPath);
                return newPath
            }catch(e){
            }
        }
    }
    return p; //解析出來的就是一個絕對路徑
}
複製程式碼

接著我們需要判斷返回的絕對路徑在快取中是否存在,如果存在就直接返回快取的exports屬性

function req(filename) { // filename是檔名 檔名可能沒有字尾
    // 我們需要弄出一個絕對路徑來,快取是根據絕對路徑來的
    filename = Module._resolvePathname(filename); 
    // 先看這個路徑在快取中有沒有,如果有直接返回
    let cacheModule = Module._cache[filename];
    if (cacheModule) { // 快取裡有 直接把快取中的exports屬性進行返回
        return cacheModule.exports
    }
}
複製程式碼

如果沒有快取的話,我們就需要開始載入模組了,首先先建立一個模組的例項

let module = new Module(filename);
複製程式碼

接著我們需要定義一個load方法來載入這個模組,對於不同的檔案型別,我們需要有不同的處理方法

let vm = require('vm');

Module.wrapper = [
    "(function(exports,require,module,__dirname,__filename){","\n})"
]

Module.wrap = function(script) {
    return Module.wrapper[0] + script + Module.wrapper[1];
}

Module._extentions["js"] = function(module) { // {filename,exports={}}
    let script = fs.readFileSync(module.filename);
    let fnStr = Module.wrap(script);
    vm.runInThisContext(fnStr).call(module.exports, module.exports, req, module);
}

Module._extentions["json"] = function(module) {
    let script = fs.readFileSync(module.filename);
    // 如果是json直接拿到內容  json.parse即可
    module.exports = JSON.parse(script); 
}

Module.prototype.load = function(filename) { //{filename:'c://xxxx',exports:'zfpx'}
    // 模組可能是json 也有可能是js
    let ext = path.extname(filename).slice(1); // .js   .json
    Module._extentions[ext](this);
}
複製程式碼

這裡對於json我們就是簡單的將json檔案的內容解析並返回給module的exports屬性即可,而對於js我們做的其實是將檔案內容拼接到我們的閉包函式字串中然後通過vm.runInThisContext(fnStr).call的方式去呼叫這個閉包執行,我們會將module這個物件傳入函式中,所以當檔案內容執行module.exports之後其實就是將要匯出的資料傳給了module物件的exports屬性。

最後我們將module存入快取中,然後返回module的exports屬性即可,下面就是整個完整的程式碼:

let path = require('path');
let fs = require('fs');
let vm = require('vm');

function Module(filename) { // 建構函式
    this.filename = filename;
    this.exports = {};
    this.loaded = true;
}

Module._extentions = ['.js','.json','.node']; // 如果沒有字尾 希望新增上查詢
Module._cache = {};
Module._resolvePathname = function(filename) {
    let p = path.resolve(__dirname, filename);
    if (!path.extname(p)) {
        for(var i = 0;i < Module._extentions.length;i++){
            let newPath = p + Module._extentions[i];
            try{ // 如果訪問的檔案不存在 就會發生異常
                fs.accessSync(newPath);
                return newPath
            }catch(e){}
        }
    }
    return p; //解析出來的就是一個絕對路徑
}

Module.wrapper = [
    "(function(exports,require,module,__dirname,__filename){","\n})"
]

Module.wrap = function(script) {
    return Module.wrapper[0] + script + Module.wrapper[1];
}
Module._extentions["js"] = function(module) { // {filename,exports={}}
    let script = fs.readFileSync(module.filename);
    let fnStr = Module.wrap(script);
    vm.runInThisContext(fnStr).call(module.exports, module.exports, req, module);
}

Module._extentions["json"] = function(module) {
    let script = fs.readFileSync(module.filename);
    // 如果是json直接拿到內容  json.parse即可
    module.exports = JSON.parse(script); 
}

Module.prototype.load = function(filename) { //{filename:'c://xxxx',exports:'zfpx'}
    // 模組可能是json 也有可能是js
    let ext = path.extname(filename).slice(1); // .js   .json
    Module._extentions[ext](this);
}

function req(filename) { // filename是檔名 檔名可能沒有字尾
    // 我們需要弄出一個絕對路徑來,快取是根據絕對路徑來的
    filename = Module._resolvePathname(filename); 
    // 先看這個路徑在快取中有沒有,如果有直接返回
    let cacheModule = Module._cache[filename];
    if (cacheModule) { // 快取裡有 直接把快取中的exports屬性進行返回
        return cacheModule.exports
    }
    // 沒快取 載入模組
    let module = new Module(filename);  // 建立模組 {filename:'絕對路徑',exports:{}}
    module.load(filename); // 載入這個模組     {filename:'xxx',exports = 'zfpx'}
    Module._cache[filename] = module;
    module.loaded = true; // 表示當前模組是否載入完 
    return module.exports;
}
let result = req('./school');
console.log(result);
複製程式碼

模組的載入策略就是按照以下這張圖的邏輯執行

NODE基礎總結(2) —— Module

對於檔案模組的查詢規則可以參照下面這張圖

NODE基礎總結(2) —— Module
這裡要注意幾個點

  1. 對於第三方模組的引入,不可以加./
  2. CommonJS會有迴圈引用的問題,所以在引入時需要注意
  3. 在使用exports時,從我們前面提到的實現解析中就可以看出exports本質上就是module.exports,所以兩者並無區別,但是需要注意直接使用exports = 這樣的寫法是錯誤的,因為這樣就將exports指向另一個地址,但是module物件裡的exports並不會跟著改變

相關文章