javascript模組化簡介

advence-liz發表於2019-04-12

所有原創並不精彩,所有精彩並非原創

歷史

JavaScript 隨著時間的推移所負責的責任越來越重從最開始的新增表單驗證功能之類的指令碼到angular 應用開發框架,隨著js任務越來越重就急需模組化的解決方案。

模組化的基礎條件就是開闢一片獨立的上下文,那些擁有模組化功能的語言或通過物理檔案組織模組,或以抽象的 namespace package 組織模組,而JavaScript 並沒這種能力只能從語法上開闢獨立的上下文,就目前瀏覽器端執行的js來說能開闢獨立上下文的方式只有一種方式 function(閉包)

  • 最開始的刀耕火種用閉包各種全域性變數組織結構
  • AMD UMD commonjs es6
  • 現在webpack 支援 AMD commonjs es6 ,不過webpack更多的只是格式上的支援

對比一下各個模組化方案程式碼寫法

仔細觀察一下下面列舉的幾個例子不難發現根上還是閉包

AMD

define(['requrie','exports','module'],function(require, exports, module) {
        var a = require('a'),
            b = require('b');

        exports.A=a
    })
;
複製程式碼

angular

angular.module('myApp', [])
  .controller('Ctl', ['$scope', '$log', function ($scope, $log) {
  $scope.name = 'leonwgc';
  $log.log('hello,world');
}]);
複製程式碼

webpack

(function(modules) { // webpackBootstrap
    /******/ 	// The module cache
    /******/ 	var installedModules = {};
    /******/
    /******/ 	// The require function
    /******/ 	function __webpack_require__(moduleId) {
    /******/
    /******/ 		// Check if module is in cache
    /******/ 		if(installedModules[moduleId]) {
    /******/ 			return installedModules[moduleId].exports;
    /******/ 		}
    /******/ 		// Create a new module (and put it into the cache)
    /******/ 		var module = installedModules[moduleId] = {
    /******/ 			i: moduleId,
    /******/ 			l: false,
    /******/ 			exports: {}
    /******/ 		};
    /******/
    /******/ 		// Execute the module function
    /******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    /******/
    /******/ 		// Flag the module as loaded
    /******/ 		module.l = true;
    /******/
    /******/ 		// Return the exports of the module
    /******/ 		return module.exports;
    /******/ 	}
    /******/
    /******/ 	// Load entry module and return exports
    /******/ 	return __webpack_require__(__webpack_require__.s = 81);
    /******/ })
    /************************************************************************/
    /******/ ([
    /* 0 */
/***/ (function(module, exports, __webpack_require__) {

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
;
exports.cla = cla;

/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {

"use strict";
var e = 2.71828182846;
function math(x) {
  return Math.exp(x);
}

module.exports = math;

/***/ })
        
    ]);
複製程式碼

對比完程式碼接下來簡要介紹一下AMD,Commonjs ,ES6模組化的語法於定義

AMD

define(id?, dependencies?, factory);

id

第一個引數,id,是個字串。它指的是定義中模組的名字,這個引數是可選的。如果沒有提供該引數,模組的名字應該預設為模組載入器請求的指定指令碼的名字。如果提供了該引數,模組名必須是“頂級”的和絕對的(不允許相對名字)。

模組格式

模組名用來唯一標識定義中模組,它們同樣在依賴陣列中使用。AMD的模組名規範是CommonJS模組名規範的超集。引用如下:

  • 模組名是由一個或多個單詞以正斜槓為分隔符拼接成的字串
  • 單詞須為駝峰形式,或者".",".."
  • 模組名不允許副檔名的形式,如".js"
  • 模組名可以為 "相對的" 或 "頂級的"。如果首字元為"."或".."則為"相對的"模組名
  • 頂級的模組名從根名稱空間的概念模組解析
  • 相對的模組名從 "require" 書寫和呼叫的模組解析 上文引用的CommonJS模組id屬性常被用於JavaScript模組。

相對模組名解析示例:

  • 如果模組 "a/b/c" 請求 "../d", 則解析為"a/d"
  • 如果模組 "a/b/c" 請求 "./e", 則解析為"a/b/e"

依賴

第二個引數,dependencies,是個定義中模組所依賴模組的陣列。依賴模組必須根據模組的工廠方法優先順序執行,並且執行的結果應該按照依賴陣列中的位置順序以引數的形式傳入(定義中模組的)工廠方法中。

依賴的模組名如果是相對的,應該解析為相對定義中的模組。換句話來說,相對名解析為相對於模組的名字,並非相對於尋找該模組的名字的路徑。

本規範定義了三種特殊的依賴關鍵字。如果"require","exports", 或 "module"出現在依賴列表中,引數應該按照CommonJS模組規範自由變數去解析。

依賴引數是可選的,如果忽略此引數,它應該預設為["require", "exports", "module"]。然而,如果工廠方法的形參個數小於3,載入器會選擇以函式指定的引數個數呼叫工廠方法。

工廠方法

第三個引數,factory,為模組初始化要執行的函式或物件。如果為函式,它應該只被執行一次。如果是物件,此物件應該為模組的輸出值。

如果工廠方法返回一個值(物件,函式,或任意強制型別轉換為true的值),應該為設定為模組的輸出值。

簡單的 CommonJS 轉換

如果依賴性引數被忽略,模組載入器可以選擇掃描工廠方法中的require語句以獲得依賴性(字面量形為require("module-id"))。第一個引數必須字面量為require從而使此機制正常工作。

在某些情況下,因為指令碼大小的限制或函式不支援toString方法(Opera Mobile是已知的不支援函式的toString方法),模組載入器可以選擇掃描不掃描依賴性。

如果有依賴引數,模組載入器不應該在工廠方法中掃描依賴性。

Simple Name/Value Pairs

If the module does not have any dependencies, and it is just a collection of name/value pairs, then just pass an object literal to define():

//Inside file my/shirt.js:
define({
    color: "black",
    size: "unisize"
});
複製程式碼

Definition Functions

If the module does not have dependencies, but needs to use a function to do some setup work, then define itself, pass a function to define():

//my/shirt.js now does setup work
//before returning its module definition.
define(function () {
    //Do setup work here

    return {
        color: "black",
        size: "unisize"
    }
});
複製程式碼

Definition Functions with Dependencies

If the module has dependencies, the first argument should be an array of dependency names, and the second argument should be a definition function. The function will be called to define the module once all dependencies have loaded. The function should return an object that defines the module. The dependencies will be passed to the definition function as function arguments, listed in the same order as the order in the dependency array:

//my/shirt.js now has some dependencies, a cart and inventory
//module in the same directory as shirt.js
define(["./cart", "./inventory"], function(cart, inventory) {
        //return an object to define the "my/shirt" module.
        return {
            color: "blue",
            size: "large",
            addToCart: function() {
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
);
複製程式碼

Define a Module with Simplified CommonJS Wrapper

If you wish to reuse some code that was written in the traditional CommonJS module format it may be difficult to re-work to the array of dependencies used above, and you may prefer to have direct alignment of dependency name to the local variable used for that dependency. You can use the simplified CommonJS wrapper for those cases:

define(function(require, exports, module) {
        var a = require('a'),
            b = require('b');

        exports.A=a
    }
);

define(['requrie','exports','module'],function(require, exports, module) {
        var a = require('a'),
            b = require('b');

        exports.A=a
    }
);
複製程式碼

r.js

babel

commonjs js 規範是 AMD 的子集 看一下demo

  • babel-plugin-transform-es2015-modules-amd
  • babel-plugin-transform-es2015-modules-commonjs
  • babel-plugin-transform-es2015-modules-systemjs
  • babel-plugin-transform-es2015-modules-umd
$ babel ES6 --out-dir AMD --plugins=transform-es2015-modules-amd
$ babel ES6 --out-dir UMD --plugins=transform-es2015-modules-umd
$ babel ES6 --out-dir common --plugins=transform-es2015-modules-commonjs

複製程式碼

Commonjs

CommonJS API定義很多普通應用程式(主要指非瀏覽器的應用)使用的API,從而填補了這個空白。它的終極目標是提供一個類似Python,Ruby和Java標 準庫。這樣的話,開發者可以使用CommonJS API編寫應用程式,然後這些應用可以執行在不同的JavaScript直譯器和不同的主機環境中。在相容CommonJS的系統中,你可以使用 JavaScript程式開發:

  • 伺服器端JavaScript應用程式
  • 命令列工具
  • 圖形介面應用程式
  • 混合應用程式(如,Titanium或Adobe AIR...)

javascript模組化簡介

基本語法

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
// 上面程式碼通過module.exports輸出變數x和函式addX。

// require方法用於載入模組。
var example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6
複製程式碼

module物件

  • module.id 模組的識別符,通常是帶有絕對路徑的模組檔名。
  • module.filename 模組的檔名,帶有絕對路徑。
  • module.loaded 返回一個布林值,表示模組是否已經完成載入。
  • module.parent 返回一個物件,表示呼叫該模組的模組。
  • module.children 返回一個陣列,表示該模組要用到的其他模組。
  • module.exports 表示模組對外輸出的值。
{ id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/path/to/example.js',
  loaded: false,
  children:
   [ { id: '/path/to/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/path/to/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Object] } ],
  paths:
   [ '/home/user/deleted/node_modules',
     '/home/user/node_modules',
     '/home/node_modules',
     '/node_modules' ]
}
複製程式碼

exports

exports 要注意的問題

exports = function(x) {console.log(x)};

複製程式碼

函式傳參傳入引用的引用

函式傳參基本是兩種型別 值型別和引用型別 最早接觸這個問題是在湯姆大叔的部落格中

var liz={age:18}

function fun(liz){
 liz={age:19}
}

複製程式碼
(function (exports, require, module, __filename, __dirname) {
  // exports = module.exports
});
複製程式碼

ES6

在 ES6 之前,社群制定了一些模組載入方案,最主要的有 CommonJS 和 AMD 兩種。前者用於伺服器,後者用於瀏覽器。ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模組解決方案。

ES6 模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。

// CommonJS模組
let { stat, exists, readFile } = require('fs');

// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
複製程式碼

上面程式碼的實質是整體載入fs模組(即載入fs的所有方法),生成一個物件(_fs),然後再從這個物件上面讀取 3 個方法。這種載入稱為“執行時載入”,因為只有執行時才能得到這個物件,導致完全沒辦法在編譯時做“靜態優化”。

ES6 模組不是物件,而是通過export命令顯式指定輸出的程式碼,再通過import命令輸入。

// ES6模組
import { stat, exists, readFile } from 'fs';
複製程式碼

上面程式碼的實質是從fs模組載入 3 個方法,其他方法不載入。這種載入稱為“編譯時載入”或者靜態載入,即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件。

由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態載入帶來的各種好處,ES6 模組還有以下好處。

  • 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
  • 將來瀏覽器的新 API 就能用模組格式提供,不再必須做成全域性變數或者navigator物件的屬性。
  • 不再需要物件作為名稱空間(比如Math物件),未來這些功能可以通過模組提供。

module 語法

推薦

工作上的體會這種模組載入的語法,就用最簡單常見的方式就好,千萬不要過多操作

import _default from "xxx"
import _default,{a,b,c} from "xxx"

export default class xxx{}
export class xxx{}
export {xx,xxx,xxxx}
複製程式碼

export 與 import 的複合寫法

如果在一個模組之中,先輸入後輸出同一個模組,import語句可以與export語句寫在一起。

export { foo, bar } from 'my_module';

// 可以簡單理解為
import { foo, bar } from 'my_module';
export { foo, bar };
上面程式碼中,exportimport語句可以結合在一起,寫成一行。但需要注意的是,寫成一行以後,foo和bar實際上並沒有被匯入當前模組,只是相當於對外轉發了這兩個介面,導致當前模組不能直接使用foo和bar。

模組的介面改名和整體輸出,也可以採用這種寫法。

// 介面改名
export { foo as myFoo } from 'my_module';

// 整體輸出
export * from 'my_module';
預設介面的寫法如下。

export { default } from 'foo';
具名介面改為預設介面的寫法如下。

export { es6 as default } from './someModule';

// 等同於
import { es6 } from './someModule';
export default es6;
同樣地,預設介面也可以改名為具名介面。

export { default as es6 } from './someModule';
下面三種import語句,沒有對應的複合寫法。

import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";
為了做到形式的對稱,現在有提案,提出補上這三種複合寫法。

export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";
複製程式碼

模組的繼承

模組之間也可以繼承。

假設有一個circleplus模組,繼承了circle模組。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}
上面程式碼中的export *,表示再輸出circle模組的所有屬性和方法。注意,export *命令會忽略circle模組的default方法。然後,上面程式碼又輸出了自定義的e變數和預設方法。

這時,也可以將circle的屬性或方法,改名後再輸出。

// circleplus.js

export { area as circleArea } from 'circle';
上面程式碼表示,只輸出circle模組的area方法,且將其改名為circleArea。

載入上面模組的寫法如下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
上面程式碼中的import exp表示,將circleplus模組的預設方法載入為exp方法。
複製程式碼

本文最後推一手我寫的自動生成模組索引的工具index-creater

相關文章