淺談Node中module的實現原理

曾幾什麼時候,Javascript還沒那麼牛逼,幾乎全部人都以爲它是用來作網頁特效的腳本而已。彼時倉促建立出來的javascript的自身缺點被各類吐槽。隨着web的發展,Javascript現在是媳婦熬成婆,應用愈來愈普遍。javascript

雖然Javascript自身很努力,可是仍是缺少一項重要功能,那就是模塊。畢竟Python有require,PHP有include和require。js經過<script>標籤引入的方式雖然說也沒問題,可是缺少組織和約束,也很難達到安全和易用。因此CommonJS規範的提出簡直就是革命性的。java

如今咱們就來講說在Node中的CommonJS模塊的規範和實現。node

CommonJS的模塊規範

CommonJS對模塊定義分爲三部分:模塊定義、模塊引用和模塊標識。web

  1. 模塊定義:

首先建立一個a.js的文件,在裏面寫上:json

module.exports = 'hello world';
複製代碼

在Node中,一個文件就是一個模塊,module.exports對象能夠導出當前模塊的方法或者變量。以上的代碼就是將字符串hello world導出,上下文就提供了require()方法來引入外部模塊。數組

  1. 模塊引用:

建立一個b.js的文件,在裏面寫上:瀏覽器

let b = require('./b.js'); //.js能夠不寫,這咱們後面會講到
console.log(b); // hello world
複製代碼

3)模塊標識: 模塊標識其實就是require()方法裏的參數,它能夠以 . 和 .. 開頭的相對路徑,也能夠是絕對路徑。能夠沒有文件名後綴.js,後面會講到爲什麼能夠沒有後綴。緩存


Node的模塊實現

上面的代碼就是最簡單的模塊的使用。那麼在咱們這幾行簡單的代碼背後,在實現過程當中到底是什麼樣的過程呢?咱們一點點來分析。安全

Node在引入模塊經歷3個步驟:路徑分析、文件定位和編譯執行bash

在Node中模塊有兩類:一類是Node提供的核心模塊,另外一類是用戶本身編寫的文件模塊。

部分的核心模塊直接加載在內存中,因此引入這部分模塊時,文件定義和編譯執行均可以省略,在路徑分析中優先判斷,加載速度也是最快的。

另外須要知曉的是,Node引入的模塊都會進行緩存,以減小二次引用時的開銷。它緩存的是編譯和執行後的對象,而不是和瀏覽器同樣,緩存的是文件。


  1. 路徑分析 上面已經說了,require()方法裏參數叫模塊標識,路徑分析其實就是基於標識符來查找的。模塊標識符在Node中分爲如下幾類:

    • 核心模塊,好比http、fs、path等。
    • . 或 .. 開始的相對路徑文件模塊。
    • 以/開始的絕對路徑文件模塊。
    • 非路徑形式的文件模塊,如自定義的connect模塊。
  • 核心模塊的優先級僅次於緩存加載,因爲在Node源碼編譯中已經被編譯爲二進制代碼,因此加載過程是最快的。

  • 相對路徑文件模塊在分析路徑時,require()方法會將路徑轉爲真實路徑,並以絕對路徑最爲索引,將編譯執行後的結果放入緩存,以使二次引用時加載更快。

  • 自定義模塊是特殊的文件模塊,多是文件或者包的形式,也是查找最慢的一種模塊。

Node在定位文件模塊的具體文件時制定的查找策略能夠表現爲一個路徑數組。

在js文件中console.log(module.paths);
放到任意目錄中執行;
就會獲得相似如下的數組:

[ '/Users/lq/Desktop/node_modules', //當前文件目錄下的node_modules目錄
  '/Users/lq/node_modules',//父目錄下的node_modules目錄
  '/Users/node_modules',//父目錄的父目錄下的node_modules目錄
  '/node_modules' ]//沿路徑向上逐級遞歸,直到找到根目錄下的node_modules目錄
複製代碼

當前文件的路徑越深,模塊查找就越耗時,這是自定義模塊加載慢的緣由。

  1. 文件定位

require()在分析標識符的時候,可能會出現沒有傳遞文件拓展名的狀況。CommonJS模塊規範是容許這種狀況出現的,不過Node會按照.js、.json、.node的順序補足拓展名。依次調用fs模塊同步阻塞式地判斷文件是否存在。

在這個過程當中,Node對CommonJS模塊規範進行了必定的支持。首先,Node會在當前目錄下查找包描述文件package.json,經過JSON.parse()解析出包描述對象,取出main屬性指定的文件名進行定位,若是缺乏拓展名,就進入上面說的拓展名分析步驟,按順序補足拓展名再查找。若是main屬性指定的文件名錯誤或者沒有包描述文件package.json,Node會將index看成默認文件名,依次查找index.js、index.json、index.node。若是全部的路徑數組都遍歷了,仍是沒有找到,就會拋出錯誤。

  1. 模塊編譯 在定位到具體的文件後,Node會新建一個模塊對象,而後根據路徑進行編譯。根據不一樣的拓展名操做不一樣的方法。
  • 若是是.js文件,經過fs模塊同步讀取文件後編譯執行
  • 若是是.json文件,經過fs模塊同步讀取文件後,用JSON.parse()解析返回結果
  • 若是是.node文件,經過dlopen()方法加載最後編譯生成的文件。這是C/C++編寫的拓展文件,本人在後面實現原理的過程當中予以忽略。

說了這麼多,下面直接進入實現環節

先建立一個a.js文件,寫入:

module.exports = 'hello world';
複製代碼

再建立一個b.js,寫入:

let b = require('./a.js');
console.log(b); //hello world
複製代碼

打印的結果是hello wrold。這是Node自帶的require方法。如今咱們來實現下咱們本身的require方法。

咱們直接在b.js裏修改下:

//引入Node的核心模塊
let fs = require('fs');
let path = require('path');
let vm = require('vm');

function Module(p) {
    this.id = p; //當前模塊的標識,也就是絕對路徑
    this.exports = {}; //每一個模塊都有exports屬性,添加一個
    this.loaded = false; //是否已經加載完
}
//對文件內容進行頭尾包裝
Module.wrapper = ['(function(exports,require,module){', '})']

//全部的加載策略
Module._extensions = {
    '.js': function (module) { //讀取js文件,增長一個閉包
        let script = fs.readFileSync(module.id, 'utf8');
        let fn = Module.wrapper[0] + script + Module.wrapper[1];//包裝在一個閉包裏
        vm.runInThisContext(fn).call(module.exports, module.exports, myRequire, module);//經過runInThisContext()方法執行不污染全局
        return module.exports;

    },
    '.json': function (module) {
        return JSON.parse(fs.readFileSync(module.id, 'utf8')); //讀取文件
    }
}

Module._cacheModule = {} //存放緩存

Module._resolveFileName = function (moduleId) { //根據傳入的路徑參數返回一個絕對路徑的方法
    let p = path.resolve(moduleId);
    if (!path.extname(moduleId)) { //若是沒有傳文件後綴
        let arr = Object.keys(Module._extensions); //將對象的key轉成數組
        for (let i = 0; i < arr.length; i++) { //循壞數組添加後綴
            let file = p + arr[i];
            try {
                fs.accessSync(file); //查看文件是否存在,存在的就返回
                return file;
            } catch (e) {
                console.log(e); //不存在報錯
            }
        }
    } else {
        return p; //若是已經傳遞了文件後綴,直接返回絕對路徑
    }
}

Module.prototype.load = function (filepath) { //模塊加載的方法
    let ext = path.extname(filepath);
    let content = Module._extensions[ext](this);
    return content;
}

function myRequire(moduleId) { //自定義的myRequire方法
    let p = Module._resolveFileName(moduleId); //將傳遞進來的模塊標示轉成絕對路徑
    if (Module._cacheModule[p]) { //若是模塊已經存在
        return Module._cacheModule[p].exports; //直接返回編譯和執行以後的對象
    }
    let module = new Module(p); //模塊不存在,先建立一個新的模塊對象
    let content = module.load(p); //模塊加載後的內容
    Module._cacheModule[p] = module;
    module.exports = content;
    return module.exports;
}

let b = myRequire('./a.js');
console.log(b);
複製代碼

這樣就能夠經過本身的myRequire()方法拿到a.js裏的字符串hello world了。固然,module的源碼不止這麼多,有興趣的能夠本身查看。本文只是說明下module加載的原理。有寫的不夠嚴謹的地方,望諒解。若有錯漏,可指出,定及時修改。

參考

部份內容根據《深刻淺出Node.js》一書整理

相關文章
相關標籤/搜索