淺析node怎樣連結多個JS模組

2023-02-07 18:00:08

有時候是不是會有這樣的疑問:紛繁的功能檔案,到最後是怎麼組合成起來並且在瀏覽器中展示的?為什麼需要 node 環境?下面本篇文章給大家介紹一下node是怎樣把多個JS模組連結在一起的?希望對大家有所幫助!

一、個人理解

瀏覽器本身只能做一些展示及使用者互動的功能,對於系統操作的能力很有限,那麼,瀏覽器內建的執行環境顯然不滿足一些更為人性化的開發模式,比如:更好的區分功能模組、實現檔案的操作。那麼,帶來的缺陷就很明顯,比如:各個 JS 檔案比較分散,需要在 html 頁面裡面單獨引入,如果某個 JS 檔案需要其他的 JS 庫,那麼很可能會因為 html 頁面未引入而報錯,在功能龐大的專案裡,手動的管理這些功能檔案確實讓人有點捉襟見肘。

那麼, node 到底是怎麼更友好的提供開發的呢?其實,上面也說了,人為的管理檔案依賴不但會消耗大量的精力,還會存在疏漏,那麼,是不是以用自動化的方式進行管理就會好很多?是的,在 node 的執行環境裡拓寬了對系統的操作能力,也就是說,或許以前開發者也想通過一些程式碼來完成那些機械瑣碎的工作,但是,只有想法沒有操作許可權,最後只能望洋興嘆。現在,可以以 node 的一些擴充套件功能對檔案進行先前加工與整理,再新增一些自動化的程式碼,最後轉換為一個瀏覽器可識別的、完整的 JS 檔案,這樣一來,多個檔案的內容,便可以彙集到一個檔案。【相關教學推薦:、】

二、建立檔案

先建立一些 JS 檔案,如下圖所示:

image.png

這些檔案都是手動建立,babel-core 這個檔案是從全域性的 node_modules 裡面複製出來的,如下圖所示:

image.png

為什麼要複製出來呢?這是因為,任何腳手架乾的事其實都是為了快速搭建,但是,怎麼能理解它乾的什麼事呢?那乾脆就直接複製吧,本身,node 除了一些內建的模組,其他的都需要通過指明 require 路徑的方式來找到相關模組,如下圖所示:

image.png

通過 require('./babel-core') 方法,解析一個功能模組下的方法。

1、編寫入口檔案,轉換ES6程式碼

entrance.js 作為入口檔案,作用就是設定工作從哪開始?怎麼開始?那麼,這裡的工作指的就是轉換ES6程式碼,以提供瀏覽器使用。

//檔案管理模組
const fs = require('fs');
//解析檔案為AST模組
const babylon = require('babylon');
//AST轉換模組
const { transformFromAst } = require('./babel-core');
//獲取JS檔案內容
let content = fs.readFileSync('./person.js','utf-8')
//轉換為AST結構,設定解析的檔案為 module 型別
let ast = babylon.parse(content,{
    sourceType:'module'
})
//將ES6轉換為ES5瀏覽器可識別程式碼
le t { code } = transformFromAst(ast, null, {
    presets: ['es2015']
});
//輸出內容
console.log('code:\n' + `${code}`)
登入後複製

上面的程式碼很簡單,最終的目的就是將 module 型別的 person.js 檔案轉換為 ES5

let person = {name:'wsl'}
export default person
登入後複製

終端執行入口檔案,如下所示:

node entrance.js
登入後複製

列印一下程式碼,如下圖所示:

"use strict";
//宣告了一個 __esModule 為 true 的屬性
Object.defineProperty(exports, "__esModule", {
  value: true
});
var person = { name: 'wsl' };
exports.default = person;
登入後複製

可以,看到列印的程式碼,裡面都是瀏覽器能識別的程式碼,按照常理,看看能不能直接執行一下?

下面將這段程式碼通過 fs 功能寫入一個 js 檔案並讓一個頁面參照,來看看效果:

fs.mkdir('cache',(err)=>{
    if(!err){
        fs.writeFile('cache/main.js',code,(err)=>{
            if(!err){
                console.log('檔案建立完成')
            }
        })
    }
})
登入後複製

再次執行命令,如圖所示:

image.png

瀏覽器執行結構,如圖所示:

image.png

其實程式碼生成完就有很明顯的錯誤,未宣告變數,怎麼會不報錯呢?這時候,在入口檔案輸入之前就需要新增一些自定義輔助程式碼,來解決一下這個報錯。

解決的方式也很簡單,將原 code 的未宣告的 exports 變數通過自執行函數的方式包裹一下,再返回給指定物件。

//完善不嚴謹的code程式碼
function perfectCode(code){
    let exportsCode = `
    var exports = (function(exports){
    ${code}
    return exports
    })({})
    console.log(exports.default)`
    return exportsCode
}
//重新定義code
code = perfectCode(code)
登入後複製

看一下輸出完善後的 main.js 檔案

var exports = (function(exports){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
    value: true
    });
    var person = { name: 'wsl' };
    exports.default = person;
    return exports
})({})
console.log(exports.default)
登入後複製

瀏覽器執行,如圖所示:

image.png

現在瀏覽器執行正常了。

2、處理 import 邏輯

既然是模組,肯定會存在一個模組依賴另一個或其他很多個模組的情況。這裡先不著急,先看看person 模組引入單一 animal 模組後的程式碼是怎樣的?

animal 模組很簡單,僅僅是一個物件匯出

let animal = {name:'dog'}
export default animal
登入後複製

person 模組引入

import animal from './animal'
let person = {name:'wsl',pet:animal}
export default person
登入後複製

看下轉換後的程式碼

"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
var _animal = require("./animal");
var _animal2 = _interopRequireDefault(_animal);
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj };
}
var person = { name: 'wsl', pet: _animal2.default };
exports.default = person;
登入後複製

可以看到,轉換後會多一個未宣告的 require 方法,內部宣告的 _interopRequireDefault 方法已宣告,是對 animal 匯出部分進行了一個包裹,讓其後續的程式碼取值 default 的時候保證其屬性存在!

下面就需要對 require 方法進行相關的處理,讓其轉為返回一個可識別、可解析、完整的物件。

是不是可以將之前的邏輯對 animal 模組重新執行一遍獲取到 animal 的程式碼轉換後的物件就行了?

但是,這裡先要解決一個問題,就是對於 animal 模組的路徑需要提前獲取並進行程式碼轉換,這時候給予可以利用 babel-traverse 工具對 AST 進行處理。

說到這裡,先看一下 JS 轉換為 AST 是什麼內容?

這裡簡單放一張截圖,其實是一個 JSON 物件,儲存著相關的程式碼資訊,有程式碼位置的、指令內容的、變數的等等。

image.png

拿到它的目的其實就是找到import 對應節點下的引入其他模組的路徑

image.png

通過 babel-traverse 找到 AST 裡面 import 對應的資訊

const traverse = require('babel-traverse').default;
//遍歷找到 import 節點
traverse(ast,{
    ImportDeclaration:({ node })=>{
        console.log(node)
    }
})
登入後複製

輸出看下節點列印的結構

Node {
  type: 'ImportDeclaration',
  start: 0,
  end: 29,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 29 }
  },
  specifiers: [
    Node {
      type: 'ImportDefaultSpecifier',
      start: 7,
      end: 13,
      loc: [SourceLocation],
      local: [Node]
    }
  ],
  source: Node {
    type: 'StringLiteral',
    start: 19,
    end: 29,
    loc: SourceLocation { start: [Position], end: [Position] },
    extra: { rawValue: './animal', raw: "'./animal'" },
    value: './animal'
  }
}
登入後複製

可以看到 node.source.value 就是 animal 模組的路徑,需要的就是它。

擴充套件入口檔案功能,解析 import 下的 JS 模組,

新增 require 方法

//完善程式碼
function perfectCode(code){
    let exportsCode = `
        //新增require方法
        let require = function(path){
            return {}
        }

        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
    `
    return exportsCode
}
登入後複製

這樣轉換完的 main.js 給不會報錯了,但是,這裡需要解決怎麼讓 require 方法返回 animal 物件

let require = function(path){
    return {}
}

let exports = (function(exports,require){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
        value: true
    });

    var _animal = require("./animal");
    var _animal2 = _interopRequireDefault(_animal);
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    var person = { name: 'wsl', pet: _animal2.default };
    exports.default = person;
    return exports
})({},require)
登入後複製

下面就需要新增 require 方法進行 animal 物件的返回邏輯

//引入模組路徑
let importFilesPaths = []
//引入路徑下的模組程式碼
let importFilesCodes = {}

//獲取import節點,儲存模組路徑
traverse(ast,{
    ImportDeclaration:({ node })=>{
        importFilesPaths.push(node.source.value)
    }
})

//解析import邏輯
function perfectImport(){
    //遍歷解析import裡面對應路徑下的模組程式碼
    importFilesPaths.forEach((path)=>{
        let content = fs.readFileSync(path + '.js','utf-8')
        let ast = babylon.parse(content,{
            sourceType:'module'
        })
        let { code } = transformFromAst(ast, null, {
            presets: ['es2015']
        });
        //轉換code
        code = perfectImportCode(code)
        importFilesCodes[path] = code
    })
}

//完善import程式碼
function perfectImportCode(code){
    let exportsCode = `(
        function(){
                let require = function(path){
                    let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
                    return exports
                }
                return (function(exports,require){${code}
                    return exports
                })({},require)
            }
        )()
    `
    return exportsCode
}

//完善最終輸出程式碼
function perfectCode(code){
    let exportsCode = `
        let require = function(path){
            let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
        console.log(exports.default)
    `
    return exportsCode
}
登入後複製

上面的程式碼其實沒有什麼特別難理解的部分,裡面的自執行閉包看著亂,最終的目的也很清晰,就是找到對應模組下的檔案 code 程式碼進行自執行返回一個對應的模組物件即可。

看下轉換後的 main.js 程式碼

let require = function(path){
    let exports = (function(){ return eval({"./animal":"(\n function(){\n let require = function(path){\n let exports = (function(){ return eval({}[path])})()\n return exports\n }\n return (function(exports,require){\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar animal = { name: 'dog' };\n\nexports.default = animal; \n return exports\n })({},require)\n }\n )()\n "}[path])})()
    return exports
}

let exports = (function(exports,require){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
    value: true
    });

    var _animal = require("./animal");
    var _animal2 = _interopRequireDefault(_animal);

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }


    var person = { name: 'wsl', pet: _animal2.default };
    exports.default = person;
    return exports
})({},require)
console.log(exports.default)
登入後複製

重新整理瀏覽器,列印結果如下:

image.png

可以看到,pet 屬性被賦予了新值。

三、完整的入口檔案程式碼

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('./babel-core');
//解析person檔案
let content = fs.readFileSync('./person.js','utf-8')
let ast = babylon.parse(content,{
    sourceType:'module'
})

//引入模組路徑
let importFilesPaths = []
//引入路徑下的模組程式碼
let importFilesCodes = {}

//儲存import引入節點
traverse(ast,{
    ImportDeclaration:({ node })=>{
        importFilesPaths.push(node.source.value)
    }
})

//person.js 對應的code
let { code } = transformFromAst(ast, null, {
    presets: ['es2015']
});

//解析import邏輯
function perfectImport(){
    importFilesPaths.forEach((path)=>{
        let content = fs.readFileSync(path + '.js','utf-8')
        let ast = babylon.parse(content,{
            sourceType:'module'
        })
        let { code } = transformFromAst(ast, null, {
            presets: ['es2015']
        });
        code = perfectImportCode(code)
        importFilesCodes[path] = code
    })
}

//完善import程式碼
function perfectImportCode(code){
let exportsCode = `
    (
    function(){
        let require = function(path){
        let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        return (function(exports,require){${code}
            return exports
        })({},require)
        }
    )()
    `
    return exportsCode
}

//開始解析import邏輯
perfectImport()

//完善最終程式碼
function perfectCode(code){
    let exportsCode = `
        let require = function(path){
            let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
        console.log(exports.default)
    `
    return exportsCode
}

//最後的程式碼
code = perfectCode(code)

//刪除檔案操作
const deleteFile = (path)=>{
    if(fs.existsSync(path)){
        let files = []
        files = fs.readdirSync(path)
        files.forEach((filePath)=>{
            let currentPath = path + '/' + filePath
            if(fs.statSync(currentPath).isDirectory()){
                deleteFile(currentPath)
            } else {
                fs.unlinkSync(currentPath)
            }
        })
        fs.rmdirSync(path)
    }
}

deleteFile('cache')

//寫入檔案操作
fs.mkdir('cache',(err)=>{
        if(!err){
            fs.writeFile('cache/main.js',code,(err)=>{
            if(!err){
                console.log('檔案建立完成')
            }
        })
    }
})
登入後複製

四、總結與思考

古代鑽木取火遠比現代打火機烤麵包的意義深遠的多。這個世界做過的事情沒有對或錯之分,但有做與不做之別。程式碼拙劣,大神勿笑[抱拳][抱拳][抱拳]

更多node相關知識,請存取:!

以上就是淺析node怎樣連結多個JS模組的詳細內容,更多請關注TW511.COM其它相關文章!