再嘮叨JS模塊化加載之CommonJS、AMD、CMD、ES6

Javascript模塊化編程,已經成為一個迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。

Javascript社區做了很多努力,在現有的運行環境中,實現”模塊”的效果。

CommonJS

CommonJS定義的模塊分為:? 模塊引用(require)? ? 模塊輸出(exports)? ? ? ?模塊標識(module)

CommonJS Modules有1.0、1.1、1.1.1三個版本:

Node.js、SproutCore實現了 Modules 1.0

SeaJS、AvocadoDB、CouchDB等實現了Modules 1.1.1

SeaJS、FlyScript實現了Modules/Wrappings

這里的CommonJS規范指的是CommonJS Modules/1.0規范。

CommonJS是一個更偏向于服務器端的規范。NodeJS采用了這個規范。CommonJS的一個模塊就是一個腳本文件。require命令第一次加載該腳本時就會執行整個腳本,然后在內存中生成一個對象。

{

??id:?'...',

??exports:?{?...?},

??loaded:?true,

??...

}

id是模塊名,exports是該模塊導出的接口,loaded表示模塊是否加載完畢。此外還有很多屬性,這里省略了。

以后需要用到這個模塊時,就會到exports屬性上取值。即使再次執行require命令,也不會再次執行該模塊,而是到緩存中取值。

//?math.js

exports.add?=?function(a,?b)?{

??return?a?+?b;

}

var?math?=?require('math');

math.add(2,?3);?//?512

由于CommonJS是同步加載模塊,這對于服務器端不是一個問題,因為所有的模塊都放在本地硬盤。等待模塊時間就是硬盤讀取文件時間,很小。但是,對于瀏覽器而言,它需要從服務器加載模塊,涉及到網速,代理等原因,一旦等待時間過長,瀏覽器處于”假死”狀態。

所以在瀏覽器端,不適合于CommonJS規范。所以在瀏覽器端又出現了一個規范—AMD(AMD是RequireJs在推廣過程中對模塊定義的規范化產出)。

AMD

CommonJS解決了模塊化的問題,但這種同步加載方式并不適合于瀏覽器端。

AMD是”Asynchronous Module Definition”的縮寫,即”異步模塊定義”。它采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。

這里異步指的是不堵塞瀏覽器其他任務(dom構建,css渲染等),而加載內部是同步的(加載完模塊后立即執行回調)。

AMD也采用require命令加載模塊,但是不同于CommonJS,它要求兩個參數:

require([module],?callback);1

第一個參數[module],是一個數組,里面的成員是要加載的模塊,callback是加載完成后的回調函數。如果將上述的代碼改成AMD方式:

require(['math'],?function(math)?{

??math.add(2,?3);

})

其中,回調函數中參數對應數組中的成員(模塊)。

requireJS加載模塊,采用的是AMD規范。也就是說,模塊必須按照AMD規定的方式來寫。

具體來說,就是模塊書寫必須使用特定的define()函數來定義。如果一個模塊不依賴其他模塊,那么可以直接寫在define()函數之中。

define(id?,?dependencies?,?factory);12

id:模塊的名字,如果沒有提供該參數,模塊的名字應該默認為模塊加載器請求的指定腳本的名字;

dependencies:模塊的依賴,已被模塊定義的模塊標識的數組字面量。依賴參數是可選的,如果忽略此參數,它應該默認為["require", "exports", "module"]。然而,如果工廠方法的長度屬性小于3,加載器會選擇以函數的長度屬性指定的參數個數調用工廠方法。

factory:模塊的工廠函數,模塊初始化要執行的函數或對象。如果為函數,它應該只被執行一次。如果是對象,此對象應該為模塊的輸出值。

假定現在有一個math.js文件,定義了一個math模塊。那么,math.js書寫方式如下:

//?math.js

define(function()?{

??var?add?=?function(x,?y)?{

????return?x?+?y;

??}

??return??{

????add:?add

??}

})

加載方法如下:

//?main.js

require(['math'],?function(math)?{

??alert(math.add(1,?1));

})

如果math模塊還依賴其他模塊,寫法如下:

//?math.js

define(['dependenceModule'],?function(dependenceModule)?{

????//?...

})

當require()函數加載math模塊的時候,就會先加載dependenceModule模塊。當有多個依賴時,就將所有的依賴都寫在define()函數第一個參數數組中,所以說AMD是依賴前置的。這不同于CMD規范,它是依賴就近的。

CMD

CMD推崇依賴就近,延遲執行。可以把你的依賴寫進代碼的任意一行,如下:

define(factory)

factory為函數時,表示是模塊的構造方法。執行該構造方法,可以得到模塊向外提供的接口。factory 方法在執行時,默認會傳入三個參數:require、exports 和 module.

//?CMD

define(function(require,?exports,?module)?{

??var?a?=?require('./a');

??a.doSomething();

??var?b?=?require('./b');

??b.doSomething();

})

如果使用AMD寫法,如下:

//?AMDdefine(['a',?'b'],?function(a,?b)?{

??a.doSomething();

??b.doSomething();

})

這個規范實際上是為了Seajs的推廣然后搞出來的。那么看看SeaJS是怎么回事兒吧,基本就是知道這個規范了。

同樣Seajs也是預加載依賴js跟AMD的規范在預加載這一點上是相同的,明顯不同的地方是調用,和聲明依賴的地方。AMD和CMD都是用difine和require,但是CMD標準傾向于在使用過程中提出依賴,就是不管代碼寫到哪突然發現需要依賴另一個模塊,那就在當前代碼用require引入就可以了,規范會幫你搞定預加載,你隨便寫就可以了。但是AMD標準讓你必須提前在頭部依賴參數部分寫好(沒有寫好? 倒回去寫好咯)。這就是最明顯的區別。

sea.js通過sea.use()來加載模塊。

seajs.use(id,?callback?)

ES6

es6模塊特性,推薦參看阮一峰老師的:ECMAScript 6 入門 - Module 的語法

說起 ES6 模塊特性,那么就先說說 ES6 模塊跟 CommonJS 模塊的不同之處。

ES6 模塊輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝

ES6 模塊編譯時執行,而 CommonJS 模塊總是在運行時加載

CommonJS 輸出值的拷貝

CommonJS 模塊輸出的是值的拷貝(原始值的拷貝),也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。

//?a.js

var?b?=?require('./b');

console.log(b.foo);

setTimeout(()?=>?{

??console.log(b.foo);

??console.log(require('./b').foo);

},?1000);

//?b.js

let?foo?=?1;

setTimeout(()?=>?{

??foo?=?2;

},?500);

module.exports?=?{

??foo:?foo,

};

//?執行:node?a.js

//?執行結果:

//?1

//?1

//?1

上面代碼說明,b 模塊加載以后,它的內部 foo 變化就影響不到輸出的 exports.foo 了。這是因為 foo 是一個原始類型的值,會被緩存。所以如果你想要在 CommonJS 中動態獲取模塊中的值,那么就需要借助于函數延時執行的特性。

//?a.js

var?b?=?require('./b');

console.log(b.foo());

setTimeout(()?=>?{

??console.log(b.foo());

??console.log(require('./b').foo());

},?1000);

//?b.js

let?foo?=?1;

setTimeout(()?=>?{

??foo?=?2;

},?500);

module.exports?=?{

??foo:?()?=>?{

????return?foo;

??},

};

//?執行:node?a.js

//?執行結果:

//?1

//?2

//?2

所以我們可以總結一下:

CommonJS 模塊重復引入的模塊并不會重復執行,再次獲取模塊直接獲得暴露的 module.exports 對象

如果你要處處獲取到模塊內的最新值的話,也可以你每次更新數據的時候每次都要去更新 module.exports 上的值

如果你暴露的 module.exports 的屬性是個對象,那就不存在這個問題了

所以如果你要處處獲取到模塊內的最新值的話,也可以你每次更新數據的時候每次都要去更新?module.exports 上的值,比如:

//?a.js

var?b?=?require('./b');

console.log(b.foo);

setTimeout(()?=>?{

??console.log(b.foo);

??console.log(require('./b').foo);

},?1000);

//?b.js

module.exports.foo?=?1;???//?同?exports.foo?=?1?

setTimeout(()?=>?{

??module.exports.foo?=?2;

},?500);

//?執行:node?a.js

//?執行結果:

//?1

//?2

//?2

ES6 輸出值的引用

然而在 ES6 模塊中就不再是生成輸出對象的拷貝,而是動態關聯模塊中的值。

ES6 靜態編譯,CommonJS 運行時加載

關于第二點,ES6 模塊編譯時執行會導致有以下兩個特點:

import 命令會被 JavaScript 引擎靜態分析,優先于模塊內的其他內容執行。

export 命令會有變量聲明提前的效果。

import 優先執行:?

從第一條來看,在文件中的任何位置引入 import 模塊都會被提前到文件頂部。

//?a.js

console.log('a.js')

import?{?foo?}?from?'./b';

//?b.js

export?let?foo?=?1;

console.log('b.js?先執行');

//?執行結果:

//?b.js?先執行

//?a.js

從執行結果我們可以很直觀地看出,雖然 a 模塊中 import 引入晚于 console.log('a'),但是它被 JS 引擎通過靜態分析,提到模塊執行的最前面,優于模塊中的其他部分的執行。

由于 import 是靜態執行,所以 import 具有提升效果即 import 命令在模塊中的位置并不影響程序的輸出。

/?a.js

import?{?foo?}?from?'./b';

console.log('a.js');

export?const?bar?=?1;

export?const?bar2?=?()?=>?{

??console.log('bar2');

}

export?function?bar3()?{

??console.log('bar3');

}

//?b.js

export?let?foo?=?1;

import?*?as?a?from?'./a';

console.log(a);

//?執行結果:

//?{?bar:?undefined,?bar2:?undefined,?bar3:?[Function:?bar3]?}

//?a.js

從上面的例子可以很直觀地看出,a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明的變量也是優于模塊其它內容的執行的,但是具體對變量賦值需要等到執行到相應代碼的時候。(當然函數聲明和表達式聲明不一樣,這一點跟 JS 函數性質一樣,這里就不過多解釋)

好了,講完了 ES6 模塊和 CommonJS 模塊的不同點之后,接下來就講講相同點:

模塊不會重復執行

這個很好理解,無論是 ES6 模塊還是 CommonJS 模塊,當你重復引入某個相同的模塊時,模塊只會執行一次。

CommonJS 模塊循環依賴

//?a.js

console.log('a?starting');

exports.done?=?false;

const?b?=?require('./b');

console.log('in?a,?b.done?=',?b.done);

exports.done?=?true;

console.log('a?done');

//?b.js

console.log('b?starting');

exports.done?=?false;

const?a?=?require('./a');

console.log('in?b,?a.done?=',?a.done);

exports.done?=?true;

console.log('b?done');

//?node?a.js

//?執行結果:

//?a?starting

//?b?starting

//?in?b,?a.done?=?false

//?b?done

//?in?a,?b.done?=?true

//?a?done

結合之前講的特性很好理解,當你從 b 中想引入 a 模塊的時候,因為 node 之前已經加載過 a 模塊了,所以它不會再去重復執行 a 模塊,而是直接去生成當前 a 模塊吐出的 module.exports 對象,因為 a 模塊引入 b 模塊先于給 done 重新賦值,所以當前 a 模塊中輸出的 module.exports 中 done 的值仍為 false。而當 a 模塊中輸出 b 模塊的 done 值的時候 b 模塊已經執行完畢,所以 b 模塊中的 done 值為 true。

從上面的執行過程中,我們可以看到,在 CommonJS 規范中,當遇到 require() 語句時,會執行 require 模塊中的代碼,并緩存執行的結果,當下次再次加載時不會重復執行,而是直接取緩存的結果。正因為此,出現循環依賴時才不會出現無限循環調用的情況。雖然這種模塊加載機制可以避免出現循環依賴時報錯的情況,但稍不注意就很可能使得代碼并不是像我們想象的那樣去執行。因此在寫代碼時還是需要仔細的規劃,以保證循環模塊的依賴能正確工作。

所以有什么辦法可以出現循環依賴的時候避免自己出現混亂呢?一種解決方式便是將每個模塊先寫 exports 語法,再寫 requre 語句,利用 CommonJS 的緩存機制,在 require() 其他模塊之前先把自身要導出的內容導出,這樣就能保證其他模塊在使用時可以取到正確的值。比如:

//?a.js

exports.done?=?true;

let?b?=?require('./b');

console.log(b.done)

//?b.js

exports.done?=?true;

let?a?=?require('./a');

console.log(a.done)

這種寫法簡單明了,缺點是要改變每個模塊的寫法,而且大部分同學都習慣了在文件開頭先寫 require 語句。

ES6 模塊循環依賴

跟 CommonJS 模塊一樣,ES6 不會再去執行重復加載的模塊,又由于 ES6 動態輸出綁定的特性,能保證 ES6 在任何時候都能獲取其它模塊當前的最新值。

//?a.js

console.log('a?starting')

import?{foo}?from?'./b';

console.log('in?b,?foo:',?foo);

export?const?bar?=?2;

console.log('a?done');

//?b.js

console.log('b?starting');

import?{bar}?from?'./a';

export?const?foo?=?'foo';

console.log('in?a,?bar:',?bar);

setTimeout(()?=>?{

??console.log('in?a,?setTimeout?bar:',?bar);

})

console.log('b?done');

//?babel-node?a.js

//?執行結果:

//?b?starting

//?in?a,?bar:?undefined

//?b?done

//?a?starting

//?in?b,?foo:?foo

//?a?done

//?in?a,?setTimeout?bar:?2

動態 import()

ES6 模塊在編譯時就會靜態分析,優先于模塊內的其他內容執行,所以導致了我們無法寫出像下面這樣的代碼:

if(some?condition)?{

??import?a?from?'./a';

}else?{

??import?b?from?'./b';

}

//?or?

import?a?from?(str?+?'b');

因為編譯時靜態分析,導致了我們無法在條件語句或者拼接字符串模塊,因為這些都是需要在運行時才能確定的結果在 ES6 模塊是不被允許的,所以 動態引入 import() 應運而生。

import() 允許你在運行時動態地引入 ES6 模塊,想到這,你可能也想起了 require.ensure 這個語法,但是它們的用途卻截然不同的。

require.ensure 的出現是 webpack 的產物,它是因為瀏覽器需要一種異步的機制可以用來異步加載模塊,從而減少初始的加載文件的體積,所以如果在服務端的話 require.ensure 就無用武之地了,因為服務端不存在異步加載模塊的情況,模塊同步進行加載就可以滿足使用場景了。 CommonJS 模塊可以在運行時確認模塊加載。

而 import() 則不同,它主要是為了解決 ES6 模塊無法在運行時確定模塊的引用關系,所以需要引入 import()

我們先來看下它的用法:

動態的 import() 提供一個基于 Promise 的 API

動態的import() 可以在腳本的任何地方使用

import() 接受字符串文字,你可以根據你的需要構造說明符

舉個簡單的使用例子:

//?a.js

const?str?=?'./b';

const?flag?=?true;

if(flag)?{

??import('./b').then(({foo})?=>?{

????console.log(foo);

??})

}

import(str).then(({foo})?=>?{

??console.log(foo);

})

//?b.js

export?const?foo?=?'foo';

//?babel-node?a.js

//?執行結果

//?foo

//?foo

當然,如果在瀏覽器端的 import() 的用途就會變得更廣泛,比如 按需異步加載模塊,那么就和 require.ensure 功能類似了。

因為是基于 Promise 的,所以如果你想要同時加載多個模塊的話,可以是 Promise.all 進行并行異步加載。

Promise.all([

??import('./a.js'),

??import('./b.js'),

??import('./c.js'),

]).then(([a,?{default:?b},?{c}])?=>?{

????console.log('a.js?is?loaded?dynamically');

????console.log('b.js?is?loaded?dynamically');

????console.log('c.js?is?loaded?dynamically');

});

還有 Promise.race 方法,它檢查哪個 Promise 被首先 resolved 或 reject。我們可以使用import()來檢查哪個CDN速度更快:

const?CDNs?=?[

??{

????name:?'jQuery.com',

????url:?'https://code.jquery.com/jquery-3.1.1.min.js'

??},

??{

????name:?'googleapis.com',

????url:?'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'

??}

];

console.log(`------`);

console.log(`jQuery?is:?${window.jQuery}`);

Promise.race([

??import(CDNs[0].url).then(()=>console.log(CDNs[0].name,?'loaded')),

??import(CDNs[1].url).then(()=>console.log(CDNs[1].name,?'loaded'))

]).then(()=>?{

??console.log(`jQuery?version:?${window.jQuery.fn.jquery}`);

});

當然,如果你覺得這樣寫還不夠優雅,也可以結合 async/await 語法糖來使用。

async?function?main()?{

??const?myModule?=?await?import('./myModule.js');

??const?{export1,?export2}?=?await?import('./myModule.js');

??const?[module1,?module2,?module3]?=

????await?Promise.all([

??????import('./module1.js'),

??????import('./module2.js'),

??????import('./module3.js'),

????]);

}

動態 import() 為我們提供了以異步方式使用 ES 模塊的額外功能。 根據我們的需求動態或有條件地加載它們,這使我們能夠更快,更好地創建更多優勢應用程序。

export

一個模塊就是一個獨立的文件。該文件內部的所有變量,外部無法獲取。如果希望外部文件能夠讀取該模塊的變量,就需要在這個模塊內使用export關鍵字導出變量。如:

//?profile.jsexport?var?a?=?1;export?var?b?=?2;export?var?c?=?3;1234

下面的寫法是等價的,這種方式更加清晰(在底部一眼能看出導出了哪些變量):

var?a?=?1;var?b?=?2;var?c?=?3;

export?{a,?b,?c}1234

import

import命令可以導入其他模塊通過export導出的部分。

var?a?=?1;var?b?=?2;var?c?=?3;

export?{a,?b,?c}

//main.js

import?{a,?b,?c}?from?'./abc';

console.log(a,?b,?c);

如果想為導入的變量重新取一個名字,使用as關鍵字(也可以在導出中使用)。

import?{a?as?aa,?b,?c};

console.log(aa,?b,?c)12

如果想在一個模塊中先輸入后輸出一個模塊,import語句可以和export語句寫在一起。

import?{a,?b,?c}?form?'./abc';export?{a,?b,??c}//?使用連寫,?可讀性不好,不建議export?{a,?b,?c}?from?'./abc';12345

模塊的整體加載

使用*關鍵字。

import?*?from?as?abc?form?'./abc';

export default

在export輸出內容時,如果同時輸出多個變量,需要使用大括號{},同時導入也需要大括號。使用export defalut輸出時,不需要大括號,而輸入(import)export default輸出的變量時,不需要大括號。

//?abc.jsvar?a?=?1,?b?=?2,?c?=?3;export?{a,?b};export?default?c;1234

import?{a,?b}?from?'./abc';

import?c?from?'./abc';?//?不需要大括號console.log(a,?b,?c)?//?1?2?3123

本質上,export default輸出的是一個叫做default的變量或方法,輸入這個default變量時不需要大括號。

//?abc.js

export?{a?as?default};

//?main.js

import?a?from?'./abc';?//?這樣也是可以的

import?{default?as?aa}?from?'./abc';?//?這樣也是可以的

console.log(aa);123456789

就到這里了吧。關于循環加載(模塊相互依賴)沒寫,CommonJS和ES6處理方式不一樣。

參考文章:

javascript模塊化之CommonJS、AMD、CMD、UMD、ES6

深入理解 ES6 模塊機制

該如何理解AMD ,CMD,CommonJS規范–javascript模塊化加載學習總結

AMD/CMD與前端規范

前端模塊化之旅(二):CommonJS、AMD和CMD

研究一下javascript的模塊規范(CommonJs/AMD/CMD)

Javascript模塊化編程(一):模塊的寫法

Javascript模塊化編程(二):AMD規范

Javascript模塊化編程(三):require.js的用法

Module

轉載請注明來源,再嘮叨JS模塊化加載之CommonJS、AMD、CMD、ES6 - javascript入門容易深入難,民工看似康莊大道卻是陷阱遍地 - 周陸軍的個人網站https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2016_0203_528.html

文有不妥之處,望告之,謝謝

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容