JavaScript 模塊化的前世今生

我最早接觸前端應(yīng)該是在 2013 年左右频敛,雖然那個(gè)時(shí)候還在讀大二,但已經(jīng)和同學(xué)開始折騰一些校園創(chuàng)業(yè)項(xiàng)目。當(dāng)時(shí)希望開發(fā)一個(gè)面向校園的網(wǎng)上零食商城蜂厅,我們從批發(fā)市場進(jìn)貨然后在校園內(nèi)網(wǎng)上以低于「小賣部」的價(jià)格出售愉择。由于人手不足劫乱,寫后端的我也同時(shí)兼顧了前端的開發(fā)工作[1]

[1] 這種情況在讀研甚至工作的時(shí)候也在不斷發(fā)生锥涕,以至于現(xiàn)在我都懷疑自己的前端代碼量可能快要超過后端代碼量了衷戈。

雖然當(dāng)時(shí)沒有系統(tǒng)性的學(xué)習(xí)過前端,但得益于諸多開發(fā)者在網(wǎng)上分享和開源自己的前端組件层坠,使得我在開發(fā)過程中能夠隨時(shí)使用現(xiàn)成組件殖妇,從而快速搭建出了系統(tǒng)的第一版。

海量的前端組件庫為當(dāng)時(shí)的開發(fā)帶來了極大的便利破花,但也帶來了一些困擾谦趣。這些組件很多時(shí)候需要使用 script 命令引入,例如:

<!-- a 組件需要用到的庫 -->
<script src="a1.js"></script>
<!-- b 組件需要用到的庫 -->
<script src="b1.js"></script>
<script src="b2.js"></script>
<!-- c 組件需要用到的庫 -->
<script src="c1.js"></script>

在開發(fā)過程中座每,經(jīng)常會(huì)遇到:

  • 引入的 .js 文件越來越多前鹅,越來越復(fù)雜
  • 上述 .js 文件引入順序被不小心改變導(dǎo)致某個(gè)庫不可用
  • 引入一個(gè)新的 .js 文件導(dǎo)致某個(gè)庫不可用

隨著使用的組件越來越多,引入的庫越來越復(fù)雜峭梳,上面的問題反復(fù)出現(xiàn)舰绘,很多時(shí)候需要花很多時(shí)間去理清各個(gè)組件庫的依賴,必要時(shí)更新庫的版本、調(diào)整引入順序捂寿,甚至修改組件內(nèi)部的沖突代碼才能使得頁面正常運(yùn)行口四。可以說當(dāng)時(shí)我有大量的時(shí)間都花費(fèi)在了這些不必要的調(diào)試上秦陋,最終導(dǎo)致我的前端開發(fā)初體驗(yàn)并不好窃祝,甚至可以說糟糕。

直到后面接觸了 Node.js踱侣,我才意識(shí)到之前在瀏覽器端遇到的種種問題的根本原因是:模塊化的缺失粪小。

模塊化歷史與演化

函數(shù)封裝

樸實(shí)無華:全局函數(shù)寫法

上面提及的通過 script 引入的 .js 文件通常都是第三方 UI 組件所需要的模塊,這些模塊內(nèi)封裝了對(duì)應(yīng)的函數(shù)或功能抡句,為的是提高程序的復(fù)用性和可維護(hù)性探膊。

那么該如何封裝一個(gè)可被復(fù)用的模塊?最為原始和簡單的方式就是將公用函數(shù)放到一個(gè) xxx.js 文件中:

/* utils.js */
function HandleDog() {
  // ...
}

function HandleCat() {
  // ...
}

使用時(shí)通過 script 語句導(dǎo)入皆可:

<script src="utils.js"/>

但是通過 script 語句引入之后待榔,HandleDog 函數(shù) 和 HandleCat 函數(shù) 以及內(nèi)部的其他變量實(shí)際就成為了全局變量逞壁。

全局變量很容易遇到命名沖突問題,即很有可能你引用的另一個(gè)模塊 utils2.js 內(nèi)有同名的 HandleDog 函數(shù)锐锣。同時(shí)自己編寫代碼的時(shí)候更要處處當(dāng)心自己的函數(shù)腌闯、變量和模塊內(nèi)的函數(shù)、變量產(chǎn)生沖突雕憔。

小小優(yōu)化:對(duì)象寫法

為了在一定程度上解決或改進(jìn)「全局函數(shù)寫法」姿骏,可以將函數(shù)、變量封裝成一個(gè)對(duì)象斤彼,如:

var Utils = {
  _name: 'utils',
  _desc: 'utils for dog and cat',
  HandleDog: function() {/* ... */},
  HandleCat: function() {/* ... */},
}

因?yàn)閷?duì) HandleDog分瘦、HandleCat 等變量封裝了一層,所以減少了產(chǎn)生沖突的概率琉苇。但這依然不能從根本上解決命名沖突這個(gè)問題嘲玫,所以當(dāng)時(shí)有一些模塊如 Yahoo! 的 YUI2 進(jìn)一步引入了「命名空間」的概念,即給模塊再加上一個(gè)類似「Java 包名」的唯一前綴:

var cn = {};
cn.front = {};
cn.front.Utils = {};

cn.front.Utils._name = 'utils';
cn.front.Utils._desc = 'utils for dog and cat';
cn.front.Utils.HandleDog = function() {/* ... */};
cn.front.Utils.HandleCat = function() {/* ... */};

上述代碼確實(shí)算是解決了命名沖突的問題并扇,但由于沒有 Java 中的 import 關(guān)鍵字實(shí)現(xiàn)前綴省略去团,所以每次調(diào)用模塊內(nèi)函數(shù)或變量都得寫全命名空間:

if (type === 'dog') {
  var d_result = cn.front.Utils.HandleDog(dog);
} else {
  var c_result = cn.front.Utils.HandleCat(cat);
}

這樣的編程體驗(yàn)著實(shí)令人痛苦。同時(shí)當(dāng)前的寫法依然存在一個(gè)重大的缺陷穷蛹,即變量和函數(shù)實(shí)際沒能實(shí)現(xiàn)真正的封裝土陪,變量和函數(shù)對(duì)外都是公開暴露的,我們隨時(shí)可以訪問甚至修改模塊內(nèi)部的屬性:

// 修改模塊內(nèi)部變量
cn.front.Utils._name = 'changed';

有點(diǎn)意思:立即執(zhí)行函數(shù)寫法 IIFE

立即執(zhí)行函數(shù)(Immediately-Invoked Function Expression俩莽,IIFE)旺坠,能夠?qū)崿F(xiàn)函數(shù)聲明的同時(shí)立即被執(zhí)行。在 JS 中扮超,一個(gè)函數(shù)被執(zhí)行時(shí)將創(chuàng)建一個(gè)新的上下文取刃,在這個(gè)函數(shù)內(nèi)定義的變量處于一個(gè)單獨(dú)的作用域內(nèi)蹋肮,只能被函數(shù)內(nèi)部訪問,不會(huì)暴露到函數(shù)外部璧疗。這樣再結(jié)合閉包[2]坯辩,就能夠?qū)崿F(xiàn)私有變量封裝的效果:

function createModule() {
  var _name = 'utils';
  var _desc = 'utils for dog and cat';
  var HandleDog = function() {/* ... */};
  var HandleCat = function() {/* ... */};
  var GetName = function() {/* ... */};
  
  const ret = {
    HandleDog: HandleDog,
    HandleCat: HandleCat,
    GetName: GetName,
  };

  return ret;
}

const moduleA = createModule();

// 如果想要訪問 _name,只能通過對(duì)外開放的 GetName 函數(shù)
// console.log(moduleA._name);   // 錯(cuò)誤崩侠,moduleA 無法直接訪問內(nèi)部私有的 _name

[2] 有關(guān)閉包可以閱讀這篇文章: 一個(gè)故事講閉包

我們需要再進(jìn)一步漆魔,上面的 createModule 實(shí)際也是對(duì)這個(gè)模塊的不必要命名,因?yàn)槲覀冎皇窍氲玫缴厦婧瘮?shù)返回的 ret 結(jié)果 而已却音,因?yàn)檫@個(gè) ret 結(jié)果 就是模塊本身改抡。既然如此我們希望 createModule 函數(shù)是匿名函數(shù)能夠立即執(zhí)行,從而獲取到我們需要的模塊系瓢。那么該如何讓這個(gè)函數(shù)立即執(zhí)行呢阿纤?調(diào)用形式是否可以如下呢:

// 錯(cuò)誤寫法
function createModule() {
  /* ... */
}();

調(diào)用一個(gè)函數(shù)的語法,是在函數(shù)名的后面添加上 () 符號(hào)夷陋,所以可能自然的想到上面的寫法士败。但是上面寫法是錯(cuò)誤的攒钳,因?yàn)?JavaScript 引擎在解析時(shí),在行首遇到 funciton 關(guān)鍵字時(shí)會(huì)將其視「函數(shù)聲明」吕粹,而函數(shù)調(diào)用語句 funcA() 則是一個(gè)「函數(shù)表達(dá)式」稽煤。為了讓 JavaScript 引擎知曉后面是一個(gè)函數(shù)表達(dá)式富玷,我們可以在行首加上 (败潦,也就是用 () 將函數(shù)聲明包起來:

// 當(dāng)然還有其他寫法讓 JavaScript 引擎知曉后面是一個(gè)函數(shù)表達(dá)式
// 例如行首添加  `+`啃擦、`-` 等符號(hào),這里不做擴(kuò)展
(function createModule() {
  /* ... */
})();

同時(shí) createModule 可以是一個(gè)匿名函數(shù)诺凡,最終得到:

var module = (function() {
  /* ... */
})();

上述的立即執(zhí)行函數(shù) IIFE东揣,最終實(shí)現(xiàn)了對(duì)模塊內(nèi)函數(shù)和私有變量的真正封裝践惑,我們可以通過 module 調(diào)用模塊內(nèi)的公開函數(shù)或公開變量腹泌,但同時(shí)又無法訪問任何不該被外部知曉的私有變量。

立即執(zhí)行函數(shù) IIFE 比起之前的幾種封裝方案尔觉,已經(jīng)有相當(dāng)大的進(jìn)步了凉袱,當(dāng)年的 JQuery 就是大量采用 IIFE 實(shí)現(xiàn)模塊封裝。但模塊化并不是一個(gè)簡單的事情侦铜,要考慮的事情是多方面的专甩。IIFE 方案依然面臨一個(gè)最基本的問題:模塊之間的依賴問題

如果我們想要在一個(gè)模塊中使用另一個(gè)模塊钉稍,其實(shí)可以通過立即執(zhí)行函數(shù)傳參的方式實(shí)現(xiàn):

var module = (function($) {
  /* ... */
  $('.class').css("color","red");
})(jQuery);

// 通過傳入 window涤躲,將模塊掛在 window 下
(function(window, $) {
  /* ... */
  $('.class').css("color","red");
  window.utils = utils;
})(window, jQuery);

如上所示,將一個(gè)模塊 jQuery 通過參數(shù)傳入 module 模塊贡未,就能在 module 模塊內(nèi)部使用 jQuery 模塊了种樱。但是將 jQuery 這個(gè)參數(shù)傳遞給 module 的前提是:jQuery 已經(jīng)被加載初始化蒙袍。也就是在通過 script 引入模塊時(shí),需要將 jQuery 引入寫在 module 模塊引入之前嫩挤,此時(shí)這個(gè)潛在的次序關(guān)系必須由程序員手動(dòng)管理害幅。這也就是上文我提到的現(xiàn)象:「需要經(jīng)常調(diào)整和管理 script 腳本之間的先后順序」。

隨著模塊的數(shù)量不斷增加岂昭,模塊之前的依賴變得愈發(fā)復(fù)雜以现,看來 JavaScript 的模塊化[3]依然任重道遠(yuǎn)。

[3] 因?yàn)?JavaScript 的設(shè)計(jì)發(fā)明過于倉促和簡化(Brendan Eich 僅用了十天就推出了第一版)约啊,所以在設(shè)計(jì)之處留下了很多的「坑」邑遏。模塊化就是眾多的「坑」之一,這些不完善的模塊化方案都是由于 JavaScript 一開始沒能提供模塊化規(guī)范導(dǎo)致的恰矩。包括下文將要介紹的各種模塊化規(guī)范有很多都是「民間」自發(fā)推行的模塊化規(guī)范无宿,實(shí)際上這多少增加了 JavaScript 語言的繁雜性。

最后再對(duì)立即執(zhí)行函數(shù) IIFE 做一些補(bǔ)充枢里,由于 ES6 正式引入了模塊化規(guī)范孽鸡,所以立即執(zhí)行函數(shù) IIFE 實(shí)現(xiàn)模塊化也就沒有什么必要了。

同時(shí)立即執(zhí)行函數(shù) IIFE 由于作用域封裝的特性栏豺,能夠達(dá)到避免污染全局變量或外部變量的效果彬碱,所以在 ES6 之前經(jīng)常被用來實(shí)現(xiàn)「塊作用域」。但由于 ES6 引入了 let奥洼、const 關(guān)鍵字實(shí)現(xiàn)了「塊作用域」巷疼,所以也沒必要通過立即執(zhí)行函數(shù) IIFE 來實(shí)現(xiàn)「塊作用域」了。

模塊化規(guī)范

前端領(lǐng)域在模塊化這件事磕磕碰碰的前行了很久灵奖,雖然摸索出了上文提及的一些方案嚼沿,但每種方案距離真正的模塊化都有一定的距離。更重要的是瓷患,模塊化需要每個(gè)人采用同一種方案骡尽,也就是模塊化最終還是需要定下規(guī)范。

CommonJS

概述

時(shí)間來到了 2009 年擅编,JavaScript 在瀏覽器端已經(jīng)運(yùn)行了十多年攀细,逐步扎穩(wěn)了腳跟。此時(shí)一個(gè)有趣的想法正在不斷發(fā)酵中:即讓 JavaScript 運(yùn)行在服務(wù)器端爱态。當(dāng)然在具體實(shí)現(xiàn)之前谭贪,如果能夠先統(tǒng)一出一個(gè)規(guī)范的話,JavaScript 在服務(wù)器端的發(fā)展可能會(huì)更加可期锦担。

于是 2009 年 1 月俭识,Mozilla 的工程師 Kevin Dangoor 發(fā)起了制定這個(gè)規(guī)范的提案,這個(gè)規(guī)范最初被命名為 ServerJS洞渔。服務(wù)器端的 JavaScript 規(guī)范自然會(huì)涉及到文件系統(tǒng)套媚、IO理盆、HTTP 客戶端等方方面面,當(dāng)然其中最重要的是不會(huì)再重蹈瀏覽器端的覆轍凑阶,所以 ServerJS 一開始就引入了模塊化規(guī)范并命名為 Modules/1.0猿规。

大概是同年的 2 月份左右,Ryan Dahl 開始開發(fā) JavaScript 服務(wù)器端的運(yùn)行時(shí)環(huán)境 Node.js宙橱,而在模塊化這一方面則采用了 ServerJS 的模塊化規(guī)范姨俩。

2009 年的下半年,由于 ServerJS 規(guī)范推動(dòng)的很不錯(cuò)师郑,同時(shí)看到瀏覽器端一直缺少模塊化規(guī)范的窘境环葵,所以社區(qū)開始期望將已經(jīng)得到 Node.js 實(shí)踐的模塊化規(guī)范推廣到瀏覽器端,社區(qū)也改名為 CommonJS宝冕。

之后我們也將 CommonJS 社區(qū)推行张遭,Node.js 實(shí)踐的模塊化規(guī)范稱為 CommonJS 規(guī)范

CommonJS 規(guī)定了一個(gè)文件就是一個(gè)模塊地梨,每個(gè)模塊內(nèi)定義的變量菊卷、函數(shù)等都是私有的,對(duì)其他模塊不可見宝剖。但可通過 global 來定義對(duì)其他文件可見的變量:

/* utils.js */
var _name = 'utils';
var _desc = 'utils for dog and cat';
var HandleDog = function() {/* ... */};
var HandleCat = function() {/* ... */};
var GetName = function() {/* ... */};

global.isGlobal = true;

上述代碼定義的 _name洁闰、_desc 等變量或 HandleDog 等函數(shù)是 utils.js 模塊私有的,其他文件(模塊)不可見万细。但 isGlobal 則可以在其他文件訪問[4]扑眉。

[4] 但一般情況下,不推薦使用赖钞。

模塊導(dǎo)出

CommonJS 規(guī)范可使用 module.exports 實(shí)現(xiàn)模塊導(dǎo)出:

/* utils.js */
var _name = 'utils';
var _desc = 'utils for dog and cat';
var HandleDog = function() {/* ... */};
var HandleCat = function() {/* ... */};
var GetName = function() {/* ... */};

module.exports.HandleDog = HandleDog;
module.exports.HandleCat = HandleCat;
module.exports.GetName = GetName;

console.log('模塊信息:', module);

每個(gè)模塊內(nèi)部都自帶了一個(gè)變量 module腰素,表示當(dāng)前模塊,上述代碼的 console.log 將輸出如下內(nèi)容:

模塊信息: Module {
  id: '.',  // 模塊的識(shí)別符
  exports: // 本模塊導(dǎo)出的值
   { HandleDog: [Function: HandleDog],
     HandleCat: [Function: HandleCat],
     GetName: [Function: GetName] },
  parent: null,  // 調(diào)用本模塊的模塊雪营,null 表明此文件為入口文件
  filename: '/Users/xxxx/work/git_repository/demo/js-demo/es6_module.js',  // 模塊的文件名(帶絕對(duì)路徑)
  loaded: false,  // 模塊是否已經(jīng)被加載
  children: [],    // 本模塊引用的其他模塊
  paths:  // 模塊的搜索路徑
   [ '/Users/xxxx/work/git_repository/demo/js-demo/node_modules',
     '/Users/xxxx/work/git_repository/demo/node_modules',
     '/Users/xxxx/work/git_repository/node_modules',
     '/Users/xxxx/work/node_modules',
     '/Users/xxxx/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }

由上述結(jié)果可知 module 對(duì)象存儲(chǔ)了本文件對(duì)應(yīng)的模塊信息弓千,而其中 module.exports 屬性更是重點(diǎn),它表示的實(shí)際上就是本模塊對(duì)外導(dǎo)出了什么卓缰。當(dāng)我們進(jìn)行所謂的導(dǎo)入加載模塊時(shí)计呈,實(shí)際上就是在獲取 module.exports 屬性

同時(shí)為了寫法的一點(diǎn)點(diǎn)簡化征唬,CommonJS 在每個(gè)模塊內(nèi)還設(shè)置一個(gè)屬性 exports 指向 module.exports,相當(dāng)于在每個(gè)模塊的頭部幫我們執(zhí)行了以下代碼:

var exports = module.exports;

需要注意的是茁彭,我們可以在 exports 上掛載各種屬性或函數(shù)总寒,因?yàn)檫@相當(dāng)于掛載在 module.exports 下,但不可以直接對(duì) exports 進(jìn)行賦值理肺,因?yàn)檫@將導(dǎo)致 exports 指向另一個(gè)地方摄闸,也就沒有導(dǎo)出的效果了:

/* utils.js */
var exports = module.exports;  // exports 指向 module.exports;

var funcA = function() {/* ... */};
exports.funcA = funcA();          // 有效善镰,導(dǎo)出 {funcA: funcA}
module.exports.funcA = funcA();  // 有效,與上條等價(jià)
module.exports = { funcA: funcA }; // 有效年枕,與上面等價(jià)

module.exports = funcA();  // 有效炫欺,導(dǎo)出函數(shù) funcA()

// 無效,exports 原先指向 module.exports熏兄,現(xiàn)在指向 funcA()
// 而 module.exports 內(nèi)容依然為空品洛,固最終導(dǎo)出的是 {}
exports = funcA();

exportsmodule.exports 混在一起有時(shí)候令人迷惑,在實(shí)際開發(fā)時(shí)摩桶,也可以全部統(tǒng)一使用 module.exports桥状。

模塊導(dǎo)入

上文介紹了 CommonJS 如何編寫模塊并導(dǎo)出后,我們來看看 CommonJS 如何導(dǎo)入加載一個(gè)模塊硝清。

CommonJS 使用 require 來導(dǎo)入加載模塊辅斟,如下所示:

/* main.js */
// 導(dǎo)入加載模塊
var Utils = require('./utils.js');

// 使用模塊的函數(shù)
Utils.HandleDog();

上述代碼中的 require('./utils.js')加載并執(zhí)行 utils.js 文件,然后返回 module.exports 屬性芦拿。

require 的模塊加載還具有以下幾個(gè)特點(diǎn):

  • 同步加載CommonJSrequire 進(jìn)行的是「同步加載」士飒,依次同步加載模塊。
  • 緩存機(jī)制Node.js 在第一次加載時(shí)執(zhí)行模塊并對(duì)模塊進(jìn)行緩存蔗崎,之后的加載將直接從緩存中獲取变汪。
  • 值拷貝與引用拷貝:網(wǎng)上不少中文資料將 require 的輸出視為「值拷貝」,但實(shí)際上是不嚴(yán)謹(jǐn)?shù)恼f法蚁趁。這里需要區(qū)分導(dǎo)出的是基本類型還是復(fù)合類型裙盾,如果導(dǎo)出的是布爾、數(shù)字等原始基本類型他嫡,那么得到的是「值拷貝」番官,對(duì)這份值拷貝進(jìn)行修改不會(huì)影響到模塊內(nèi)變量。但是如果是對(duì)象钢属、數(shù)組等引用類型徘熔,那么得到的是「引用拷貝」,這份「引用拷貝」和模塊內(nèi)引用變量指向同一塊內(nèi)存區(qū)域(即一個(gè)閉包空間淆党,Node.js 實(shí)際會(huì)將每個(gè)文件包裝在一個(gè)函數(shù)中實(shí)現(xiàn)閉包)酷师,所以通過「引用拷貝」改變值將會(huì)影響到模塊內(nèi)的變量。
/* utils.js 模塊 */
var count = 1;
var moreInfo = {
  innerCount: 1,
};

module.exports = {
  count: count,
  moreInfo: moreInfo
}

/* main.js 使用 utils 模塊*/
const Utils = require('./es6_module_utils.js');
let count = Utils.count;
count++;
console.log('導(dǎo)出 count染乌,自增后: ', count);
console.log('模塊內(nèi) count 不受影響: ', Utils.count);

let moreInfo = Utils.moreInfo;
moreInfo.innerCount++;
console.log('導(dǎo)出 inner_count, 自增后: ', moreInfo.innerCount);
console.log('模塊內(nèi) inner_count 受影響: ', Utils.moreInfo.innerCount);

AMD

在上一節(jié)對(duì) CommonJS 的介紹中提及了 2009 年由于 ServerJS 模塊化規(guī)范在服務(wù)器端推動(dòng)的較好山孔,于是社區(qū)改名為 CommonJS,期望將服務(wù)器端的成功經(jīng)驗(yàn)推廣至瀏覽器端荷憋。

但由于瀏覽器端和服務(wù)器端還是存在許多差異台颠,所以這個(gè)推廣過程中產(chǎn)生了諸多不同的觀點(diǎn)和看法,社區(qū)逐步形成了三種流派:

  • Modules/1.x 流派勒庄。
    這個(gè)流派認(rèn)為服務(wù)器端的 Modules/1.0 規(guī)范(即上文介紹的 CommonJS 規(guī)范)已經(jīng)夠用了串前,直接移植到瀏覽器端即可瘫里。當(dāng)然還需要添加一個(gè)所謂的「轉(zhuǎn)換」規(guī)范即 Modules/Transport,用來將服務(wù)器端模塊翻譯為瀏覽端模塊荡碾。

  • Modules/2.0 流派谨读。
    這個(gè)流派則認(rèn)為瀏覽器端有自己的特點(diǎn)(例如模塊可能通過網(wǎng)絡(luò)加載等),直接使用 Modules/1.0 規(guī)范不合適坛吁,需要針對(duì)瀏覽器端的特點(diǎn)做更改劳殖,但另一方面應(yīng)該盡可能的接近 Modules/1.0

  • Modules/Async 流派阶冈。
    這個(gè)流派則是更為激進(jìn)一些闷尿,認(rèn)為瀏覽器端和服務(wù)器端差異較大,所以需要對(duì)瀏覽器端進(jìn)行相對(duì)獨(dú)立的模塊化規(guī)范設(shè)計(jì)女坑。這個(gè)流派的代表就是本節(jié)介紹的 AMD 規(guī)范填具。

上述不同流派都有相應(yīng)的實(shí)現(xiàn),但最終在瀏覽器端被廣泛應(yīng)用的是 Modules/Async 流派AMD 規(guī)范匆骗,即異步模塊定義規(guī)范 Asynchronous Module Definition劳景。

再次回看當(dāng)初各個(gè)流派的觀點(diǎn),不難發(fā)現(xiàn)其中大家關(guān)注的關(guān)鍵點(diǎn)在于瀏覽器端與服務(wù)器端存在差異碉就。

服務(wù)器端的模塊通常從本地加載盟广,所以同步加載的方式不會(huì)影響程序運(yùn)行的效率和性能。但瀏覽器的模塊通常需要從網(wǎng)絡(luò)加載瓮钥,如果一個(gè)模塊加載完成才能執(zhí)行后續(xù)代碼筋量,那么將會(huì)影響瀏覽器端程序的執(zhí)行效率,甚至直接影響用戶的瀏覽體驗(yàn)碉熄。

針對(duì)上述差異桨武,AMD 規(guī)范提倡采用異步加載的方式加載模塊,模塊加載時(shí)不影響其他代碼執(zhí)行锈津,在加載完成之后呀酸,再通過回調(diào)函數(shù)的方式執(zhí)行相關(guān)邏輯。

模塊定義與導(dǎo)出

不同于 CommonJS 的一個(gè)文件對(duì)應(yīng)一個(gè)模塊琼梆,AMD 規(guī)定了一個(gè)全局函數(shù) define 來實(shí)現(xiàn)模塊定義[5]

[5]: 需要注意的是性誉,雖然可以將多個(gè)模塊 define 寫在同一個(gè)文件里,但 requirejs 官方文檔 還是提倡一個(gè)文件里只寫一個(gè)模塊 define

define(id?, dependencies?, factory); 

其中:

  • id:表示定義的模塊的唯一標(biāo)識(shí)符茎杂,可選错览。默認(rèn)為腳本名稱。
  • dependencies:數(shù)據(jù)類型蛉顽,聲明本模塊所需要的依賴模塊蝗砾,可選。默認(rèn)為 ["require", "exports", "module"]
  • factory:模塊初始化需要執(zhí)行的函數(shù)或?qū)ο笮H绻呛瘮?shù)悼粮,則會(huì)被執(zhí)行,其中的返回值為模塊的輸出值曾棕。如果是對(duì)象扣猫,則該對(duì)象即為模塊的輸出值。

AMD 定義一個(gè)簡單模塊如下所示:

// id -> utils 本模塊名稱(標(biāo)識(shí)符)為 utils
// dependencies -> 本模塊依賴 jquery 模塊
// factory -> 本模塊初始化執(zhí)行函數(shù)翘地,且函數(shù)的參數(shù)依次為第二個(gè)參數(shù)聲明的依賴的模塊
define('utils', ['jquery'], function ($) {
  /* ... */

  // 本模塊輸出值(導(dǎo)出值)
  return {
    HandleDog: function() { /* ... */ }
  };
});

注意 dependencies 處模塊的定義風(fēng)格是「前置聲明所有模塊」申尤,而且不管模塊實(shí)際執(zhí)行時(shí)是否會(huì)用到某個(gè)依賴模塊,所以模塊都會(huì)被提前執(zhí)行衙耕。

例如代碼實(shí)際執(zhí)行路徑中未用到模塊昧穿,但依然會(huì)被加載執(zhí)行

// AMD 依賴模塊加載的兩個(gè)特點(diǎn)
// 1. 本模塊所依賴的模塊都需要一次性「前置聲明」
// 2. 且無論是否被使用到,都會(huì)被提前執(zhí)行
define('utils', ['jquery'], function ($) {
  /* ... */
  // 只有當(dāng) flag === xxx 時(shí)才會(huì)使用到 jquery
  // 但無論代碼如何執(zhí)行橙喘,AMD 在一開始就已經(jīng)加載和執(zhí)行 jquery
  if (flag === xxx) {
    $('.class').css("color","red");
  }
  // 本模塊輸出值(導(dǎo)出值)
  return {
    HandleDog: function() { /* ... */ }
  };
});

AMD 后續(xù)補(bǔ)充了一些更為接近 CommonJS 的寫法时鸵,仿佛能夠?qū)崿F(xiàn)「就近聲明」:

/* utils.js */
// 省略模塊 id,則 id 默認(rèn)為腳本文件名稱 utils
define(function(require, exports, module) {
        var a = require('a');  // 在函數(shù)體內(nèi)通過 require 加載模塊
        var b = require('b');

        //Return the module value
        return function () {};
    }
);

但是上述代碼只是為滿足當(dāng)時(shí)開發(fā)者對(duì) CommonJS 風(fēng)格的訴求實(shí)現(xiàn)的「CommonJS 偽支持」厅瞎,其底層依然是 AMD 的「前置聲明和執(zhí)行」饰潜。

如果不依賴其他模塊,還可簡化為如下形式:

/* utils.js */
// 模塊 id 也同樣是可選和簸,不填則默認(rèn)為腳本文件名稱 utils
define(function () {
  /* ... */

  // 本模塊輸出值(導(dǎo)出值)
  return {
    HandleDog: function() { /* ... */ }
  };
});

模塊導(dǎo)入

AMD 使用 require 函數(shù)實(shí)現(xiàn)模塊的導(dǎo)入和加載彭雾,如下所示:

require(modules?, callback); 

其中:

  • modules: 數(shù)組形式,聲明需要使用的模塊锁保。
  • callback: 所需模塊加載完成后的回調(diào)函數(shù)薯酝。

一個(gè)簡單的模塊加載實(shí)例如下所示:

require(['utils'], function (Utils) { 
  /* ... */
  Utils.HandleDog();
}) 

一點(diǎn)補(bǔ)充

AMD 規(guī)范的主要踐行者是 RequireJS,或者反過來說 RequireJSAMD 規(guī)范的主要推動(dòng)者和制定者爽柒。對(duì)更多 RequireJS 語法和細(xì)節(jié)興趣的讀者可以參閱其 官方文檔吴菠。

雖然 RequireJS 在當(dāng)時(shí)具有很好的推廣度,但是由上文的介紹不難看出霉赡,RequireJSCommonJS 在模塊定義和使用的風(fēng)格有較大差異橄务,雖然補(bǔ)充了對(duì) CommonJS 風(fēng)格的支持,但底層加載執(zhí)行方式依然是「前置執(zhí)行[6]穴亏。這些差異導(dǎo)致 AMD 不太受 CommonJS 認(rèn)可蜂挪,甚至于后期 AMD 流派從 CommonJS 社區(qū)獨(dú)立出去成立了自己的 AMD 社區(qū),由 RequireJS 實(shí)際推動(dòng)嗓化。

[6] RequireJS 從 2.0 開始實(shí)現(xiàn)了下文即將介紹的 CMD 所一直推崇的「延遲執(zhí)行」

CMD

CMD(Common Module Definition)規(guī)范玉伯 在推廣自己的前端模塊加載庫 seajs 的過程中產(chǎn)出的模塊化規(guī)范棠涮。

上面提及了早期 AMD 依賴模塊加載的兩個(gè)關(guān)鍵特點(diǎn):

  1. 模塊前置聲明AMD 的依賴模塊需要通過參數(shù)在模塊定義時(shí)給出刺覆。
  2. 模塊提前執(zhí)行严肪。不管函數(shù)體內(nèi)執(zhí)行時(shí)是否會(huì)用到模塊,所有依賴模塊都會(huì)被提前加載執(zhí)行。

CMD 規(guī)范則是主要針對(duì)這兩點(diǎn)實(shí)踐了自己的理念:

  1. 模塊就近聲明驳糯。CMD 的模塊可以在代碼實(shí)際需要的地方聲明導(dǎo)入篇梭。
  2. 模塊延遲執(zhí)行CMD 的模塊在代碼被實(shí)際需要的時(shí)候才會(huì)執(zhí)行酝枢。

模塊定義與導(dǎo)出

CMD 規(guī)范中恬偷,規(guī)定一個(gè)模塊對(duì)應(yīng)一個(gè)文件,使用 define 函數(shù)進(jìn)行模塊定義:

define(factory);

其中:

  • factory: 可以為對(duì)象或函數(shù)帘睦。當(dāng)傳入對(duì)象是袍患,該模塊對(duì)外輸出就是該對(duì)象。當(dāng)傳入函數(shù)時(shí)竣付,則函數(shù)相當(dāng)于該模塊的構(gòu)造函數(shù)诡延,被執(zhí)行后返回的值即為該模塊對(duì)外的輸出。

CMD 定義一個(gè)簡單的模塊:

// factory 函數(shù)默認(rèn)傳入三個(gè)參數(shù):require, exports, module
// require:用來加載模塊
// exports:導(dǎo)出模塊
// module:含有模塊基本信息的對(duì)象
define(function(require, exports, module) {
  // 模塊加載
  var utils = require('./utils');

  var HandleAnimal = function(type) {
    if (type === 'dog') {
      // 就近聲明古胆,延遲執(zhí)行
      var module1 = require('./module1');
      module1.handle();
      utils.HandleDog();
      /* ... */
    } else if (type === 'cat') {
      // 異步加載可使用:require.async(id, callback?)
      var module2 = require.async('./module2', function() {/**/});
      module2.handle();
      utils.HandleCat();
      /* ... */
    }
  }
  
  exports.HandleAnimal = HandleAnimal;  // 模塊對(duì)外輸出
});

CMD 也支持 define(id?, deps?, factory) 的寫法:

但這不是 CMD 默認(rèn)或推崇的寫法肆良,只是對(duì) Modules/Transport 規(guī)范的支持

// define(id?, deps?, factory)
define('animal', ['utils', 'module1', 'module2'], function(require, exports, module) {
  /* ... */
});

模塊導(dǎo)入

CMD 使用 use 函數(shù)在頁面中導(dǎo)入一個(gè)或多個(gè)模塊:

// 加載多個(gè)模塊,加載完成后執(zhí)行回調(diào)
seajs.use('./utils', './module1', './module2', function(utils, module1, module2) {
  utils.HandleDog();
  module1.handle();
  module2.handle();
});

一點(diǎn)補(bǔ)充

更多有關(guān) CMD 的語法和細(xì)節(jié)可查閱 官方文檔赤兴。

UMD 模式

UMD(Universal Module Definition)通用模塊定義妖滔。網(wǎng)上不少文章將其稱為 UMD 規(guī)范,實(shí)際上 UMD 與其說是一種規(guī)范桶良,不如說是一種編程模式座舍,一種為了兼容 CommonJS、AMD陨帆、CMD 等規(guī)范的編程模式曲秉。且實(shí)現(xiàn)原理也非常簡單,本質(zhì)就是通過 if-else 判斷當(dāng)前環(huán)境支持哪種規(guī)范疲牵,支持哪種規(guī)范就按某種規(guī)范做相應(yīng)的模塊封裝承二。按照這種模式封裝后,可以讓代碼運(yùn)行在不同模塊化規(guī)范的服務(wù)器或?yàn)g覽器環(huán)境中纲爸。

UMD 作為編程模式亥鸠,在不同場景可以有不同的寫法:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {  // 如果支持 AMD 規(guī)范
      // 則注冊(cè)為一個(gè) AMD 模塊
      define(['b'], function (b) {
      return (root.returnExportsGlobal = factory(b));
    });
  } else if (typeof module === 'object' && module.exports) { // 不支持 AMD 但支持 CommonJS 規(guī)范的 Node 環(huán)境
      // 則注冊(cè)一個(gè) CommonJS 模塊
    module.exports = factory(require('b'));
  } else {
    // 都不支持,則將模塊輸出注冊(cè)到瀏覽器的全局變量
    root.returnExportsGlobal = factory(root.b);
  }
}(typeof self !== 'undefined' ? self : this, function (b) {
  /* 模塊構(gòu)造函數(shù)本體 */
  /* 使用 b */
  
  // 模塊輸出值
  return {/* ... */};
}));

更多的 UMD 寫法可以查閱 umdjs识啦,其中收集和維護(hù)了一些常用的 UMD 寫法负蚊。

ESM

CommonJSAMDCMD 甚至于 UMD颓哮,前端模塊化規(guī)范可謂「百家爭鳴」家妆。但天下大勢(shì)分久必合,群雄并起的時(shí)代終究是要落下帷幕冕茅。無論是上面哪種模塊化規(guī)范都只是各種前端社區(qū)推行的「民間方案」伤极,做的再好也只是割據(jù)一方罷了蛹找。最終一統(tǒng)天下的還是 JavaScript 的官方標(biāo)準(zhǔn),即 ECMA 標(biāo)準(zhǔn)哨坪。

2015 年 ECMA39 號(hào)技術(shù)委員會(huì)(Technical Committee 39庸疾,簡稱 TC39) 終于推出 ECMAScript 2015 也就是耳熟能詳?shù)?ES6 規(guī)范。其中就包含了 JavaScript 模塊化標(biāo)準(zhǔn) ECMAScript Module齿税,也被簡稱為 ESM彼硫。

模塊定義與導(dǎo)出

ESM 中炊豪,一個(gè)模塊對(duì)應(yīng)一個(gè)文件凌箕,所以直接在文件中定義模塊。同時(shí)使用 export 實(shí)現(xiàn)模塊導(dǎo)出:

/* utils.js */
export const _name = 'utils';
export const _desc = 'utils for dog and cat';
export function HandleDog() {/*...*/}; 
export function HandleCat() {/*...*/}; 

或者將導(dǎo)出值統(tǒng)一寫在一處

// ......

// 導(dǎo)出非 default 變量時(shí)必須加括號(hào)词渤,以下語法是錯(cuò)誤的
// 錯(cuò)誤用法:export _name;
// 錯(cuò)誤用法:export 123; 
export { _name, _desc, HandleDog, HandleCat };

模塊導(dǎo)出的同時(shí)牵舱,可以對(duì)導(dǎo)出的內(nèi)容進(jìn)行重命名

// ......
export {
  _name as name,
  _name as another_name,
  _desc as desc,
  HandleDog as handleDog,
  HandleCat as handleCat,
};

ESM 中還規(guī)定了一個(gè)「默認(rèn)導(dǎo)出」:

const HandleDefault = function() {/* ... */};

// export default 無需添加大括號(hào) {}
// 相當(dāng)于將 HandleDefault 賦值給 default 變量
export default HandleDefault;

對(duì)于模「非默認(rèn)輸出值」缺虐,我們需要使用如下方式導(dǎo)入:

// 想要導(dǎo)入的是模塊導(dǎo)出的 name 
// name 必須和 export 導(dǎo)出的名稱對(duì)應(yīng)
import { name } from '模塊';  // name 就是 export 時(shí)起的名字

而對(duì)于模塊的「默認(rèn)輸出值」芜壁,我們無需指定名稱即可獲得:

// 從模塊獲取值,由于沒有指定名稱高氮,所以獲取的是默認(rèn)輸出
// xxx 不與模塊導(dǎo)出值的名稱對(duì)應(yīng)慧妄,xxx 是給默認(rèn)輸出起的任意名稱
import xxx from '模塊';

模塊導(dǎo)入

ESM 使用 import 命令實(shí)現(xiàn)導(dǎo)入加載模塊:

import { name, another_name, handleDog, handleCat } from './utils.js';

// 如上所示,使用 import 導(dǎo)入時(shí)剪芍,括號(hào)內(nèi)的變量名需要和 utils 模塊導(dǎo)出的變量名相同
// 當(dāng)然也可以在導(dǎo)入的同時(shí)使用 as 起一個(gè)新名稱
import { name as old_name, another_name as new_name } from './utils.js';

也可以不按名稱一一導(dǎo)入塞淹,使用 * 符號(hào)將模塊導(dǎo)出的所有變量作為一個(gè)整體一次性導(dǎo)入到一個(gè)對(duì)象

import * as Utils from './utils.js';
console.log(Utils.name);
console.log(Utils.another_name);
Utils.handleDog();

對(duì)于「默認(rèn)輸出」,其對(duì)應(yīng)的變量名為 default罪裹,所以在導(dǎo)入時(shí)無需指定特定名稱饱普,還可以直接為其自定義一個(gè)任意名稱:

// 模塊內(nèi)導(dǎo)出的 HandleDefault 已經(jīng)被賦值為默認(rèn) default,所以導(dǎo)出的是 default 變量名
// 獲取模塊的默認(rèn) default 時(shí)無需通過模塊內(nèi)的定義特定名稱獲取
// 直接給這個(gè)默認(rèn) default 自定義一個(gè)任意名稱即可状共,例如 HandleOther
import HandleOther from './utils.js';

一點(diǎn)補(bǔ)充

ESM 規(guī)范中的 import 具有一些特性套耕,如導(dǎo)入的變量是只讀的import 具有提升效果峡继、比起 CommonJS冯袍、AMDimport 為編譯期執(zhí)行(語句分析階段) 等。同時(shí) ESM 中還有 importexport 混合使用等語法碾牌,關(guān)于這些更多內(nèi)容可以查閱 ES6~ES11 特性介紹之 ES6 篇 中的第 4 節(jié)「模塊 Module」康愤。

小結(jié)

在那個(gè) JavaScript 還在野蠻生長的「遠(yuǎn)古」時(shí)代,全局函數(shù)封裝小染、對(duì)象封裝以及立即函數(shù)執(zhí)行 IIFE 這些模塊化的「不完全體」陪伴著無數(shù)開發(fā)者度過了一段漫長的靜默期翘瓮。但隨著 JavaScript 的快速成長,無數(shù) JavaScript 開發(fā)人員在日益龐大和復(fù)雜的系統(tǒng)中磕磕碰碰裤翩,艱難前行资盅。

時(shí)間來到 2009调榄,CommonJS 社區(qū)建立,Node.js 更是橫空出世呵扛,從此開啟了 CommonJS每庆、AMDCMD今穿、UMD 百家爭鳴的時(shí)代缤灵,這一晃就是 6 個(gè)春秋,最終這一切隨著 2015 年 ES 官方規(guī)范的推出而落下帷幕蓝晒。

原本 ESM 規(guī)范推出后腮出,各個(gè)瀏覽器對(duì)此實(shí)現(xiàn)支持需要一定的開發(fā)時(shí)間。然而時(shí)代浪潮滾滾向前芝薇,這次不再給我們留下回味 AMD胚嘲、CMD 等規(guī)范的時(shí)間。

2014 年洛二,babel 誕生馋劈。這款能夠?qū)?「ECMAScript 2015+ 語法代碼」編譯成「運(yùn)行在舊平臺(tái)的舊語法代碼」的工具使得開發(fā)人員能夠使用還未被瀏覽器支持的最新語法進(jìn)行開發(fā)。甚至于將前端帶入了一個(gè)新的階段晾嘶,一個(gè)開發(fā)階段代碼與生產(chǎn)階段代碼不同的階段妓雾,即預(yù)編譯階段

同時(shí)隨著 webpack 的發(fā)明垒迂,前端領(lǐng)域后續(xù)又進(jìn)入全新的自動(dòng)化構(gòu)建階段械姻。這個(gè)過程中當(dāng)然還有 broswerifywebpack娇斑、grunt策添、gulp 等工具的發(fā)展和興衰,這又是另一個(gè)「前端故事」了毫缆。

如同 CommonJS唯竹、AMDCMD苦丁、UMD 一樣浸颓,前端領(lǐng)域每年都有新的技術(shù)不斷產(chǎn)生、更有新生力量不斷注入旺拉,也有舊的技術(shù)被淘汰产上,而這故事仍然在不斷上演。技術(shù)更替會(huì)不斷的被后人整理記錄蛾狗,但絕大多數(shù)的程序員恐怕只會(huì)被淹沒在歷史的浪潮中了吧晋涣。

參考資料

前端模塊化開發(fā)那點(diǎn)歷史
前端模塊化的前世
前端模塊化的十年征程
RequireJS 官方文檔
CMD 模塊定義規(guī)范
JavaScript for impatient programmers - 24 Modules

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沉桌,隨后出現(xiàn)的幾起案子谢鹊,更是在濱河造成了極大的恐慌算吩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佃扼,死亡現(xiàn)場離奇詭異偎巢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兼耀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門压昼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘤运,你說我怎么就攤上這事窍霞。” “怎么了尽超?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵官撼,是天一觀的道長。 經(jīng)常有香客問我似谁,道長,這世上最難降的妖魔是什么掠哥? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任巩踏,我火速辦了婚禮,結(jié)果婚禮上续搀,老公的妹妹穿的比我還像新娘塞琼。我一直安慰自己,他們只是感情好禁舷,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布彪杉。 她就那樣靜靜地躺著,像睡著了一般牵咙。 火紅的嫁衣襯著肌膚如雪派近。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天洁桌,我揣著相機(jī)與錄音渴丸,去河邊找鬼。 笑死另凌,一個(gè)胖子當(dāng)著我的面吹牛谱轨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吠谢,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼土童,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了工坊?” 一聲冷哼從身側(cè)響起献汗,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤错沃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后雀瓢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枢析,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年刃麸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了醒叁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泊业,死狀恐怖把沼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吁伺,我是刑警寧澤饮睬,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站篮奄,受9級(jí)特大地震影響捆愁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜窟却,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一昼丑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧夸赫,春花似錦菩帝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至切平,卻和暖如春握础,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揭绑。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工弓候, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人他匪。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓菇存,卻偏偏與公主長得像,于是被迫代替她去往敵國和親邦蜜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子依鸥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容