前端模塊化一——規範詳述

前言

模塊化——這個東西能夠說在研發圈子裏,耳熟能詳,家喻戶曉。
都快說爛了,今天爲何又拿出來說?
做爲一個開發經驗剛滿一年的前端,
接觸過es6 的import,nodejs 的require,webpack下的import或者require。那麼他們之間到底有什麼區別?
CommonJS 據說過,nodejs的模塊化實現?AMD據說過,也據說過CMD,還有個ES6 模塊化、webpack 模塊化。
當項目裏使用的時候,咱們是否真正知道,你用的到底基於哪一個規範的模塊化?
最重要的是,當咱們require( )或者import的時候,咱們要知道底發生了什麼?

文章很長,須要一點耐心,可是關於前端的模塊化的東西從萌芽時代到如今的ES6 、webpack實戰中全部模塊化的東西都有詳盡的講解。

1 爲什麼前端須要模塊化?

ps:基礎比較好的同窗能夠跳過第一章節
爲了完全弄清楚模塊化這個東西,咱們要從最開始模塊化的起源提及。

1.1 無模塊化的原始時代

最開始js只是做爲一個腳本語言來使用,作一些簡單的表單校驗,動畫實現等等。
代碼都是這樣的,直接把代碼寫進<script>標籤裏,代碼量很是少。
<script>
 if(true) {
   ...
 } else {
   ...
 }
 for(var i=0; i< 100; i++){
   ...
 }
 document.getElementById('button').onClick = function () {
   ...
 }
</script>複製代碼

1.2 代碼量劇增帶來的災難性問題

後來隨着ajax異步請求的出現,前端能作的事情愈來愈多,代碼量飛速增加。
也暴露出了一些問題。

全局變量的災難
這個很是好理解,就是你們的代碼都在一個做用域,不一樣的人定義的變量可能會重複從而產生覆蓋。
//試想彭彭定義了一個變量 
name = 'pengpeng';

//後來,丁滿後面又定義了
name =  'dingman';

//再後來, 彭彭開始使用他定義的變量

if(name === 'pengpeng'){
  ...
}複製代碼
這就杯具了。
依賴關係管理的災難
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>複製代碼
若是c依賴了b,b依賴了c,則script引入的順序必須被依賴的放在前面,試想要是有幾十個文件,咱們都要弄清楚文件依賴關係而後手動,按順序引入,無疑這是很是痛苦的事情。

1.3 早期的解決方式

(1)閉包
moduleA = function() {
   var a,b;
   return {
      add: function (c){
         return a + b + c;
      };
   }
}()複製代碼
這樣function內部的變量就對全局隱藏了,達到了封裝的目的,可是最外層模塊名仍是暴露在全局,要是模快愈來愈多,依然會存在模塊名衝突的問題。
(2)命名空間
Yahoo的YUI早起的作法
app.tools.moduleA.add = function(c){
   return app.tools.moduleA.a + c;
}複製代碼
毫無疑問以上兩種方法都不夠優雅。

那麼,模塊化到底須要解決什麼問題提呢?咱們先設想一下可能有如下幾點
  • 安全的包裝一個模塊的代碼,避免全局污染
  • 惟一標識一個模塊
  • 優雅的將模塊api暴露出去
  • 方便的使用模塊

2 服務端模塊化

Nodejs出現開創了一個新的紀元,使得咱們可使用javascript寫服務器代碼,對於服務端而言必然是須要模塊化的。

2.1 Nodejs和CommonJS的關係

這裏要說一下Nodejs和CommonJS的關係。
  • Nodejs的模塊化能一種成熟的姿態出現離不開CommonJS的規範的影響
  • 在服務器端CommonJS能以一種尋常的姿態寫進各個公司的項目代碼中,離不開Node的優異表現
  • Node並不是徹底按照規範實現,針對模塊規範進行了必定的取捨,同時也增長了少量自身特性

以上三點是摘自樸靈的《深刻淺出Nodejs》

2.2 CommonJS規範簡介

CommonJS對模塊的定義很是簡單,主要分爲模塊引用,模塊定義和模塊標識3部分
(1)模塊引用
var add = require('./add.js');
var config = require('config.js');
var http = require('http');複製代碼
(2)模塊定義
module.exports.add = function () {
  ...
}
module.exports = function () {
  return ...
}複製代碼
能夠在一個文件中引入模塊並導出另外一個模塊
var add = require('./add.js');
module.exports.increment = function () {
  return add(val, 1);
}複製代碼
你們可能會疑惑,並無定義module,require 這兩個屬性是怎麼來的呢??(後面在介紹Nodejs模塊化——模塊編譯部分會給你們詳細介紹,這裏先簡單說一下)。
其實,一個文件表明一個模塊,一個模塊除了本身的函數做用域以外,最外層還有一個模塊做用域,module就是表明這個模塊,exports是module的屬性。require也在這個模塊的上下文中,用來引入外部模塊。
(3)模塊標識
模塊標識就是require( )函數的參數,規範是這樣的:
  • 必須是字符串
  • 能夠是以./ ../開頭的相對路徑
  • 能夠是絕對路徑
  • 能夠省略後綴名
CommonJS的模塊規範定義比較簡單,意義在於將類聚的方法和變量等限定在私有的做用域中,同時支持引入和導出將上下游模塊無縫銜接,每一個模塊具備獨立的空間,它們互不干擾。

2.3 Nodejs的模塊化實現


Node模塊在實現中並不是徹底按照CommonJS來,進行了取捨,增長了一些自身的的特性。
Node中一個文件是一個模塊——module
一個模塊就是一個Module的實例
Node中Module構造函數:
function Module(id, parent){
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if(parent && parent.children) {
    parent.children.push(this);
  }
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

//實例化一個模塊
 var module = new Module(filename, parent);複製代碼
其中id 是模塊id,exports是這個模塊要暴露出來的api,parent是父級模塊,loaded表示這個模塊是否加載完成,由於CommonJS是運行時加載,loaded表示文件是否已經執行完畢返回一個對象。

2.4 Node模塊分類

如圖所示Node模塊通常分爲兩種核心模塊和文件模塊。
圖1 模塊分類
核心模塊——就是Node內置的模塊好比http, path等。在Node的源碼的編譯時,核心模塊就一塊兒被編譯進了二進制執行文件,部分核心模塊(內建模塊)被直接加載進內存中。

在Node模塊的引入過程當中,通常要通過一下三個步驟
  • 路徑分析
  • 文件定位
  • 編譯執行
核心模塊會省略文件定位和編譯執行這兩步,而且在路徑分析中會優先判斷,加載速度比通常模塊更快。
文件模塊——就是外部引入的模塊如node_modules裏經過npm安裝的模塊,或者咱們項目工程裏本身寫的一個js文件或者json文件。
文件模塊引入過程以上三個步驟都要經歷。

2.5 那麼NodeJS require的時候是怎麼路徑分析,文件定位而且編譯執行的?


2.5.1 路徑分析
前面已經說過,不論核心模塊仍是文件模塊都須要經歷路徑分析這一步,當咱們require一個模塊的時候,Node是怎麼區分是核心模塊仍是文件模塊,而且進行查找定位呢?
Node支持以下幾種形式的模塊標識符,來引入模塊:
//核心模塊
require('http')

----------------------------
//文件模塊

//以.開頭的相對路徑,(能夠不帶擴展名)
require('./a.js')

  
//以..開頭的相對路徑,(能夠不帶擴展名)
require('../b.js')


//以/開始的絕對路徑,(能夠不帶擴展名)
require('/c.js')


//外部模塊名稱
require('express')

//外部模塊某一個文件
require('codemirror/addon/merge/merge.js');複製代碼
那麼對於這個都是字符串的引入方式,
  • Node 會優先去內存中查找匹配核心模塊,若是匹配成功便不會再繼續查找
(1)好比require http 模塊的時候,會優先從核心模塊裏去成功匹配
  • 若是核心模塊沒有匹配成功,便歸類爲文件模塊
(2) 以.、..和/開頭的標識符,require都會根據當前文件路徑將這個相對路徑或者絕對路徑轉化爲真實路徑,也就是咱們平時最多見的一種路徑解析
(3)非路徑形式的文件模塊 如上面的'express' 和'codemirror/addon/merge/merge.js',這種模塊是一種特殊的文件模塊,通常稱爲自定義模塊。
自定義模塊的查找最費時,由於對於自定義模塊有一個模塊路徑,Node會根據這個模塊路徑依次遞歸查找。

模塊路徑——Node的模塊路徑是一個數組,模塊路徑存放在module.paths屬性上。
咱們能夠找一個基於npm或者yarn管理項目,在根目錄下建立一個test.js文件,內容爲console.log(module.paths),以下:
//test.js
console.log(module.paths);複製代碼
而後在根目錄下用Node執行
node test.js複製代碼
能夠看到咱們已經將模塊路徑打印出來。
圖2 模塊路徑
能夠看到模塊路徑的生成規則以下:
  • 當前路文件下的node_modules目錄
  • 父目錄下的node_modules目錄
  • 父目錄的父目錄下的node_modules目錄
  • 沿路徑向上逐級遞歸,直到根目錄下的node_modules目錄
對於自定義文件好比express,就會根據模塊路徑依次遞歸查找。
在查找同時並進行文件定位。
2.5.2 文件定位
  • 擴展名分析
咱們在使用require的時候有時候會省略擴展名,那麼Node怎麼定位到具體的文件呢?
這種狀況下,Node會依次按照.js、.json、.node的次序一次匹配。(.node是C++擴展文件編譯以後生成的文件)
若擴展名匹配失敗,則會將其當成一個包來處理,我這裏直接理解爲npm包
  • 包處理
對於包Node會首先在當前包目錄下查找package.json( CommonJS包規範)經過JSON.parse( )解析出包描述對象,根據main屬性指定的入口文件名進行下一步定位。
若是文件缺乏擴展名,將根據擴展名分析規則定位。
若main指定文件名錯誤或者壓根沒有package.json,Node會將包目錄下的index當作默認文件名。
再依次匹配index.js、index.json、index.node。
若以上步驟都沒有定位成功將,進入下一個模塊路徑——父目錄下的node_modules目錄下查找,直到查找到根目錄下的node_modules,若都沒有定位到,將拋出查找失敗的異常。
2.5.3 模塊編譯
  • .js文件——經過fs模塊同步讀取文件後編譯執行
  • .node文件——用C/C++編寫的擴展文件,經過dlopen( )方法加載最後編譯生成的文件。
  • .json——經過fs模塊同步讀取文件後,用JSON.parse( ) 解析返回結果。
  • 其他擴展名文件。它們都是被當作.js文件載入。
每個編譯成功的文件都會將其文件路徑做爲索引緩存在Module._cache對象上,以提升二次引入的性能。
這裏咱們只講解一下JavaScript模塊的編譯過程,以解答前面所說的CommonJS模塊中的require、exports、module變量的來源。
咱們還知道Node的每一個模塊中都有__filename、__dirname 這兩個變量,是怎麼來的的呢?
其實JavaScript模塊在編譯過程當中,Node對獲取的JavaScript文件內容進行了頭部和尾部的包裝。在頭部添加了(function (exports, require, module,__filename, __dirname){\n,而在尾部添加了\n}); 。
所以一個JS模塊通過編譯以後會被包裝成下面的樣子:
(function(exports, require, module, __filename, __dirname){
  var express = require('express') ;
  exports.method = function (params){
   ...
  };
});複製代碼

三、前端模塊化

前面咱們所說的CommonJS規範,都是基於node來講的,因此前面說的CommonJS都是針對服務端的實現。

3.1 前端模塊化和服務端模塊化有什麼區別?

  • 服務端加載一個模塊,直接就從硬盤或者內存中讀取了,消耗時間能夠忽略不計
  • 瀏覽器須要從服務端下載這個文件,因此說若是用CommonJS的require方式加載模塊,須要等代碼模塊下載完畢,並運行以後才能獲得所須要的API。

3.2 爲何CommonJS不適用於前端模塊?

若是咱們在某個代碼模塊裏使用CommonJS的方法require了一個模塊,而這個模塊須要經過http請求從服務器去取,若是網速很慢,而CommonJS又是同步的,因此將阻塞後面代碼的執行,從而阻塞瀏覽器渲染頁面,使得頁面出現假死狀態。

所以後面AMD規範隨着RequireJS的推廣被提出,異步模塊加載,不阻塞後面代碼執行的模塊引入方式,就是解決了前端模塊異步模塊加載的問題。

3.3 AMD(Asynchronous Module Definition) & RequireJS

AMD——異步模塊加載規範 與CommonJS的主要區別就是異步模塊加載,就是模塊加載過程當中即便require的模塊尚未獲取到,也不會影響後面代碼的執行。
RequireJS——AMD規範的實現。其實也能夠說AMD是RequireJS在推廣過程當中對模塊定義的規範化產出。
模塊定義:
(1)獨立模塊的定義——不依賴其它模塊的模塊定義
//獨立模塊定義
define({
  method1: function() {}
  method2: function() {}
});  

//或者
define(function(){
  return {
    method1: function() {},
    method2: function() {},
  }
});                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  複製代碼
(2)非獨立模塊——依賴其餘模塊的模塊定義
define(['math', 'graph'], function(math, graph){
  ...
});複製代碼
模塊引用:
require(['a', 'b'], function(a, b){
  a.method();
  b.method();
})複製代碼

3.4 CommonJS 和AMD的對比:

  • CommonJS通常用於服務端,AMD通常用於瀏覽器客戶端
  • CommonJS和AMD都是運行時加載

3.5 什麼是運行時加載?

我以爲要從兩個點上去理解:
  • CommonJS 和AMD模塊都只能在運行時肯定模塊之間的依賴關係
  • require一個模塊的時候,模塊會先被執行,並返回一個對象,而且這個對象是總體加載的
//CommonJS 模塊
let { basename, dirname, parse } = require('path');

//等價於
let _path = require('path');
let basename = _path.basename, dirname = _path.dirname, parse = _path.parse;複製代碼
上面代碼實質是總體加載path模塊,即加載了path全部方法,生成一個對象,而後再從這個對象上面讀取3個方法。這種加載就稱爲"運行時加載"。
再看下面一個AMD的例子:
//a.js
define(function(){
  console.log('a.js執行');
  return {
    hello: function(){
      console.log('hello, a.js');
    }
  }
});

//b.js
require(['a'], function(a){
  console.log('b.js 執行');
  a.hello();
  $('#b').click(function(){
    b.hello();
  });
});複製代碼
運行b.js時獲得結果:
//a.js執行
//b.js執行
//hello, a.js複製代碼
能夠看到當運行b.js時,由於b.js require a.js模塊的時候後a.js模塊會先執行。驗證了前面所說的"require一個模塊的時候,模塊會先被執行"。

3.6 CMD(Common Module Definition) & SeaJS

CMD——通用模塊規範,由國內的玉伯提出。
SeaJS——CMD的實現,其實也能夠說CMD是SeaJS在推廣過程當中對模塊定義的規範化產出。
與AMD規範的主要區別在於定義模塊和依賴引入的部分。AMD須要在聲明模塊的時候指定全部的依賴,經過形參傳遞依賴到模塊內容中:
define(['dep1', 'dep2'], function(dep1, dep2){
  return function(){};
})複製代碼
與AMD模塊規範相比,CMD模塊更接近於Node對CommonJS規範的定義:
define(factory);複製代碼
在依賴示例部分,CMD支持動態引入,require、exports和module經過形參傳遞給模塊,在須要依賴模塊時,隨時調用require( )引入便可,示例以下:
define(function(require, exports, module){
  //依賴模塊a
  var a = require('./a');

  //調用模塊a的方法
  a.method();
})複製代碼
也就是說與AMD相比,CMD推崇依賴就近, AMD推崇依賴前置。

3.7 UMD(Universal Module Definition) 通用模塊規範

以下是codemirror模塊lib/codemirror.js模塊的定義方式:
(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' 
       ? module.exports = factory()          // Node , CommonJS
       : typeof define === 'function' && define.amd  
         ? define(factory)                   //AMD CMD
         : (global.CodeMirror = factory());  //模塊掛載到全局
}(this, (function () { 
   ...
})複製代碼
能夠看說所謂的兼容模式是將幾種常見模塊定義方式都兼容處理。

目前爲止,前端經常使用的幾種模塊化規範都已經提到,還有一種咱們項目裏用得很是多的模塊化引入和導出,就是ES6的模塊化。

3.8 ES6模塊

如前面所述,CommonJS和AMD都是運行時加載。ES6在語言規格層面上實現了模塊功能,是編譯時加載,徹底能夠取代現有的CommonJS和AMD規範,能夠成爲瀏覽器和服務器通用的模塊解決方案。這裏關於ES6模塊咱們項目裏使用很是多,因此詳細講解。
ES6模塊使用——export
(1)導出一個變量
export var name = 'pengpeng';複製代碼
(2)導出一個函數
export function foo(x, y){}複製代碼
(3)經常使用導出方式(推薦)
// person.js
const name = 'dingman';
const age = '18';
const addr = '卡爾斯特森林';

export { firstName, lastName, year };複製代碼
(4)As用法
const s = 1;
export {
  s as t,
  s as m, 
}複製代碼
能夠利用as將模塊輸出屢次。
ES6模塊使用——import
(1)通常用法
import { name, age } from './person.js';複製代碼
(2)As用法
import { name as personName } from './person.js';複製代碼
import命令具備提高效果,會提高到整個模塊的頭部,首先執行,以下也不會報錯:
getName();

import { getName } from 'person_module';複製代碼
(3)總體模塊加載 *
//person.js
export name = 'xixi';
export age = 23;

//逐一加載
import { age, name } from './person.js';

//總體加載
import * as person from './person.js';
console.log(person.name);
console.log(person.age);複製代碼
ES6模塊使用——export default
其實export default,在項目裏用的很是多,通常一個Vue組件或者React組件咱們都是使用export default命令,須要注意的是使用export default命令時,import是不須要加{}的。而不使用export default時,import是必須加{},示例以下:
//person.js
export function getName() {
 ...
}
//my_module
import {getName} from './person.js';

-----------------對比---------------------

//person.js
export default function getName(){
 ...
}
//my_module
import getName from './person.js';複製代碼
export default實際上是導出一個叫作default的變量,因此其後面不能跟變量聲明語句。
//錯誤
export default var a = 1;複製代碼
值得注意的是咱們能夠同時使用export 和export default
//person.js
export name = 'dingman';
export default function getName(){
  ...
}

//my_module
import getName, { name } from './person.js';複製代碼

前面一直提到,CommonJS是運行時加載,ES6時編譯時加載,那麼兩個有什麼本質的區別呢?

3.9 ES6模塊與CommonJS模塊加載區別

ES6模塊的設計思想,是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。因此說ES6是編譯時加載,不一樣於CommonJS的運行時加載(實際加載的是一整個對象),ES6模塊不是對象,而是經過export命令顯式指定輸出的代碼,輸入時也採用靜態命令的形式:
//ES6模塊
import { basename, dirname, parse } from 'path';

//CommonJS模塊
let { basename, dirname, parse } = require('path');複製代碼
以上這種寫法與CommonJS的模塊加載有什麼不一樣?
  • 當require path模塊時,其實 CommonJS會將path模塊運行一遍,並返回一個對象,並將這個對象緩存起來,這個對象包含path這個模塊的全部API。之後不管多少次加載這個模塊都是取這個緩存的值,也就是第一次運行的結果,除非手動清除。
  • ES6會從path模塊只加載3個方法,其餘不會加載,這就是編譯時加載。ES6能夠在編譯時就完成模塊加載,當ES6遇到import時,不會像CommonJS同樣去執行模塊,而是生成一個動態的只讀引用,當真正須要的時候再到模塊裏去取值,因此ES6模塊是動態引用,而且不會緩存值。
由於CommonJS模塊輸出的是值的拷貝,因此當模塊內值變化時,不會影響到輸出的值。基於Node作如下嘗試:
//person.js
var age = 18;
module.exports ={
  age: age,
  addAge: function () {
    age++;
  }
} 

//my_module
var person = require('./person.js');
console.log(person.age);
person.addAge();
console.log(person.age);

//輸出結果
18
18複製代碼
能夠看到內部age的變化並不會影響person.age的值,這是由於person.age的值始終是第一次運行時的結果的拷貝。
再看ES6
//person.js
export let age = 18;
export function addAge(){
  age++;
}

//my_module
import { age, addAge } from './person.js';
console.log(age);
addAge();
console.log(age);

//輸出結果
18
19

複製代碼

總結

前端模塊化規範包括CommonJS/ AMD/CMD/ES6模塊化,平時咱們可能只知其中一種但不能全面瞭解他們的發展歷史、用法和區別,以及當咱們使用require 和import的時候到底發生了什麼,這篇文章給你們算是比較全面的作了一次總結(我只是搬運工)。

PS: 因爲文章太長爲了方便閱讀,前端模塊化webpack實際項目講解,放在續篇 前端模塊化二——webpack實際項目中的模塊化

參考

2. 樸靈 (2013) 深刻淺出Node.js. 人民郵電出版社, 北京。
相關文章
相關標籤/搜索