果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 這樣的術(shù)語,很快讓你不堪重負(fù)。
JavaScript 模塊系統(tǒng)可能令人生畏,但理解它對 Web 開發(fā)人員至關(guān)重要。
在這篇文章中,我將以簡單的言語(以及一些代碼示例)為你解釋這些術(shù)語。 希望這對你有會有幫助!
好作者能將他們的書分成章節(jié),優(yōu)秀的程序員將他們的程序劃分為模塊。
就像書中的章節(jié)一樣,模塊只是文字片段(或代碼,視情況而定)的集群。然而,好的模塊是高內(nèi)聚低松耦的,具有不同的功能,允許在必要時對它們進行替換、刪除或添加,而不會擾亂整體功能。
使用模塊有利于擴展、相互依賴的代碼庫,這有很多好處。在我看來,最重要的是:
1)可維護性: 根據(jù)定義,模塊是高內(nèi)聚的。一個設(shè)計良好的模塊旨在盡可能減少對代碼庫部分的依賴,這樣它就可以獨立地增強和改進,當(dāng)模塊與其他代碼片段解耦時,更新單個模塊要容易得多。
回到我們的書的例子,如果你想要更新你書中的一個章節(jié),如果對一個章節(jié)的小改動需要你調(diào)整每一個章節(jié),那將是一場噩夢。相反,你希望以這樣一種方式編寫每一章,即可以在不影響其他章節(jié)的情況下進行改進。
2)命名空間: 在 JavaScript 中,頂級函數(shù)范圍之外的變量是全局的(這意味著每個人都可以訪問它們)。因此,“名稱空間污染”很常見,完全不相關(guān)的代碼共享全局變量。
在不相關(guān)的代碼之間共享全局變量在開發(fā)中是一個大禁忌。正如我們將在本文后面看到的,通過為變量創(chuàng)建私有空間,模塊允許我們避免名稱空間污染。
3)可重用性:坦白地說:我們將前寫過的代碼復(fù)制到新項目中。 例如,假設(shè)你從之前項目編寫的一些實用程序方法復(fù)制到當(dāng)前項目中。
這一切都很好,但如果你找到一個更好的方法來編寫代碼的某些部分,那么你必須記得回去在曾經(jīng)使用過的其他項目更新它。
這顯然是在浪費時間。如果有一個我們可以一遍又一遍地重復(fù)使用的模塊,不是更容易嗎?
有多種方法來創(chuàng)建模塊,來看幾個:
模塊模式用于模擬類的概念(因為 JavaScript 本身不支持類),因此我們可以在單個對象中存儲公共和私有方法和變量——類似于在 Java 或 Python 等其他編程語言中使用類的方式。這允許我們?yōu)橄胍_的方法創(chuàng)建一個面向公共的 API,同時仍然將私有變量和方法封裝在閉包范圍中。
有幾種方法可以實現(xiàn)模塊模式。在第一個示例中,將使用匿名閉包,將所有代碼放在匿名函數(shù)中來幫助我們實現(xiàn)目標(biāo)。(記住:在 JavaScript 中,函數(shù)是創(chuàng)建新作用域的唯一方法。)
例一:匿名閉包
(function () {
// 將這些變量放在閉包范圍內(nèi)實現(xiàn)私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return '平均分 ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return '掛機科了 ' + failingGrades.length + ' 次。';
}
console.log(failing()); // 掛機科了次
}());
使用這個結(jié)構(gòu),匿名函數(shù)就有了自己的執(zhí)行環(huán)境或“閉包”,然后我們立即執(zhí)行。這讓我們可以從父(全局)命名空間隱藏變量。
這種方法的優(yōu)點是,你可以在這個函數(shù)中使用局部變量,而不會意外地覆蓋現(xiàn)有的全局變量,但仍然可以訪問全局變量,就像這樣:
var global = '你好,我是一個全局變量。)';
(function () {
// 將這些變量放在閉包范圍內(nèi)實現(xiàn)私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return '平均分 ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return '掛機科了 ' + failingGrades.length + ' 次。';
}
console.log(failing()); // 掛機科了次
onsole.log(global); // 你好,我是一個全局變量。
}());
注意,匿名函數(shù)的圓括號是必需的,因為以關(guān)鍵字 function 開頭的語句通常被認(rèn)為是函數(shù)聲明(請記住,JavaScript 中不能使用未命名的函數(shù)聲明)。因此,周圍的括號將創(chuàng)建一個函數(shù)表達式,并立即執(zhí)行這個函數(shù),這還有另一種叫法 立即執(zhí)行函數(shù)(IIFE)。如果你對這感興趣,可以在這里了解到更多。
例二:全局導(dǎo)入
jQuery 等庫使用的另一種流行方法是全局導(dǎo)入。它類似于我們剛才看到的匿名閉包,只是現(xiàn)在我們作為參數(shù)傳入全局變量:
(function (globalVariable) {
// 在這個閉包范圍內(nèi)保持變量的私有化
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// 通過 globalVariable 接口公開下面的方法
// 同時將方法的實現(xiàn)隱藏在 function() 塊中
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
在這個例子中,globalVariable 是唯一的全局變量。與匿名閉包相比,這種方法的好處是可以預(yù)先聲明全局變量,使得別人更容易閱讀代碼。
例三:對象接口
另一種方法是使用立即執(zhí)行函數(shù)接口對象創(chuàng)建模塊,如下所示:
var myGradesCalculate = (function () {
// 將這些變量放在閉包范圍內(nèi)實現(xiàn)私有化
var myGrades = [93, 95, 88, 0, 55, 91];
// 通過接口公開這些函數(shù),同時將模塊的實現(xiàn)隱藏在function()塊中
return {
average: function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'平均分 ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return '掛科了' + failingGrades.length + ' 次.';
}
}
})();
myGradesCalculate.failing(); // '掛科了 2 次.'
myGradesCalculate.average(); // '平均分 70.33333333333333.'
正如您所看到的,這種方法允許我們通過將它們放在 return 語句中(例如算平均分和掛科數(shù)方法)來決定我們想要保留的變量/方法(例如 myGrades)以及我們想要公開的變量/方法。
例四:顯式模塊模式
這與上面的方法非常相似,只是它確保所有方法和變量在顯式公開之前都是私有的:
var myGradesCalculate = (function () {
// 將這些變量放在閉包范圍內(nèi)實現(xiàn)私有化
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'平均分 ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return '掛科了' + failingGrades.length + ' 次.';
};
// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // '掛科了 2 次.'
myGradesCalculate.average(); // '平均分 70.33333333333333.'
這可能看起來很多,但它只是模塊模式的冰山一角。 以下是我在自己的探索中發(fā)現(xiàn)有用的一些資源:
所有這些方法都有一個共同點:使用單個全局變量將其代碼包裝在函數(shù)中,從而使用閉包作用域為自己創(chuàng)建一個私有名稱空間。
雖然每種方法都有效且都有各自特點,但卻都有缺點。
首先,作為開發(fā)人員,你需要知道加載文件的正確依賴順序。例如,假設(shè)你在項目中使用 Backbone,因此你可以將 Backbone 的源代碼 以<script> 腳本標(biāo)簽的形式引入到文件中。
但是,由于 Backbone 對 Underscore.js 有很強的依賴性,因此 Backbone 文件的腳本標(biāo)記不能放在Underscore.js 文件之前。
作為一名開發(fā)人員,管理依賴關(guān)系并正確處理這些事情有時會令人頭痛。
另一個缺點是它們?nèi)匀粫?dǎo)致名稱空間沖突。例如,如果兩個模塊具有相同的名稱怎么辦?或者,如果有一個模塊的兩個版本,并且兩者都需要,該怎么辦?
幸運的是,答案是肯定的。
有兩種流行且實用的方法:CommonJS 和 AMD。
CommonJS 是一個志愿者工作組,負(fù)責(zé)設(shè)計和實現(xiàn)用于聲明模塊的 JavaScript API。
CommonJS 模塊本質(zhì)上是一個可重用的 JavaScript,它導(dǎo)出特定的對象,使其可供其程序中需要的其他模塊使用。 如果你已經(jīng)使用 Node.js 編程,那么你應(yīng)該非常熟悉這種格式。
使用 CommonJS,每個 JavaScript 文件都將模塊存儲在自己獨立的模塊上下文中(就像將其封裝在閉包中一樣)。 在此范圍內(nèi),我們使用 module.exports 導(dǎo)出模塊,或使用 require 來導(dǎo)入模塊。
在定義 CommonJS 模塊時,它可能是這樣的:
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
我們使用特殊的對象模塊,并將函數(shù)的引用放入 module.exports 中。這讓 CommonJS 模塊系統(tǒng)知道我們想要公開什么,以便其他文件可以使用它。
如果想使用 myModule,只需要使用 require 方法就可以,如下:
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
與前面討論的模塊模式相比,這種方法有兩個明顯的好處:
另外需要注意的是,CommonJS 采用服務(wù)器優(yōu)先方法并同步加載模塊。 這很重要,因為如果我們需要三個其他模塊,它將逐個加載它們。
現(xiàn)在,它在服務(wù)器上運行良好,但遺憾的是,在為瀏覽器編寫 JavaScript 時使用起來更加困難。 可以這么說,從網(wǎng)上讀取模塊比從磁盤讀取需要更長的時間。 只要加載模塊的腳本正在運行,它就會阻止瀏覽器運行其他任何內(nèi)容,直到完成加載,這是因為 JavaScript 是單線程且 CommonJS 是同步加載的。
CommonJS一切都很好,但是如果我們想要異步加載模塊呢? 答案是 異步模塊定義,簡稱 AMD。
使用 AMD 的加載模塊如下:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
define 函數(shù)的第一個參數(shù)是一個數(shù)組,數(shù)組中是依賴的各種模塊。這些依賴模塊在后臺(以非阻塞的方式)加載進來,一旦加載完畢,define 函數(shù)就會調(diào)用第二個參數(shù),即回調(diào)函數(shù)執(zhí)行操作。
接下來,回調(diào)函數(shù)接收參數(shù),即依賴模塊 - 示例中就是 myModule 和 myOtherModule - 允許函數(shù)使用這些依賴項, 最后,所依賴的模塊本身也必須使用 define 關(guān)鍵字來定義。例如,myModule如下所示:
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
因此,與 CommonJS 不同,AMD 采用瀏覽器優(yōu)先的方法和異步行為來完成工作。 (注意,有很多人堅信在開始運行代碼時動態(tài)加載文件是不利的,我們將在下一節(jié)關(guān)于模塊構(gòu)建的內(nèi)容中探討更多內(nèi)容)。
除了異步性,AMD 的另一個好處是模塊可以是對象,函數(shù),構(gòu)造函數(shù),字符串,JSON 和許多其他類型,而CommonJS 只支持對象作為模塊。
也就是說,和CommonJS相比,AMD不兼容io、文件系統(tǒng)或者其他服務(wù)器端的功能特性,而且函數(shù)包裝語法與簡單的require 語句相比有點冗長。
對于同時支持 AMD 和 CommonJS 特性的項目,還有另一種格式:通用模塊定義(Universal Module Definition, UMD)。
UMD 本質(zhì)上創(chuàng)造了一種使用兩者之一的方法,同時也支持全局變量定義。因此,UMD 模塊能夠同時在客戶端和服務(wù)端同時工作。
簡單看一下 UMD 是怎樣工作的:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));
Github 上 enlightening repo 里有更多關(guān)于 UMD 的例子。
你可能已經(jīng)注意到,上面的模塊都不是 JavaScript 原生的。相反,我們已經(jīng)創(chuàng)建了通過使用模塊模式、CommonJS 或 AMD 來模擬模塊系統(tǒng)的方法。
幸運的是,TC39(定義 ECMAScript 的語法和語義的標(biāo)準(zhǔn)組織)一幫聰明的人已經(jīng)引入了ECMAScript 6(ES6)的內(nèi)置模塊。
ES6 為導(dǎo)入導(dǎo)出模塊提供了很多不同的可能性,已經(jīng)有許多其他人花時間解釋這些,下面是一些有用的資源:
與 CommonJS 或 AMD 相比,ES6 模塊最大的優(yōu)點在于它能夠同時提供兩方面的優(yōu)勢:簡明的聲明式語法和異步加載,以及對循環(huán)依賴項的更好支持。
也許我個人最喜歡的 ES6 模塊功能是它的導(dǎo)入模塊是導(dǎo)出時模塊的實時只讀視圖。(相比起 CommonJS,導(dǎo)入的是導(dǎo)出模塊的拷貝副本,因此也不是實時的)。
下面是一個例子:
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1
在這個例子中,我們基本上創(chuàng)建了兩個模塊的對象:一個用于導(dǎo)出它,一個在我們需要的時候引入。
此外,在 main.js 中的對象目前是與原始模塊是相互獨立的,這就是為什么即使我們執(zhí)行 increment 方法,它仍然返回 1,因為引入的變量和最初導(dǎo)入的變量是毫無關(guān)聯(lián)的。需要改變你引入的對象唯一的方式是手動執(zhí)行增加:
counter.counter++;
console.log(counter.counter); // 2
另一方面,ES6創(chuàng)建了我們導(dǎo)入的模塊的實時只讀視圖:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
超酷?我發(fā)現(xiàn)這一點是因為ES6允許你可以把你定義的模塊拆分成更小的模塊而不用刪減功能,然后你還能反過來把它們合成到一起, 完全沒問題。
總體上看,模塊打包只是將一組模塊(及其依賴項)以正確的順序拼接到一個文件(或一組文件)中的過程。正如 Web開發(fā)的其它方方面面,棘手的問題總是潛藏在具體的細(xì)節(jié)里。
將程序劃分為模塊時,通常會將這些模塊組織到不同的文件和文件夾中。 有可能,你還有一組用于正在使用的庫的模塊,如 Underscore 或 React。
因此,每個文件都必須以一個 <script> 標(biāo)簽引入到主 HTML 文件中,然后當(dāng)用戶訪問你的主頁時由瀏覽器加載進來。 每個文件使用 <script> 標(biāo)簽引入,意味著瀏覽器不得不分別逐個的加載它們。
這對于頁面加載時間來說簡直是噩夢。
為了解決這個問題,我們將所有文件打包或“拼接”到一個大文件(或視情況而定的幾個文件),以減少請求的數(shù)量。 當(dāng)你聽到開發(fā)人員談?wù)摗皹?gòu)建步驟”或“構(gòu)建過程”時,這就是他們所談?wù)摰膬?nèi)容。
另一種加速構(gòu)建操作的常用方法是“縮減”打包代碼。 縮減是從源代碼中移除不必要的字符(例如,空格,注釋,換行符等)的過程,以便在不改變代碼功能的情況下減少內(nèi)容的整體大小。
較少的數(shù)據(jù)意味著瀏覽器處理時間會更快,從而減少了下載文件所需的時間。 如果你見過具有 “min” 擴展名的文件,如 “underscore-min.js” ,可能會注意到與完整版相比,縮小版本非常小(不過很難閱讀)。
除了捆綁和/或加載模塊之外,模塊捆綁器還提供了許多其他功能,例如在進行更改時生成自動重新編譯代碼或生成用于調(diào)試的源映射。
構(gòu)建工具(如 Gulp 和 Grunt)能為開發(fā)者直接進行拼接和縮減,確保為開發(fā)人員提供可讀代碼,同時有利于瀏覽器執(zhí)行的代碼。
當(dāng)你使用一種標(biāo)準(zhǔn)模塊模式(上部分討論過)來定義模塊時,拼接和縮減文件非常有用。 你真正在做的就是將一堆普通的 JavaScript 代碼捆綁在一起。
但是,如果你堅持使用瀏覽器無法解析的非原生模塊系統(tǒng)(如 CommonJS 或 AMD(甚至是原生 ES6模塊格式)),則需要使用專門工具將模塊轉(zhuǎn)換為排列正確、瀏覽器可解析的代碼。 這就是 Browserify,RequireJS,Webpack 和其他“模塊打包工具”或“模塊加載工具”的用武之地。
除了打包和/或加載模塊之外,模塊打包器還提供了許多其他功能,例如在進行更改時生成自動重新編譯代碼或生成用于調(diào)試的源映射。
下面是一些常見的模塊打包方法:
正如前面所知道的,CommonJS以同步方式加載模塊,這沒有什么問題,只是它對瀏覽器不實用。我提到過有一個解決方案——其中一個是一個名為 Browserify 的模塊打包工具。Browserify 是一個為瀏覽器編譯 CommonJS模塊的工具。
例如,有個 main.js 文件,它導(dǎo)入一個模塊來計算一組數(shù)字的平均值:
var myDependency = require(‘myDependency’);
var myGrades = [93, 95, 88, 0, 91];
var myAverageGrade = myDependency.average(myGrades);
在這種情況下,我們有一個依賴項(myDependency),使用下面的命令,Browserify 以 main.js 為入口把所有依賴的模塊遞歸打包成一個文件:
browserify main.js -o bundle.js
Browserify 通過跳入文件分析每一個依賴的 抽象語法樹(AST),以便遍歷項目的整個依賴關(guān)系圖。一旦確定了依賴項的結(jié)構(gòu),就把它們按正確的順序打包到一個文件中。然后,在 html 里插入一個用于引入 “bundle.js” 的 <script> 標(biāo)簽,從而確保你的源代碼在一個 HTTP 請求中完成下載。
類似地,如果有多個文件且有多個依賴時,只需告訴 Browserify 的入口文件路徑即可。最后打包后的文件可以通過 Minify-JS 之類的工具壓縮打包后的代碼。
如果你正在使用 AMD,你需要使用像 RequireJS 或者 Curl 這樣的 AMD 加載器。模塊加載器(與模塊打包工具不同)會動態(tài)加載程序需要運行的模塊。
提醒一下,AMD 與 CommonJS 的主要區(qū)別之一是它以異步方式加載模塊。 從這個意義上說,對于 AMD,從技術(shù)上講,實際上并不需要構(gòu)建步驟,因為異步加載模塊意味著在運行過程中逐步下載那些程序所需要的文件,而不是用戶剛進入頁面就一下把所有文件都下載下來。
但實際上,對于每個用戶操作而言,隨著時間的推移,大容量請求的開銷在生產(chǎn)中沒有多大意義。 大多數(shù) Web 開發(fā)人員仍然使用構(gòu)建工具打包和壓縮 AMD 模塊以獲得最佳性能,例如使用 RequireJS 優(yōu)化器,r.js 等工具。
總的來說,AMD 和 CommonJS 在打包方面的區(qū)別在于:在開發(fā)期間,AMD 可以省去任何構(gòu)建過程。當(dāng)然,在代碼上線前,要使用優(yōu)化工具(如 r.js)進行優(yōu)化。
就打包工具而言,Webpack 是一個新事物。它被設(shè)計成與你使用的模塊系統(tǒng)無關(guān),允許開發(fā)人員在適當(dāng)?shù)那闆r下使用 CommonJS、AMD 或 ES6。
你可能想知道,為什么我們需要 Webpack,而我們已經(jīng)有了其他打包工具了,比如 Browserify 和 RequireJS,它們可以完成工作,并且做得非常好。首先,Webpack 提供了一些有用的特性,比如 “代碼分割”(code splitting) —— 一種將代碼庫分割為“塊(chunks)”的方式,從而能實現(xiàn)按需加載。
例如,如果你的 Web 應(yīng)用程序,其中只需要某些代碼,那么將整個代碼庫都打包進一個大文件就不是很高效。 在這種情況下,可以使用代碼分割,將需要的部分代碼抽離在"打包塊",在執(zhí)行按需加載,從而避免在最開始就遇到大量負(fù)載的麻煩。
代碼分割只是 Webpack 提供的眾多引人注目的特性之一,網(wǎng)上有很多關(guān)于 “Webpack 與 Browserify 誰更好” 的激烈討論。以下是一些客觀冷靜的討論,幫助我稍微理清了頭緒:
當(dāng)前 JS 模塊規(guī)范(CommonJS, AMD) 與 ES6 模塊之間最重要的區(qū)別是 ES6 模塊的設(shè)計考慮到了靜態(tài)分析。這意味著當(dāng)你導(dǎo)入模塊時,導(dǎo)入的模塊在編譯階段也就是代碼開始運行之前就被解析了。這允許我們在運行程序之前移,移除那些在導(dǎo)出模塊中不被其它模塊使用的部分。移除不被使用的模塊能節(jié)省空間,且有效地減少瀏覽器的壓力。
一個常見的問題,使用一些工具,如 Uglify.js ,縮減代碼時,有一個死碼刪除的處理,它和 ES6 移除沒用的模塊又有什么不同呢?只能說 “視情況而定”。
死碼消除(Dead codeelimination)是一種編譯器原理中編譯最優(yōu)化技術(shù),它的用途是移除對程序運行結(jié)果沒有任何影響的代碼。移除這類的代碼有兩種優(yōu)點,不但可以減少程序的大小,還可以避免程序在運行中進行不相關(guān)的運算行為,減少它運行的時間。不會被運行到的代碼(unreachable code)以及只會影響到無關(guān)程序運行結(jié)果的變量(Dead Variables),都是死碼(Dead code)的范疇。
有時,在 UglifyJS 和 ES6 模塊之間死碼消除的工作方式完全相同,有時則不然。如果你想驗證一下, Rollup’s wiki 里有個很好的示例。
ES6 模塊的不同之處在于死碼消除的不同方法,稱為“tree shaking”。“tree shaking” 本質(zhì)上是死碼消除反過程。它只包含包需要運行的代碼,而非排除不需要的代碼。來看個例子:
假設(shè)有一個帶有多個函數(shù)的 utils.js 文件,每個函數(shù)都用 ES6 的語法導(dǎo)出:
export function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
}
export function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
}
export function map(collection, iterator) {
var mapped = [];
each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
}
export function reduce(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
}
接著,假設(shè)我們不知道要在程序中使用什么 utils.js 中的哪個函數(shù),所以我們將上述的所有模塊導(dǎo)入main.js中,如下所示:
import * as Utils from ‘./utils.js’;
最終,我們只用到的 each 方法:
import * as Utils from ‘./utils.js’;
Utils.each([1, 2, 3], function(x) { console.log(x) });
“tree shaken” 版本的 main.js 看起來如下(一旦模塊被加載后):
function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
each([1, 2, 3], function(x) { console.log(x) });
注意:只導(dǎo)出我們使用的 each 函數(shù)。
同時,如果決定使用 filte r函數(shù)而不是每個函數(shù),最終會看到如下的結(jié)果:
import * as Utils from ‘./utils.js’;
Utils.filter([1, 2, 3], function(x) { return x === 2 });
tree shaken 版本如下:
function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
filter([1, 2, 3], function(x) { return x === 2 });
此時,each 和 filter 函數(shù)都被包含進來。這是因為 filter 在定義時使用了 each。因此也需要導(dǎo)出該函數(shù)模塊以保證程序正常運行。
我們知道 ES6 模塊的加載方式與其他模塊格式不同,但我們?nèi)匀粵]有討論使用 ES6 模塊時的構(gòu)建步驟。
遺憾的是,因為瀏覽器對 ES6模 塊的原生支持還不夠完善,所以現(xiàn)階段還需要我們做一些補充工作。
下面是幾個在瀏覽器中 構(gòu)建/轉(zhuǎn)換 ES6 模塊的方法,其中第一個是目前最常用的方法:
作為 web 開發(fā)人員,我們必須經(jīng)歷很多困難。轉(zhuǎn)換語法優(yōu)雅的ES6代碼以便在瀏覽器里運行并不總是容易的。
問題是,什么時候 ES6 模塊可以在瀏覽器中運行而不需要這些開銷?
答案是:“盡快”。
ECMAScript 目前有一個解決方案的規(guī)范,稱為 ECMAScript 6 module loader API。簡而言之,這是一個綱領(lǐng)性的、基于 Promise 的 API,它支持動態(tài)加載模塊并緩存它們,以便后續(xù)導(dǎo)入不會重新加載模塊的新版本。
它看起來如下:
// myModule.js
export class myModule {
constructor() {
console.log('Hello, I am a module');
}
hello() {
console.log('hello!');
}
goodbye() {
console.log('goodbye!');
}
}
// main.js
System.import(‘myModule’).then(function(myModule) {
new myModule.hello();
});
// ‘hello!’
你亦可直接對 script 標(biāo)簽指定 “type=module” 來定義模塊,如:
<script type="module">
// loads the 'myModule' export from 'mymodule.js'
import { hello } from 'mymodule';
new Hello(); // 'Hello, I am a module!'
</script>
更加詳細(xì)的介紹也可以在 Github 上查看:es-module-loader
此外,如果您想測試這種方法,請查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在瀏覽器和 Node 中動態(tài)加載任何模塊格式(ES6模塊,AMD,CommonJS 或 全局腳本)。
它跟蹤“模塊注冊表”中所有已加載的模塊,以避免重新加載先前已加載過的模塊。 更不用說它還會自動轉(zhuǎn)換ES6模塊(如果只是設(shè)置一個選項)并且能夠從任何其他類型加載任何模塊類型!
對于日益普及的 ES6 模塊,下面有一些有趣的觀點:
對于 HTTP/1,每個TCP連接只允許一個請求。這就是為什么加載多個資源需要多個請求。有了 HTTP/2,一切都變了。HTTP/2 是完全多路復(fù)用的,這意味著多個請求和響應(yīng)可以并行發(fā)生。因此,我們可以在一個連接上同時處理多個請求。
由于每個 HTTP 請求的成本明顯低于HTTP/1,因此從長遠來看,加載一組模塊不會造成很大的性能問題。一些人認(rèn)為這意味著模塊打包不再是必要的,這當(dāng)然有可能,但這要具體情況具體分析了。
例如,模塊打包還有 HTTP/2 沒有好處,比如移除冗余的導(dǎo)出模塊以節(jié)省空間。 如果你正在構(gòu)建一個性能至關(guān)重要的網(wǎng)站,那么從長遠來看,打包可能會為你帶來增量優(yōu)勢。 也就是說,如果你的性能需求不是那么極端,那么通過完全跳過構(gòu)建步驟,可以以最小的成本節(jié)省時間。
總的來說,絕大多數(shù)網(wǎng)站都用上 HTTP/2 的那個時候離我們現(xiàn)在還很遠。我預(yù)測構(gòu)建過程將會保留,至少在近期內(nèi)。
一旦 ES6 成為模塊標(biāo)準(zhǔn),我們還需要其他非原生模塊規(guī)范嗎?
我覺得還有。
Web 開發(fā)遵守一個標(biāo)準(zhǔn)方法進行導(dǎo)入和導(dǎo)出模塊,而不需要中間構(gòu)建步驟——網(wǎng)頁開發(fā)長期受益于此。但 ES6 成為模塊規(guī)范需要多長時間呢?
機會是有,但得等一段時間 。
再者,眾口難調(diào),所以“一個標(biāo)準(zhǔn)的方法”可能永遠不會成為現(xiàn)實。
希望這篇文章能幫你理清一些開發(fā)者口中的模塊和模塊打包的相關(guān)概念,共進步。
ure是一個輕量級框架。這類框架的作用就是通過給相應(yīng)元素添加預(yù)設(shè)好的class,來快速的實現(xiàn)預(yù)設(shè)效果。
pure只有短短數(shù)千行代碼,但是可控拓展,非常實用,對于新手來說,pure是css框架入門的一個比較好的選擇。在后期做項目時也可能用到類似于boostrap這樣的大型框架。所以前期對于輕量級框架源碼的理解是很有幫助的。
Pure 小得不要不要的,壓縮成gzip文件僅為3.7KB*。力求每一行代碼都精簡到極致,以便最大程度壓縮CSS大小,更利于移動Web制作。如果您僅使用其中的一部分模塊,那CSS真是小到?jīng)]有朋友了!
將 Pure 引入你的頁面,你可以借助 free unpkg CDN 添加 Pure ,而無需下載到本地。將下面的<link>內(nèi)容直接復(fù)制添加到頁面的<head>部分即可。
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css" integrity="sha384-CCTZv2q9I9m3UOxRLaJneXrrqKwUNOzZ6NGEUMwHtShDJ+nCoiXJCAgi05KfkLGY" crossorigin="anonymous">
添加 Viewport Meta 元素
Viewportmeta元素控制移動端瀏覽器的寬度和縮放。為了自適應(yīng)設(shè)備的寬度,請將下面一行加入<head>中。
<meta name="viewport" content="width=device-width, initial-scale=1">
Pure的柵格系統(tǒng)非常簡單。你可以使用.pure-g創(chuàng)建行, 使用pure-u-*創(chuàng)建列。
下面是1行3列的柵格:
<div class="pure-g">
<div class="pure-u-1-3"><p>Thirds</p></div>
<div class="pure-u-1-3"><p>Thirds</p></div>
<div class="pure-u-1-3"><p>Thirds</p></div>
</div>
Pure的柵格系統(tǒng)是移動設(shè)備優(yōu)先和響應(yīng)式的, 你也可以自定義CSS媒體查詢和柵格的class名,我們先來看個普通使用的例子。
首先引入grids-responsive.css:
<!--[if lte IE 8]>
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-old-ie-min.css">
<![endif]-->
<!--[if gt IE 8]><!-->
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css">
<!--<![endif]-->
下面是grids-responsive.css文件中默認(rèn)的響應(yīng)判斷:
Pure不同于其他框架,她更加開放化、簡單化、扁平化。我們始終認(rèn)為:編寫新的CSS規(guī)則比覆蓋已有的CSS規(guī)則更容易。通過增加幾行代碼就能做出屬于你自己的的UI,想想都讓人激動!
另外還有豐富的表單、按鈕、表格、菜單等等擴展組件
此外,還有:
想要詳細(xì)學(xué)的同學(xué)可以上官網(wǎng)學(xué)習(xí)。https://www.purecss.cn/
隨著現(xiàn)代 JavaScript 開發(fā) Web 應(yīng)用變得復(fù)雜,命名沖突和依賴關(guān)系也變得難以處理,因此需要模塊化。而引入模塊化,可以避免命名沖突、方便依賴關(guān)系管理、提高了代碼的復(fù)用性和和維護性,因此,在 JavaScript 沒有模塊功能的前提下,只能通過第三方規(guī)范實現(xiàn)模塊化:
它們都是基于 JavaScript 的語法和詞法特性 “偽造” 出類似模塊的行為。而 TC-39 在 ECMAScript 2015 中加入了模塊規(guī)范,簡化了上面介紹的模塊加載器,原生意味著可以取代上述的規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案,比使用庫更有效率。而 ES6 的模塊化的設(shè)計目標(biāo):
ECMAScript 在 2015 年開始支持模塊標(biāo)準(zhǔn),此后逐漸發(fā)展,現(xiàn)已經(jīng)得到了所有主流瀏覽器的支持。ECMAScript 2015 版本也被稱為 ECMAScript 6。
ES6 模塊借用了 CommonJS 和 AMD 的很多優(yōu)秀特性,如下所示:
ES6 模塊系統(tǒng)也增加了一些新行為。
瀏覽器運行時在知道應(yīng)該把某個文件當(dāng)成模塊時,會有條件地按照上述 ES6 模塊行為來施加限制。與 <script type="module"> 關(guān)聯(lián)或者通過 import 語句加載的 JavaScript 文件會被認(rèn)定為模塊。
ES6 模塊內(nèi)部的所有變量,外部無法獲取,因此提供了 export 關(guān)鍵字從模塊中導(dǎo)出實時綁定的函數(shù)、對象或原始值,這樣其他程序可以通過 import 關(guān)鍵字使用它們。export 支持兩種導(dǎo)出方式:命名導(dǎo)出和默認(rèn)導(dǎo)出。不同的導(dǎo)出方式對應(yīng)不同的導(dǎo)入方式。
在 ES6 模塊中,無論是否聲明 "use strict;" 語句,默認(rèn)情況下模塊都是在嚴(yán)格模式下運行。export 語句不能用在嵌入式腳本中。
通過在聲明的前面加上 export 關(guān)鍵字,一個模塊可以導(dǎo)出多個內(nèi)容。這些導(dǎo)出的內(nèi)容通過名字區(qū)分,被稱為命名導(dǎo)出。
// 導(dǎo)出單個特性(可以導(dǎo)出 var,let,const)
export let name = "小明";
export function sayHi(name) {
console.log(`Hello, ${name}!`);
}
export class Sample {
...
}
或者導(dǎo)出事先定義的特性
let name = "小明";
const age = 18;
function sayHi(name) {
console.log(`Hello, ${name}!`);
}
export {name, age, sayHi}
導(dǎo)出時也可以指定別名,別名必須在 export 子句的大括號語法中指定。因此,聲明值、導(dǎo)出值和未導(dǎo)出值提供別名不能在一行完成。
export {name as username, age, sayHi}
但導(dǎo)出語句必須在模塊頂級,不能嵌套在某個塊中:
// 允許
export ...
// 不允許
if (condition) {
export ...
}
默認(rèn)導(dǎo)出就好像模塊與被導(dǎo)出的值是一回事。默認(rèn)導(dǎo)出使用 default 關(guān)鍵字將一個值聲明為默認(rèn)導(dǎo)出,每個模塊只能有一個默認(rèn)導(dǎo)出。重復(fù)的默認(rèn)導(dǎo)出會導(dǎo)致 SyntaxError。如下所示:
// 導(dǎo)出事先定義的特性作為默認(rèn)值
export default {
name: "Xiao Ming",
age: 18,
sex: "boy"
};
export {sayHi as default} // ES 6 模塊會識別作為別名提供的 default 關(guān)鍵字。此時,雖然對應(yīng)的值是使用命名語法導(dǎo)出的,實際上則會稱為默認(rèn)導(dǎo)出 等同于 export default function sayHi() {}
// 導(dǎo)出單個特性作為默認(rèn)值
export default function () {...}
export default class {...}
ES6 規(guī)范對不同形式的 export 語句中可以使用什么不可以使用什么規(guī)定了限制。某些形式允許聲明和賦值,某些形式只允許表達式,而某些形式則只允許簡單標(biāo)識符。注意,有的形式使用了分號,有的則沒有。
下面列出幾種會導(dǎo)致錯誤的 export 形式:
// 會導(dǎo)致錯誤的不同形式:
// 行內(nèi)默認(rèn)導(dǎo)出中不能出現(xiàn)變量聲明
export default const name = '小劉';
// 只有標(biāo)識符可以出現(xiàn)在export 子句中
export { 123 as name }
// 別名只能在export 子句中出現(xiàn)
export const name = '小紅' as uname;
注意:聲明、賦值和導(dǎo)出標(biāo)識符最好分開。這樣不容易搞錯了,同時也可以讓 export 語句集中在一塊。而且,沒有被 export 關(guān)鍵字導(dǎo)出的變量、函數(shù)或類會在模塊內(nèi)保持私有。
模塊導(dǎo)入的值還可以再次導(dǎo)出,這樣的話,可以在父模塊集中多個模塊的多個導(dǎo)出。可以使用 export from 語法實現(xiàn):
export {default as m1, name} from './module1.js'
// 等效于
import {default as m1, name} from "./module1.js"
export {m1, name}
外部模塊的默認(rèn)導(dǎo)出也可以重用為當(dāng)前模塊的默認(rèn)導(dǎo)出:
export { default } from './module1.js';
也可以在重新導(dǎo)出時,將導(dǎo)入模塊修改為默認(rèn)導(dǎo)出,如下所示:
export { name as default } from './module1.js';
而想要將所有命名導(dǎo)出可以使用如下語法:
export * from './module1.js';
該語法會忽略默認(rèn)導(dǎo)出。但這種語法也要注意導(dǎo)出名稱是否沖突。如下所示:
// module1.js
export const name = "module1:name";
// module2.js
export * from './mudule1.js'
export const name = "module2:name";
// index.js
import { name } from './module2.js';
console.log(name); // module2:name
最終輸出的是 module2.js 中的值,這個 “重寫” 是靜默發(fā)生的。
使用 export 關(guān)鍵字定義了模塊的對外接口以后,其它模塊就能通過 import 關(guān)鍵字加載這個模塊了。但與 export 類似,import 也必須出現(xiàn)在模塊的頂級:
// 允許
import ...
// 不允許
if (condition) {
import ...
}
模塊標(biāo)識符可以是相對于當(dāng)前模塊的相對路徑,也可以是指向模塊文件的絕對路徑。它必須是純字符串,不能是動態(tài)計算的結(jié)果。例如,不能是拼接的字符串。
當(dāng)使用 export 命名導(dǎo)出時,可以使用 * 批量獲取并賦值給保存導(dǎo)出集合的別名,而無須列出每個標(biāo)識符:
const name = "Xiao Ming", age = 18, sex = "boy";
export {name, age, sex}
// 上面的命名導(dǎo)出可以使用如下形式導(dǎo)入(上面的代碼是在 module1.js 模塊中)
import * as Sample from "./module1.js"
console.log(`My name is ${Sample.name}, A ${Sample.sex},${Sample.age} years old.`);
也可以指名導(dǎo)入,只需要把名字放在 {} 中即可:
import {name, sex as s, age} from "./module1.js";
console.log(`My name is ${name}, A ${s},${age} years old.`);
import 引入是采用的 Singleton 模式,多次使用 import 引入同一個模塊時,只會引入一次該模塊的實例:
import {name, age} from "./module1.js";
import {sex as s} from "./module1.js";
// 等同于,并且只會引入一個 module1.js 實例
import {name, sex as s, age} from "./module1.js";
而使用默認(rèn)導(dǎo)出的話,可以使用 default 關(guān)鍵字并提供別名來導(dǎo)入,也可以直接使用標(biāo)識符就是默認(rèn)導(dǎo)出的別名導(dǎo)入:
import {default as Sample} from "./module1.js"
// 與下面的方式等效
import Sample from "./module1.js"
而模塊中同時有命名導(dǎo)出和默認(rèn)導(dǎo)出,可以在 import 語句中同時導(dǎo)入。下面三種方式都等效。
import Sample, {sayHi} from "./module1.js"
import {default as Sample, sayHi} from "./module1.js"
import Sample, * as M1 from "./module1.js"
當(dāng)然,也可以將整個模塊作為副作用而導(dǎo)入,而不導(dǎo)入模塊中的特定內(nèi)容。這將運行模塊中的全局代碼,但實際上不導(dǎo)入任何值。
import './module1.js'
import 導(dǎo)入的值與 export 導(dǎo)出的值是綁定關(guān)系,綁定是不可變的。因此,import 對所導(dǎo)入的模塊是只讀的。但是可以通過調(diào)用被導(dǎo)入模塊的函數(shù)來達到目的。
import Sample, * as M1 from "./module1.js"
Sample = "Modify Sample"; // 錯誤
M1.module1 = "Module 1"; // 錯誤
Sample.name = "小亮"; // 允許
這樣做的好處是能夠支持循環(huán)依賴,并且一個大的模塊可以拆成若干個小模塊時也可以運行,只要不嘗試修改導(dǎo)入的值。
注意:如果要在瀏覽器中原生加載模塊,則文件必須帶有 .js 擴展名,不然可能無法解析。而使用構(gòu)建工具或第三方模塊加載器打包或解析 ES6 模塊,可能不需要包含擴展名。
標(biāo)準(zhǔn)的 import 關(guān)鍵字導(dǎo)入模塊是靜態(tài)的,會使所有被導(dǎo)入的模塊,在加載時就被編譯。而最新的 ES11 標(biāo)準(zhǔn)中引入了動態(tài)導(dǎo)入函數(shù) import(),不必預(yù)先加載所有模塊。該函數(shù)會將模塊的路徑作為參數(shù),并返回一個 Promise,在它的 then 回調(diào)里使用加載后的模塊:
import ('./module1.mjs')
.then((module) => {
// Do something with the module.
});
這種使用方式也支持 await 關(guān)鍵字。
let module = await import('./module1.js');
import() 的使用場景如下:
ES6 模塊既可以通過瀏覽器原生加載,也可以與第三方加載器和構(gòu)建工具一起加載。
完全支持 ES6 模塊的瀏覽器可以從頂級模塊異步加載整個依賴圖。瀏覽器會解析入口模塊,確定依賴,并發(fā)送對依賴模塊的請求。這些文件通過網(wǎng)絡(luò)返回后,瀏覽器會解析它們的內(nèi)容,確認(rèn)依賴,如果二級依賴還沒有加載,則會發(fā)送更多請求。這個異步遞歸加載過程會持續(xù)到整個依賴圖都解析完成。解析完依賴,應(yīng)用就可以正式加載模塊了。
模塊文件按需加載,且后續(xù)模塊的請求會因為每個依賴模塊的網(wǎng)絡(luò)延遲而同步延遲。即,module1 依賴 module2,module2 依賴 module3。瀏覽器在對 module2 的請求完成之前并不知道要請求 module3。這種架子啊方式效率高,也不需要外部工具,但加載大型應(yīng)用的深度依賴圖可能要花費很長時間。
想要在 HTML 頁面中使用 ES6 模塊,需要將 type="module" 屬性放在 <script> 標(biāo)簽中,來聲明該 <script> 所包含的代碼在瀏覽器中作為模塊執(zhí)行。它可以嵌入在網(wǎng)頁中,也可以作為外部文件引入:
<script type="module">
// 模塊代碼
</script>
<script type="module" src="./module1.js"></script>
<script type="module">模塊加載的順序與 <script defer> 加載的腳本一樣按順序執(zhí)行。但執(zhí)行會延遲到文檔解析完成,但執(zhí)行順序就是<script type="module">在頁面中出現(xiàn)的順序。
也可以給模塊標(biāo)簽添加 async 屬性。這樣影響是雙重的,不僅模塊執(zhí)行順序不再與 <script> 標(biāo)簽在頁面中的順序綁定,模塊也不會等待文檔完成解析才執(zhí)行。不過,入口模塊必須等待其依賴加載完成。
Worker 為了支持 ES6 模塊,在 Worker 構(gòu)造函數(shù)中可以接收第二個參數(shù),其 type 屬性的默認(rèn)值是 classic,可以將 type 設(shè)置為 module 來加載模塊文件。如下所示:
// 第二個參數(shù)默認(rèn)為{ type: 'classic' }
const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });
在基于模塊的工作者內(nèi)部,self.importScripts() 方法通常用于在基于腳本的工作者中加載外部腳本,調(diào)用它會拋出錯誤。這是因為模塊的 import 行為包含了 importScripts()。
如果瀏覽器原生支持 ES6 模塊,可以直接使用,而不支持的瀏覽器可以使用第三方模塊系統(tǒng)(System.js)或在構(gòu)建時將 ES6 模塊進行轉(zhuǎn)譯。
腳本模塊可以使用 type="module" 屬性設(shè)定,而對于不支持模塊的瀏覽器,可以使用 nomodule 屬性。此屬性會通知支持 ES6 模塊的瀏覽器不執(zhí)行腳本。不支持模塊的瀏覽器無法識別該屬性,從而忽略該屬性。如下所示:
// 支持模塊的瀏覽器會執(zhí)行這段腳本
// 不支持模塊的瀏覽器不會執(zhí)行這段腳本
<script type="module" src="module.js"></script>
// 支持模塊的瀏覽器不會執(zhí)行這段腳本
// 不支持模塊的瀏覽器會執(zhí)行這段腳本
<script nomodule src="script.js"></script>
ES6 在語言層面上支持了模塊,結(jié)束了 CommonJS 和 AMD 這兩個模塊加載器的長期分裂狀況,重新定義了模塊功能,集兩個規(guī)范于一身,并通過簡單的語法聲明來暴露。
模塊的使用不同方式加載 .js 文件,它與腳本有很大的不同:
瀏覽器對原生模塊的支持越來越好,但也提供了穩(wěn)健的工具以實現(xiàn)從不支持到支持 ES6 模塊的過渡。
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。