JavaScript模塊化原理淺析

文章首發於:github.com/USTB-musion…javascript

寫在前面

模塊化簡單來講就是是指把一個複雜的系統分解到多個模塊以方便編碼。JS模塊化的大體流程爲:CommonJS(服務端) -> AMD(瀏覽器端)-> UMD(兼容了CommonJS和AMD) -> ES Module(ES6標準)。本文將從它們的用法進行介紹,簡單實現其原理。並簡易實現一個模塊化打包工具webpack。html

本文將從如下幾部分進行總結:前端

  1. CommonJS的用法及原理
  2. AMD的用法及原理
  3. ES2015標準化
  4. 自動化構建
  5. 簡易實現webpack

CommonJS

CommonJS 是一種使用普遍的JavaScript模塊化規範,核心思想是經過require方法來同步地加載依賴的其餘模塊,經過 module.exports 導出須要暴露的接口。java

CommonJS的用法

採用CommonJS來進行導入導出的代碼用法以下:node

// 用require方法導入
const someFun= require('./moduleA');
someFun();

// 用module.exports導出
module.exports = someFunc;
複製代碼

CommonJS原理

a.js:webpack

console.log('aaa');
exports.name = '這是a模塊的內容';
複製代碼

b.js:git

let fs = require('fs');
let path = require('path');
let b = req('./a.js');
// req即便CommonJS中的require方法
function req(mod) {
    let filename = path.join(__dirname, mod);
    let content = fs.readFileSync(filename, 'utf8');
    /**
       * 最後一個參數是函數的內容體,至關於如下函數
       *function fn(exports, module, require, __dirname, __filename) {
       *  module.exports = '這是另一個文件導出的內容'
       *  return module.exports
       *}
    */
    let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + '\n return module.exports;');
    let module = {
        exports: {}
    };

    return fn(module.exports, req, module, __filename, __dirname);
}
複製代碼

AMD

AMD 也是一種 JavaScript 模塊化規範,與 CommonJS 最大的不一樣在於它採用異步的方式去加載依賴的模塊。 AMD 規範主要是爲了解決針對瀏覽器環境的模塊化問題,最具表明性的實現是 requirejs。github

AMD 的優勢:

  • 可在不轉換代碼的狀況下直接在瀏覽器中運行
  • 可加載多個依賴
  • 代碼可運行在瀏覽器環境和 Node.js 環境下

AMD 的缺點:

  • JavaScript 運行環境沒有原生支持 AMD,須要先導入實現了 AMD 的庫後才能正常使用。
// 使用define方法定義一個模塊
define('a', [], function () {
    return 'a';
});
define('b', ['a'], function (a) {
    return a + 'b';
});
// 使用require來導入和使用
require(['b'], function (b) {
    console.log(b);
});
複製代碼

AMD的原理

let factories = {};
/**
 * 實現AMD的define方法
 * @param moduleName 模塊的名字
 * @param dependencies 依賴
 * @param factory 工廠函數
*/
function define(modName, dependencies, factory) {
    factory.dependencies = dependencies;
    factories[modName] = factory;
}
/**
 * 實現AMD的require方法
 * @param mods 引入的模塊
 * @param callback 回調函數
 */
function require(modNames, callback) {
    let loadedModNames = modNames.map(function (modName) {
        let factory = factories[modName];
        let dependencies = factory.dependencies;
        let exports;
        require(dependencies, function (...dependencyMods) {
            exports = factory.apply(null, dependencyMods);
        });
        return exports;
    })
    callback.apply(null, loadedModNames);
}
複製代碼

ES2015模塊化

ES2015 模塊化是ECMA提出的JavaScript模塊化規範,它在語言的層面上實現了模塊化。瀏覽器廠商和Node.js 都宣佈要原生支持該規範。它將逐漸取代CommonJS和AMD規範,成爲瀏覽器和服務器通用的模塊解決方案。 採用 ES2015 模塊化導入及導出時的代碼以下:web

// 使用import導入
import { name } from './person.js';

// 使用export導出
export const name = 'musion';
複製代碼

ES2015模塊雖然是終極模塊化方案,但它的缺點在於目前沒法直接運行在大部分 JavaScript 運行環境下,必須經過構建工具轉換成標準的 ES5 後才能正常運行。編程

自動化構建

自動化構建就是作這件事情,把源代碼轉換成發佈到線上的可執行 JavaScrip、CSS、HTML 代碼,包括以下內容:

  • 代碼轉換:ECMASCRIPT6 編譯成 ECMASCRIPT五、LESS 編譯成 CSS 等。
  • 文件優化:壓縮 JavaScript、CSS、HTML 代碼,壓縮合並圖片等。
  • 代碼分割:提取多個頁面的公共代碼、提取首屏不須要執行部分的代碼讓其異步加載。
  • 模塊合併:在採用模塊化的項目裏會有不少個模塊和文件,須要構建功能把模塊分類合併成一個文件。
  • 自動刷新:監聽本地源代碼的變化,自動從新構建、刷新瀏覽器。
  • 代碼校驗:在代碼被提交到倉庫前須要校驗代碼是否符合規範,以及單元測試是否經過。
  • 自動發佈:更新完代碼後,自動構建出線上發佈代碼並傳輸給發佈系統。

webpack

webpack 是一個打包模塊化 JavaScript 的工具,在 webpack 裏一切文件皆模塊,經過 Loader 轉換文件,經過 Plugin 注入鉤子,最後輸出由多個模塊組合成的文件。Webpack 專一於構建模塊化項目。

一切文件: JavaScript、CSS、SCSS、圖片、模板,在 Webpack 眼中都是一個個模塊,這樣的好處是能清晰的描述出各個模塊之間的依賴關係,以方便 Webpack 對模塊進行組合和打包。 通過 Webpack 的處理,最終會輸出瀏覽器能使用的靜態資源。

webpack簡單揭祕

用webpack來進行打包構建的時候,查看打包📦以後的文件,刪掉多餘的代碼,剩下的核心代碼以下:

(function (modules) {
    function require(moduleId) {
        var module = {
            exports: {}
        };
        // moduleId -> 模塊的名字
        modules[moduleId].call(module.exports, module, module.exports, require);
        return module.exports;

    }
    return require("./index.js");
}) ({
    "./index.js":
    (function (module, exports, require) {
        eval("console.log('hello');\n\n");
    })
});
複製代碼

簡易實現一個webpack

#! /usr/bin/env node
// 這個文件用來描述如何打包
const pathLib = require('path');
const fs = require('fs');
let ejs = require('ejs');
let cwd = process.cwd();
let { entry, output: { filename, path } } = require(pathLib.join(cwd, './webpack.config.js'));
let script = fs.readFileSync(entry, 'utf8');
let bundle = `
(function (modules) {
    function require(moduleId) {
        var module = {
            exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, require);
        return module.exports;

    }
    return require("<%-entry%>");
})
    ({
        "<%-entry%>":
            (function (module, exports, require) {
                eval("<%-script%>");
            })
    });
`
let bundlejs = ejs.render(bundle, {
    entry,
    script
});
try {
    fs.writeFileSync(pathLib.join(path, filename), bundlejs);
} catch (e) {
    console.error('編譯失敗 ', e);
}
console.log('compile sucessfully!');
複製代碼

依賴其它模塊的狀況

#! /usr/bin/env node
// 這個文件用來描述如何打包
const pathLib = require('path');
const fs = require('fs');
let ejs = require('ejs');
let cwd = process.cwd();
let { entry, output: { filename, path } } = require(pathLib.join(cwd, './webpack.config.js'));
let script = fs.readFileSync(entry, 'utf8');
let modules = [];
script.replace(/require\(['"](.+?)['"]\)/g, function () { let name = arguments[1]; let script = fs.readFileSync(name, 'utf8'); modules.push({ name, script }); }); let bundle = ` (function (modules) { function require(moduleId) { var module = { exports: {} }; modules[moduleId].call(module.exports, module, module.exports, require); return module.exports; } return require("<%-entry%>"); }) ({ "<%-entry%>": (function (module, exports, require) { eval(\`<%-script%>\`); }) <%if(modules.length>0){%>,<%}%> <%for(let i=0;i<modules.length;i++){ let module = modules[i];%> "<%-module.name%>": (function (module, exports, require) { eval(\`<%-module.script%>\`); }) <% }%> }); ` let bundlejs = ejs.render(bundle, { entry, script, modules }); try { fs.writeFileSync(pathLib.join(path, filename), bundlejs); } catch (e) { console.error('編譯失敗 ', e); } console.log('compile sucessfully!'); 複製代碼

參考資料

精讀 js 模塊化發展

珠峯前端架構師課程

補充閱讀

JavaScript 模塊化七日談

JavaScript模塊化編程簡史(2009-2016)

相關文章
相關標籤/搜索