果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 這樣的術語,很快讓你不堪重負。
JavaScript 模塊系統可能令人生畏,但理解它對 Web 開發人員至關重要。
在這篇文章中,我將以簡單的言語(以及一些代碼示例)為你解釋這些術語。 希望這對你有會有幫助!
好作者能將他們的書分成章節,優秀的程序員將他們的程序劃分為模塊。
就像書中的章節一樣,模塊只是文字片段(或代碼,視情況而定)的集群。然而,好的模塊是高內聚低松耦的,具有不同的功能,允許在必要時對它們進行替換、刪除或添加,而不會擾亂整體功能。
使用模塊有利于擴展、相互依賴的代碼庫,這有很多好處。在我看來,最重要的是:
1)可維護性: 根據定義,模塊是高內聚的。一個設計良好的模塊旨在盡可能減少對代碼庫部分的依賴,這樣它就可以獨立地增強和改進,當模塊與其他代碼片段解耦時,更新單個模塊要容易得多。
回到我們的書的例子,如果你想要更新你書中的一個章節,如果對一個章節的小改動需要你調整每一個章節,那將是一場噩夢。相反,你希望以這樣一種方式編寫每一章,即可以在不影響其他章節的情況下進行改進。
2)命名空間: 在 JavaScript 中,頂級函數范圍之外的變量是全局的(這意味著每個人都可以訪問它們)。因此,“名稱空間污染”很常見,完全不相關的代碼共享全局變量。
在不相關的代碼之間共享全局變量在開發中是一個大禁忌。正如我們將在本文后面看到的,通過為變量創建私有空間,模塊允許我們避免名稱空間污染。
3)可重用性:坦白地說:我們將前寫過的代碼復制到新項目中。 例如,假設你從之前項目編寫的一些實用程序方法復制到當前項目中。
這一切都很好,但如果你找到一個更好的方法來編寫代碼的某些部分,那么你必須記得回去在曾經使用過的其他項目更新它。
這顯然是在浪費時間。如果有一個我們可以一遍又一遍地重復使用的模塊,不是更容易嗎?
有多種方法來創建模塊,來看幾個:
模塊模式用于模擬類的概念(因為 JavaScript 本身不支持類),因此我們可以在單個對象中存儲公共和私有方法和變量——類似于在 Java 或 Python 等其他編程語言中使用類的方式。這允許我們為想要公開的方法創建一個面向公共的 API,同時仍然將私有變量和方法封裝在閉包范圍中。
有幾種方法可以實現模塊模式。在第一個示例中,將使用匿名閉包,將所有代碼放在匿名函數中來幫助我們實現目標。(記住:在 JavaScript 中,函數是創建新作用域的唯一方法。)
例一:匿名閉包
(function () {
// 將這些變量放在閉包范圍內實現私有化
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()); // 掛機科了次
}());
使用這個結構,匿名函數就有了自己的執行環境或“閉包”,然后我們立即執行。這讓我們可以從父(全局)命名空間隱藏變量。
這種方法的優點是,你可以在這個函數中使用局部變量,而不會意外地覆蓋現有的全局變量,但仍然可以訪問全局變量,就像這樣:
var global = '你好,我是一個全局變量。)';
(function () {
// 將這些變量放在閉包范圍內實現私有化
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); // 你好,我是一個全局變量。
}());
注意,匿名函數的圓括號是必需的,因為以關鍵字 function 開頭的語句通常被認為是函數聲明(請記住,JavaScript 中不能使用未命名的函數聲明)。因此,周圍的括號將創建一個函數表達式,并立即執行這個函數,這還有另一種叫法 立即執行函數(IIFE)。如果你對這感興趣,可以在這里了解到更多。
例二:全局導入
jQuery 等庫使用的另一種流行方法是全局導入。它類似于我們剛才看到的匿名閉包,只是現在我們作為參數傳入全局變量:
(function (globalVariable) {
// 在這個閉包范圍內保持變量的私有化
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// 通過 globalVariable 接口公開下面的方法
// 同時將方法的實現隱藏在 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 是唯一的全局變量。與匿名閉包相比,這種方法的好處是可以預先聲明全局變量,使得別人更容易閱讀代碼。
例三:對象接口
另一種方法是使用立即執行函數接口對象創建模塊,如下所示:
var myGradesCalculate = (function () {
// 將這些變量放在閉包范圍內實現私有化
var myGrades = [93, 95, 88, 0, 55, 91];
// 通過接口公開這些函數,同時將模塊的實現隱藏在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 語句中(例如算平均分和掛科數方法)來決定我們想要保留的變量/方法(例如 myGrades)以及我們想要公開的變量/方法。
例四:顯式模塊模式
這與上面的方法非常相似,只是它確保所有方法和變量在顯式公開之前都是私有的:
var myGradesCalculate = (function () {
// 將這些變量放在閉包范圍內實現私有化
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.'
這可能看起來很多,但它只是模塊模式的冰山一角。 以下是我在自己的探索中發現有用的一些資源:
所有這些方法都有一個共同點:使用單個全局變量將其代碼包裝在函數中,從而使用閉包作用域為自己創建一個私有名稱空間。
雖然每種方法都有效且都有各自特點,但卻都有缺點。
首先,作為開發人員,你需要知道加載文件的正確依賴順序。例如,假設你在項目中使用 Backbone,因此你可以將 Backbone 的源代碼 以<script> 腳本標簽的形式引入到文件中。
但是,由于 Backbone 對 Underscore.js 有很強的依賴性,因此 Backbone 文件的腳本標記不能放在Underscore.js 文件之前。
作為一名開發人員,管理依賴關系并正確處理這些事情有時會令人頭痛。
另一個缺點是它們仍然會導致名稱空間沖突。例如,如果兩個模塊具有相同的名稱怎么辦?或者,如果有一個模塊的兩個版本,并且兩者都需要,該怎么辦?
幸運的是,答案是肯定的。
有兩種流行且實用的方法:CommonJS 和 AMD。
CommonJS 是一個志愿者工作組,負責設計和實現用于聲明模塊的 JavaScript API。
CommonJS 模塊本質上是一個可重用的 JavaScript,它導出特定的對象,使其可供其程序中需要的其他模塊使用。 如果你已經使用 Node.js 編程,那么你應該非常熟悉這種格式。
使用 CommonJS,每個 JavaScript 文件都將模塊存儲在自己獨立的模塊上下文中(就像將其封裝在閉包中一樣)。 在此范圍內,我們使用 module.exports 導出模塊,或使用 require 來導入模塊。
在定義 CommonJS 模塊時,它可能是這樣的:
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
我們使用特殊的對象模塊,并將函數的引用放入 module.exports 中。這讓 CommonJS 模塊系統知道我們想要公開什么,以便其他文件可以使用它。
如果想使用 myModule,只需要使用 require 方法就可以,如下:
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
與前面討論的模塊模式相比,這種方法有兩個明顯的好處:
另外需要注意的是,CommonJS 采用服務器優先方法并同步加載模塊。 這很重要,因為如果我們需要三個其他模塊,它將逐個加載它們。
現在,它在服務器上運行良好,但遺憾的是,在為瀏覽器編寫 JavaScript 時使用起來更加困難。 可以這么說,從網上讀取模塊比從磁盤讀取需要更長的時間。 只要加載模塊的腳本正在運行,它就會阻止瀏覽器運行其他任何內容,直到完成加載,這是因為 JavaScript 是單線程且 CommonJS 是同步加載的。
CommonJS一切都很好,但是如果我們想要異步加載模塊呢? 答案是 異步模塊定義,簡稱 AMD。
使用 AMD 的加載模塊如下:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
define 函數的第一個參數是一個數組,數組中是依賴的各種模塊。這些依賴模塊在后臺(以非阻塞的方式)加載進來,一旦加載完畢,define 函數就會調用第二個參數,即回調函數執行操作。
接下來,回調函數接收參數,即依賴模塊 - 示例中就是 myModule 和 myOtherModule - 允許函數使用這些依賴項, 最后,所依賴的模塊本身也必須使用 define 關鍵字來定義。例如,myModule如下所示:
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
因此,與 CommonJS 不同,AMD 采用瀏覽器優先的方法和異步行為來完成工作。 (注意,有很多人堅信在開始運行代碼時動態加載文件是不利的,我們將在下一節關于模塊構建的內容中探討更多內容)。
除了異步性,AMD 的另一個好處是模塊可以是對象,函數,構造函數,字符串,JSON 和許多其他類型,而CommonJS 只支持對象作為模塊。
也就是說,和CommonJS相比,AMD不兼容io、文件系統或者其他服務器端的功能特性,而且函數包裝語法與簡單的require 語句相比有點冗長。
對于同時支持 AMD 和 CommonJS 特性的項目,還有另一種格式:通用模塊定義(Universal Module Definition, UMD)。
UMD 本質上創造了一種使用兩者之一的方法,同時也支持全局變量定義。因此,UMD 模塊能夠同時在客戶端和服務端同時工作。
簡單看一下 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 里有更多關于 UMD 的例子。
你可能已經注意到,上面的模塊都不是 JavaScript 原生的。相反,我們已經創建了通過使用模塊模式、CommonJS 或 AMD 來模擬模塊系統的方法。
幸運的是,TC39(定義 ECMAScript 的語法和語義的標準組織)一幫聰明的人已經引入了ECMAScript 6(ES6)的內置模塊。
ES6 為導入導出模塊提供了很多不同的可能性,已經有許多其他人花時間解釋這些,下面是一些有用的資源:
與 CommonJS 或 AMD 相比,ES6 模塊最大的優點在于它能夠同時提供兩方面的優勢:簡明的聲明式語法和異步加載,以及對循環依賴項的更好支持。
也許我個人最喜歡的 ES6 模塊功能是它的導入模塊是導出時模塊的實時只讀視圖。(相比起 CommonJS,導入的是導出模塊的拷貝副本,因此也不是實時的)。
下面是一個例子:
// 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
在這個例子中,我們基本上創建了兩個模塊的對象:一個用于導出它,一個在我們需要的時候引入。
此外,在 main.js 中的對象目前是與原始模塊是相互獨立的,這就是為什么即使我們執行 increment 方法,它仍然返回 1,因為引入的變量和最初導入的變量是毫無關聯的。需要改變你引入的對象唯一的方式是手動執行增加:
counter.counter++;
console.log(counter.counter); // 2
另一方面,ES6創建了我們導入的模塊的實時只讀視圖:
// 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
超酷?我發現這一點是因為ES6允許你可以把你定義的模塊拆分成更小的模塊而不用刪減功能,然后你還能反過來把它們合成到一起, 完全沒問題。
總體上看,模塊打包只是將一組模塊(及其依賴項)以正確的順序拼接到一個文件(或一組文件)中的過程。正如 Web開發的其它方方面面,棘手的問題總是潛藏在具體的細節里。
將程序劃分為模塊時,通常會將這些模塊組織到不同的文件和文件夾中。 有可能,你還有一組用于正在使用的庫的模塊,如 Underscore 或 React。
因此,每個文件都必須以一個 <script> 標簽引入到主 HTML 文件中,然后當用戶訪問你的主頁時由瀏覽器加載進來。 每個文件使用 <script> 標簽引入,意味著瀏覽器不得不分別逐個的加載它們。
這對于頁面加載時間來說簡直是噩夢。
為了解決這個問題,我們將所有文件打包或“拼接”到一個大文件(或視情況而定的幾個文件),以減少請求的數量。 當你聽到開發人員談論“構建步驟”或“構建過程”時,這就是他們所談論的內容。
另一種加速構建操作的常用方法是“縮減”打包代碼。 縮減是從源代碼中移除不必要的字符(例如,空格,注釋,換行符等)的過程,以便在不改變代碼功能的情況下減少內容的整體大小。
較少的數據意味著瀏覽器處理時間會更快,從而減少了下載文件所需的時間。 如果你見過具有 “min” 擴展名的文件,如 “underscore-min.js” ,可能會注意到與完整版相比,縮小版本非常小(不過很難閱讀)。
除了捆綁和/或加載模塊之外,模塊捆綁器還提供了許多其他功能,例如在進行更改時生成自動重新編譯代碼或生成用于調試的源映射。
構建工具(如 Gulp 和 Grunt)能為開發者直接進行拼接和縮減,確保為開發人員提供可讀代碼,同時有利于瀏覽器執行的代碼。
當你使用一種標準模塊模式(上部分討論過)來定義模塊時,拼接和縮減文件非常有用。 你真正在做的就是將一堆普通的 JavaScript 代碼捆綁在一起。
但是,如果你堅持使用瀏覽器無法解析的非原生模塊系統(如 CommonJS 或 AMD(甚至是原生 ES6模塊格式)),則需要使用專門工具將模塊轉換為排列正確、瀏覽器可解析的代碼。 這就是 Browserify,RequireJS,Webpack 和其他“模塊打包工具”或“模塊加載工具”的用武之地。
除了打包和/或加載模塊之外,模塊打包器還提供了許多其他功能,例如在進行更改時生成自動重新編譯代碼或生成用于調試的源映射。
下面是一些常見的模塊打包方法:
正如前面所知道的,CommonJS以同步方式加載模塊,這沒有什么問題,只是它對瀏覽器不實用。我提到過有一個解決方案——其中一個是一個名為 Browserify 的模塊打包工具。Browserify 是一個為瀏覽器編譯 CommonJS模塊的工具。
例如,有個 main.js 文件,它導入一個模塊來計算一組數字的平均值:
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),以便遍歷項目的整個依賴關系圖。一旦確定了依賴項的結構,就把它們按正確的順序打包到一個文件中。然后,在 html 里插入一個用于引入 “bundle.js” 的 <script> 標簽,從而確保你的源代碼在一個 HTTP 請求中完成下載。
類似地,如果有多個文件且有多個依賴時,只需告訴 Browserify 的入口文件路徑即可。最后打包后的文件可以通過 Minify-JS 之類的工具壓縮打包后的代碼。
如果你正在使用 AMD,你需要使用像 RequireJS 或者 Curl 這樣的 AMD 加載器。模塊加載器(與模塊打包工具不同)會動態加載程序需要運行的模塊。
提醒一下,AMD 與 CommonJS 的主要區別之一是它以異步方式加載模塊。 從這個意義上說,對于 AMD,從技術上講,實際上并不需要構建步驟,因為異步加載模塊意味著在運行過程中逐步下載那些程序所需要的文件,而不是用戶剛進入頁面就一下把所有文件都下載下來。
但實際上,對于每個用戶操作而言,隨著時間的推移,大容量請求的開銷在生產中沒有多大意義。 大多數 Web 開發人員仍然使用構建工具打包和壓縮 AMD 模塊以獲得最佳性能,例如使用 RequireJS 優化器,r.js 等工具。
總的來說,AMD 和 CommonJS 在打包方面的區別在于:在開發期間,AMD 可以省去任何構建過程。當然,在代碼上線前,要使用優化工具(如 r.js)進行優化。
就打包工具而言,Webpack 是一個新事物。它被設計成與你使用的模塊系統無關,允許開發人員在適當的情況下使用 CommonJS、AMD 或 ES6。
你可能想知道,為什么我們需要 Webpack,而我們已經有了其他打包工具了,比如 Browserify 和 RequireJS,它們可以完成工作,并且做得非常好。首先,Webpack 提供了一些有用的特性,比如 “代碼分割”(code splitting) —— 一種將代碼庫分割為“塊(chunks)”的方式,從而能實現按需加載。
例如,如果你的 Web 應用程序,其中只需要某些代碼,那么將整個代碼庫都打包進一個大文件就不是很高效。 在這種情況下,可以使用代碼分割,將需要的部分代碼抽離在"打包塊",在執行按需加載,從而避免在最開始就遇到大量負載的麻煩。
代碼分割只是 Webpack 提供的眾多引人注目的特性之一,網上有很多關于 “Webpack 與 Browserify 誰更好” 的激烈討論。以下是一些客觀冷靜的討論,幫助我稍微理清了頭緒:
當前 JS 模塊規范(CommonJS, AMD) 與 ES6 模塊之間最重要的區別是 ES6 模塊的設計考慮到了靜態分析。這意味著當你導入模塊時,導入的模塊在編譯階段也就是代碼開始運行之前就被解析了。這允許我們在運行程序之前移,移除那些在導出模塊中不被其它模塊使用的部分。移除不被使用的模塊能節省空間,且有效地減少瀏覽器的壓力。
一個常見的問題,使用一些工具,如 Uglify.js ,縮減代碼時,有一個死碼刪除的處理,它和 ES6 移除沒用的模塊又有什么不同呢?只能說 “視情況而定”。
死碼消除(Dead codeelimination)是一種編譯器原理中編譯最優化技術,它的用途是移除對程序運行結果沒有任何影響的代碼。移除這類的代碼有兩種優點,不但可以減少程序的大小,還可以避免程序在運行中進行不相關的運算行為,減少它運行的時間。不會被運行到的代碼(unreachable code)以及只會影響到無關程序運行結果的變量(Dead Variables),都是死碼(Dead code)的范疇。
有時,在 UglifyJS 和 ES6 模塊之間死碼消除的工作方式完全相同,有時則不然。如果你想驗證一下, Rollup’s wiki 里有個很好的示例。
ES6 模塊的不同之處在于死碼消除的不同方法,稱為“tree shaking”。“tree shaking” 本質上是死碼消除反過程。它只包含包需要運行的代碼,而非排除不需要的代碼。來看個例子:
假設有一個帶有多個函數的 utils.js 文件,每個函數都用 ES6 的語法導出:
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;
}
接著,假設我們不知道要在程序中使用什么 utils.js 中的哪個函數,所以我們將上述的所有模塊導入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) });
注意:只導出我們使用的 each 函數。
同時,如果決定使用 filte r函數而不是每個函數,最終會看到如下的結果:
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 函數都被包含進來。這是因為 filter 在定義時使用了 each。因此也需要導出該函數模塊以保證程序正常運行。
我們知道 ES6 模塊的加載方式與其他模塊格式不同,但我們仍然沒有討論使用 ES6 模塊時的構建步驟。
遺憾的是,因為瀏覽器對 ES6模 塊的原生支持還不夠完善,所以現階段還需要我們做一些補充工作。
下面是幾個在瀏覽器中 構建/轉換 ES6 模塊的方法,其中第一個是目前最常用的方法:
作為 web 開發人員,我們必須經歷很多困難。轉換語法優雅的ES6代碼以便在瀏覽器里運行并不總是容易的。
問題是,什么時候 ES6 模塊可以在瀏覽器中運行而不需要這些開銷?
答案是:“盡快”。
ECMAScript 目前有一個解決方案的規范,稱為 ECMAScript 6 module loader API。簡而言之,這是一個綱領性的、基于 Promise 的 API,它支持動態加載模塊并緩存它們,以便后續導入不會重新加載模塊的新版本。
它看起來如下:
// 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 標簽指定 “type=module” 來定義模塊,如:
<script type="module">
// loads the 'myModule' export from 'mymodule.js'
import { hello } from 'mymodule';
new Hello(); // 'Hello, I am a module!'
</script>
更加詳細的介紹也可以在 Github 上查看:es-module-loader
此外,如果您想測試這種方法,請查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在瀏覽器和 Node 中動態加載任何模塊格式(ES6模塊,AMD,CommonJS 或 全局腳本)。
它跟蹤“模塊注冊表”中所有已加載的模塊,以避免重新加載先前已加載過的模塊。 更不用說它還會自動轉換ES6模塊(如果只是設置一個選項)并且能夠從任何其他類型加載任何模塊類型!
對于日益普及的 ES6 模塊,下面有一些有趣的觀點:
對于 HTTP/1,每個TCP連接只允許一個請求。這就是為什么加載多個資源需要多個請求。有了 HTTP/2,一切都變了。HTTP/2 是完全多路復用的,這意味著多個請求和響應可以并行發生。因此,我們可以在一個連接上同時處理多個請求。
由于每個 HTTP 請求的成本明顯低于HTTP/1,因此從長遠來看,加載一組模塊不會造成很大的性能問題。一些人認為這意味著模塊打包不再是必要的,這當然有可能,但這要具體情況具體分析了。
例如,模塊打包還有 HTTP/2 沒有好處,比如移除冗余的導出模塊以節省空間。 如果你正在構建一個性能至關重要的網站,那么從長遠來看,打包可能會為你帶來增量優勢。 也就是說,如果你的性能需求不是那么極端,那么通過完全跳過構建步驟,可以以最小的成本節省時間。
總的來說,絕大多數網站都用上 HTTP/2 的那個時候離我們現在還很遠。我預測構建過程將會保留,至少在近期內。
一旦 ES6 成為模塊標準,我們還需要其他非原生模塊規范嗎?
我覺得還有。
Web 開發遵守一個標準方法進行導入和導出模塊,而不需要中間構建步驟——網頁開發長期受益于此。但 ES6 成為模塊規范需要多長時間呢?
機會是有,但得等一段時間 。
再者,眾口難調,所以“一個標準的方法”可能永遠不會成為現實。
希望這篇文章能幫你理清一些開發者口中的模塊和模塊打包的相關概念,共進步。
言
目前,教學、教研各種內容線上沉淀、展示豐富多彩,但線上內容“線下化”能力不足或過分依賴人力,比如,線上練習題組卷后以PDF形式分發給學生,家長希望將考試、練習題目打印后,學生帶到學校去做(高中生使用手機等電子設備的時間有限),線上各類分析報告以PDF形式分享給學生/家長等。
從業務方面看,不同業務線的多個業務場景都有輸出PDF的訴求,如果各業務線自己設計、實現符合自身業務場景的具體方案,除調研、開發工作量較大之外,還會有重復調研,踩坑的情況。
從技術角度看,線上內容轉PDF的內容源頭來自于H5富文本內容,業界內以此為基礎的PDF生成方案多種多樣,也各有優劣,比如:
方案對比-表格-1
因此,我們綜合了各種PDF生成方案并總結了在探索講義生成PDF過程中的經驗,抽象出了一套通用的,可復用的能力供各業務線快速利用,基本方案和優劣如下:
最終方案-表格-2
目 標
旨在提供一套以H5為載體的PDF通用生成方案,這套方案有如下特點:
這套方案可分為兩個核心部分,頁面展示側 - Medusa,PDF生成側 - Hydra
頁面展示側 - Medusa
我們頁面展示側的通用能力——Medusa,是基于Paged.js的二次封裝,并以NPM包形式提供給業務方使用。Medusa可對任何HTML進行分頁、并根據配置添加頁眉、頁腳等,最終將處理后的HTML渲染到頁面中。Medusa封裝并簡化了對PDF格式的配置,可覆蓋絕大多數業務場景,使得各業務場景將更多精力投入其自身業務邏輯的開發。
之所以選擇Pagedjs為基礎開發我們自己的SDK,是因為它是目前我們能找到的唯一開源的、具有HTML內容分頁,樣式處理的前端庫,同時我們也在講義中經過了長期的摸索與沉淀。
接下來將詳細介紹Paged.js原理、Medusa支持的功能與使用方法。
一 Paged.js是如何工作的
Paged.js包含了 3 個大模塊
這里將主要介紹 Previewer 和 Chunker,因為我們的二次開發和維護不涉及到Polisher。
Previewer
Previewer 的工作非常簡單,但我們會主要利用它封裝我們的Medusa,初始化一個Previewer對象,Previewer初始化了Chunker和Polisher對象:
Medusa-代碼-1
再調用Previewer的preview()方法,preview()方法做了兩件事:
Medusa-代碼-2
當chunker.flow結束,即可在瀏覽器看到整個頁面處理完之后的樣子。
Chunker
首先,Chunker解析、預處理需要分頁的HTML,為其添加一些必要的屬性
Medusa-代碼-3
然后創建容納所有頁(pages)的容器,并掛載到renderTo容器下(默認Body),以備組織后續的所有頁:
Medusa-代碼-4
接著,chunker創建了一個page模版,以便增加頁面使用:
Medusa-代碼-5
其中,TEMPLATE是Pagedjs內部創建頁面時所使用的基礎模版。
Medusa-代碼-6
接下來,chunker進入了渲染+分頁過程(這個過程我們不會在二次開發中做修改,但需要了解其基本思路以便在出問題時能有解決思路),這個過程在循環一個迭代器(*layout),迭代器一直在做3件事:
原則:
尋找overflow時會將盡可能多的內容節點插入內容區域,這里,“盡可能多”分為幾種情況,比如:
步驟:
Pagedjs遵循了如下步驟去尋找overflow:
兩個前置條件:
i. 從需要處理的內容第一個節點開始,判斷是否 node.left >= contentArea.right || node.top >= contentArea.bottom
Medusa-代碼-7
ii.如果不滿足,則判斷 node.right <= contentArea.right && node.bottom <= contentArea.bottom
Medusa-代碼-8
iii.如果不滿足,那說明有子節點overflow了,則繼續深入其子節點查找即可。
3.使用模版添加新的頁面,并從BreakToken處繼續上述動作。
二 Medusa支持的功能及使用方法
基于Paged.js,Medusa支持了如下功能,并為業務方提供了更加簡潔、定制化的配置。
下方是調用Medusa的代碼示例:
Medusa-代碼-9
1.1 動態頁面分頁能力
Medusa核心功能,可將連續的HTML頁面轉化成一頁頁PDF樣式的HTML。
1.2 單頁模版配置 -> 生成能力
通過Grid布局,Paged.js將一個單頁模版分為多個區域,整體分為2個大的部分:
業務方通過簡單的配置,即可還原UI設計稿中的PDF樣式,例子如下圖:
1.2.1 base
頁面基礎配置是對每頁的。支持紙型或頁面寬高、內容區域margin、padding、背景及水印的設置。
在封裝Medusa時,Medusa將讀取傳入的頁面模版配置、靜態頁內容配置,并將樣式上的配置解析并轉化為Previewer可理解的樣式內容,比如頁面寬高的設置:
Medusa-代碼-10
將被轉化為:
Medusa-代碼-11
1.2.2 surround
2. 目前支持3種類型的surround item:
example:
Medusa-代碼-12
1.3 前/后置靜態頁面
業務方可通過如下方式配置靜態頁面的具體內容:
Medusa-代碼-13
其中,傳入的React JSX Element將會被這樣處理:
Medusa-代碼-14
處理完成后,將HTML String拼接到頁面模版中,再插入分頁后內容的前后。
PDF生成側 - Hydra:
頁面展示側為PDF生成做好了頁面的準備,對于PDF生成側,需要做的工作就更純粹了,業務方除了請求生成PDF,定期檢查PDF生成的進度,無需做任何額外工作。
1.整體流程:
PDF生成是CPU和內存密集型的,由于頁面內容的不確定性,也意味著頁面渲染時間與生成PDF的時間都是不確定的,因此整體PDF生成的鏈路被設計成是異步的,如下圖:
整體流程上,業務方在請求生成PDF時,會先在后端做一條記錄,后端再將任務發送給Node服務,即Hydra;
在生成PDF時, 第 1 步是做頁面上的準備,一個生成任務可能有多個URL頁面需要生成PDF,所以我們預先啟動對應URL數量的PPTR Page,頁面都啟動完成后,進入下一步;
第 2 步:渲染頁面,這個過程中,如果請求是包含多個URL的,這些頁面會同步渲染,在所有頁面渲染完成后,進入下一步。
第 2.5 步,如果是需要生成連續頁碼的一整個PDF,還會做額外的一個動作:頁碼矯正,通過頁碼矯正,可以將同步渲染的每個頁面,按照其之前頁面的頁碼數修正,以保證整體PDF的頁碼的連貫。
第 3 步,通過PPTR Page的能力將頁面轉換為PDF buffer,如有必要,再將生成的PDF buffer拼接到一起生成一整個PDF,或者將每個PDF buffer都生成一個PDF,壓縮成zip文件。
第 4 步,文件上傳OSS,最終返回OSS CDN鏈接。
2.請求生成PDF:
業務側請求將對應頁面生成PDF的時,只需傳入如下字段:
Hydra-代碼-1
3.PDF生成過程:
正如在整體流程中所述,PDF生成側,我們借助 PPTR 的能力打開頁面并生成PDF流。
在頁面調用 Medusa 分頁、組裝能力時,所有內容分頁組裝完成后會向body中插入了一個額外的DOM以標識該頁面處理完成:
Hydra-代碼-2
這是為了 Hydra 感知頁面渲染完成所做的準備,當生成服務的 PPTR 等到該DOM出現時,則表示頁面成功渲染并處理完成了:
Hydra-代碼-3
此后,在上面已經提到過,對于需要將多個頁面生成的PDF拼接成一個PDF的情況,在生成PDF之前需要做一個重要的動作,即頁碼矯正,原因如下:
并且我們不希望頁面的處理是串行的,因為串行勢必導致速度較慢,生成時間長。
這個問題的解決方案如下:
1. 對于每個頁面都啟用一個page,并同時處理
2. 每個頁面處理完成后(pdfLastDOM出現),通過Page.$eval()來統計頁數并記錄:
Hydra-代碼-4
3. 計算出頁面中分頁之后每一個頁面的起始頁碼,以及所有頁面的頁碼總和
4. 再修改頁碼容器樣式的 counterReset 值即可,其后續頁碼可自遞增。
Hydra-代碼-5
5. 之后,再通過 Medusa 在頁面window對象中Polyfill的相關配置,比如需要生成的PDF的單頁寬、高以生成PDF流。
Hydra-代碼-6
6. 最后如有必要,通過pdf-lib拼接這些 pdfBuffer 即可。
Hydra-代碼-7
7. PDF生成完成后,上傳OSS并返回URL鏈接
4.性能、穩定性保證:
在整體方案落地前,我們對服務進行了多次性能測試:
以下載題目為例,在4個容器,每個容器 3C 12G 的配置下的并行處理能力如下:
對于 20 道題目,每個PDF生成任務在 15 頁左右,平均 1 分鐘內能完成 280 個任務的處理。
對于 40 道題目,每個PDF生成任務在 30 頁左右,平均 1 分鐘內能完成 105 個任務的處理。
對于 60 到題目,每個PDF生成任務在 40 頁左右,平均 1 分鐘內能完成 54 個任務的處理。
同時,根據 Hydra 服務的整體的處理能力,后端通過任務隊列的形式幫助我們保證服務不被瞬間的突刺流量擊垮。
已接入/正在接入的相關業務線及場景:
目前,公司有 5 大業務線,8 個場景已經完全接入我們的能力用于 H5 轉 PDF,如下是錯題本、內容資料庫接入后生成的PDF樣例:
錯題本:
內容資料庫試卷:
未來展望
目前整體的PDF生成方案已經能夠滿足大多數場景和內容,但依然有可改進空間。
HTML的流式布局要求我們必須手動的對內容分頁,才能添加頁眉,頁腳等(即Mdusa做的工作),正因為如此,在處理復雜的內容時,可能會出現一些問題:比如,遇到復雜表格時,由于表格可能會有多種多樣的行、列合并,同時表格單元格內的內容也可以多種多樣,在分頁過程中,Medusa內部的PagedJS并不能完美的處理對于長、且復雜的表格的分割,因此可能遇到分割后表格單元格缺失、錯亂或寬高錯誤的問題,這些問題在講義中體現較明顯。
我們仍在持續關注與研究復雜DOM內容的分割問題,會嘗試加以優化和改進PagedJS的能力,同時,我們也以另外一種思路設計了自己的DOM分頁器方案,但經過評估,由于實現比較復雜,成本較高,暫時沒有投入開發資源。
不過,我們相信,未來我們一定能以更完美的方式分割DOM以生成更高質量的PDF。
作者:高源、陳欣博
來源:微信公眾號:高途技術
出處:https://mp.weixin.qq.com/s/c_N7jdNklrNFKR_Cub2Tgg
般商業網站都有一個語言的需求,就是為了照顧使用正體中文的國人,會特地提供一個切換到正體中文的選項(或曰“繁體中文”)。傳統做法是在服務端完成的,即通過某些控件或者過濾器轉換文本語言。這里筆者介紹一種簡單可行的方法,不是在服務端而是利用前端的 JavaScript 就可以切換正體中文。
由于頁面假設設定均使用 Unicode 字符集,于是一般不用考慮 GB2312/BIG5 字符集問題,偉大的 Unicode 字符集已經涵蓋了繁體字的字符了(UTF-8 編碼)。
怎么在頁面調用這個功能呢?很簡單,首先引入下面 Js(注意還依賴 Function.prototype.delegate(), 下面會提及):
;(function(){ var 簡化中文 = '啊阿埃挨哎唉哀皚癌藹矮艾礙愛隘鞍氨安俺按暗岸胺案骯昂盎凹敖熬翱襖傲奧懊澳芭捌扒叭吧笆八疤巴拔跋靶把耙壩霸罷爸白柏百擺佰敗拜稗斑班搬扳般頒板版扮拌伴瓣半辦絆邦幫梆榜膀綁棒磅蚌鎊傍謗苞胞包褒剝薄雹保堡飽寶抱報暴豹鮑爆杯碑悲卑北輩背貝鋇倍狽備憊焙被奔苯本笨崩繃甭泵蹦迸逼鼻比鄙筆彼碧蓖蔽畢斃毖幣庇痹閉敝弊必辟壁臂避陛鞭邊編貶扁便變卞辨辯辮遍標彪膘表鱉憋別癟彬斌瀕濱賓擯兵冰柄丙秉餅炳病并玻菠播撥缽波博勃搏鉑箔伯帛舶脖膊渤泊駁捕卜哺補埠不布步簿部怖擦猜裁材才財睬踩采彩菜蔡餐參蠶殘慚慘燦蒼艙倉滄藏操糙槽曹草廁策側冊測層蹭插叉茬茶查碴搽察岔差詫拆柴豺攙摻蟬饞讒纏鏟產闡顫昌猖場嘗常長償腸廠敞暢唱倡超抄鈔朝嘲潮巢吵炒車扯撤掣徹澈郴臣辰塵晨忱沉陳趁襯撐稱城橙成呈乘程懲澄誠承逞騁秤吃癡持匙池遲弛馳恥齒侈尺赤翅斥熾充沖沖蟲崇寵抽酬疇躊稠愁籌仇綢瞅丑臭初出櫥廚躇鋤雛滁除楚礎儲矗搐觸處揣川穿椽傳船喘串瘡窗幢床闖創吹炊捶錘垂春椿醇唇淳純蠢戳綽疵茨磁雌辭慈瓷詞此刺賜次聰蔥囪匆從叢湊粗醋簇促躥篡竄摧崔催脆瘁粹淬翠村存寸磋撮搓措挫錯搭達答瘩打大呆歹傣戴帶殆代貸袋待逮怠耽擔丹單鄲撣膽旦氮但憚淡誕彈蛋當擋黨蕩檔刀搗蹈倒島禱導到稻悼道盜德得的蹬燈登等瞪凳鄧堤低滴迪敵笛狄滌翟嫡抵底地蒂第帝弟遞締顛掂滇碘點典靛墊電佃甸店惦奠淀殿碉叼雕凋刁掉吊釣調跌爹碟蝶迭諜疊丁盯叮釘頂鼎錠定訂丟東冬董懂動棟侗恫凍洞兜抖斗陡豆逗痘都督毒犢獨讀堵睹賭杜鍍肚度渡妒端短鍛段斷緞堆兌隊對墩噸蹲敦頓囤鈍盾遁掇哆多奪垛躲朵跺舵剁惰墮蛾峨鵝俄額訛娥惡厄扼遏鄂餓恩而兒耳爾餌洱二貳發罰筏伐乏閥法琺藩帆番翻樊礬釩繁凡煩反返范販犯飯泛坊芳方肪房防妨仿訪紡放菲非啡飛肥匪誹吠肺廢沸費芬酚吩氛分紛墳焚汾粉奮份忿憤糞豐封楓蜂峰鋒風瘋烽逢馮縫諷奉鳳佛否夫敷膚孵扶拂輻幅氟符伏俘服浮涪福袱弗甫撫輔俯釜斧脯腑府腐赴副覆賦復傅付阜父腹負富訃附婦縛咐噶嘎該改概鈣蓋溉干甘桿柑竿肝趕感稈敢贛岡剛鋼缸肛綱崗港杠篙皋高膏羔糕搞鎬稿告哥歌擱戈鴿胳疙割革葛格蛤閣隔鉻個各給根跟耕更庚羹埂耿梗工攻功恭龔供躬公宮弓鞏汞拱貢共鉤勾溝茍狗垢構購夠辜菇咕箍估沽孤姑鼓古蠱骨谷股故顧固雇刮瓜剮寡掛褂乖拐怪棺關官冠觀管館罐慣灌貫光廣逛瑰規圭硅歸龜閨軌鬼詭癸桂柜跪貴劊輥滾棍鍋郭國果裹過哈骸孩海氦亥害駭酣憨邯韓含涵寒函喊罕翰撼捍旱憾悍焊汗漢夯杭航壕嚎豪毫郝好耗號浩呵喝荷菏核禾和何合盒貉閡河涸赫褐鶴賀嘿黑痕很狠恨哼亨橫衡恒轟哄烘虹鴻洪宏弘紅喉侯猴吼厚候后呼乎忽瑚壺葫胡蝴狐糊湖弧虎唬護互滬戶花嘩華猾滑畫劃化話槐徊懷淮壞歡環桓還緩換患喚瘓豢煥渙宦幻荒慌黃磺蝗簧皇凰惶煌晃幌恍謊灰揮輝徽恢蛔回毀悔慧卉惠晦賄穢會燴匯諱誨繪葷昏婚魂渾混豁活伙火獲或惑霍貨禍擊圾基機畸稽積箕肌饑跡激譏雞姬績緝吉極棘輯籍集及急疾汲即嫉級擠幾脊己薊技冀季伎祭劑悸濟寄寂計記既忌際妓繼紀嘉枷夾佳家加莢頰賈甲鉀假稼價架駕嫁殲監堅尖箋間煎兼肩艱奸緘繭檢柬堿鹼揀撿簡儉剪減薦檻鑒踐賤見鍵箭件健艦劍餞漸濺澗建僵姜將漿江疆蔣槳獎講匠醬降蕉椒礁焦膠交郊澆驕嬌嚼攪鉸矯僥腳狡角餃繳絞剿教酵轎較叫窖揭接皆秸街階截劫節莖睛晶鯨京驚精粳經井警景頸靜境敬鏡徑痙靖竟競凈炯窘揪究糾玖韭久灸九酒廄救舊臼舅咎就疚鞠拘狙疽居駒菊局咀矩舉沮聚拒據巨具距踞鋸俱句懼炬劇捐鵑娟倦眷卷絹撅攫抉掘倔爵桔杰捷睫竭潔結解姐戒藉芥界借介疥誡屆巾筋斤金今津襟緊錦僅謹進靳晉禁近燼浸盡勁荊兢覺決訣絕均菌鈞軍君峻俊竣浚郡駿喀咖卡咯開揩楷凱慨刊堪勘坎砍看康慷糠扛抗亢炕考拷烤靠坷苛柯棵磕顆科殼咳可渴克刻客課肯啃墾懇坑吭空恐孔控摳口扣寇枯哭窟苦酷庫褲夸垮挎跨胯塊筷儈快寬款匡筐狂框礦眶曠況虧盔巋窺葵奎魁傀饋愧潰坤昆捆困括擴廓闊垃拉喇蠟臘辣啦萊來賴藍婪欄攔籃闌蘭瀾讕攬覽懶纜爛濫瑯榔狼廊郎朗浪撈勞牢老佬姥酪烙澇勒樂雷鐳蕾磊累儡壘擂肋類淚棱楞冷厘梨犁黎籬貍離漓理李里鯉禮莉荔吏栗麗厲勵礫歷利傈例俐痢立粒瀝隸力璃哩倆聯蓮連鐮廉憐漣簾斂臉鏈戀煉練糧涼梁粱良兩輛量晾亮諒撩聊僚療燎寥遼潦了撂鐐廖料列裂烈劣獵琳林磷霖臨鄰鱗淋凜賃吝拎玲菱零齡鈴伶羚凌靈陵嶺領另令溜琉榴硫餾留劉瘤流柳六龍聾嚨籠窿隆壟攏隴樓婁摟簍漏陋蘆盧顱廬爐擄鹵虜魯麓碌露路賂鹿潞祿錄陸戮驢呂鋁侶旅履屢縷慮氯律率濾綠巒攣孿灤卵亂掠略掄輪倫侖淪綸論蘿螺羅邏鑼籮騾裸落洛駱絡媽麻瑪碼螞馬罵嘛嗎埋買麥賣邁脈瞞饅蠻滿蔓曼慢漫謾芒茫盲氓忙莽貓茅錨毛矛鉚卯茂冒帽貌貿么玫枚梅酶霉煤沒眉媒鎂每美昧寐妹媚門悶們萌蒙檬盟錳猛夢孟瞇醚靡糜迷謎彌米秘覓泌蜜密冪棉眠綿冕免勉娩緬面苗描瞄藐秒渺廟妙蔑滅民抿皿敏憫閩明螟鳴銘名命謬摸摹蘑模膜磨摩魔抹末莫墨默沫漠寞陌謀牟某拇牡畝姆母墓暮幕募慕木目睦牧穆拿哪吶鈉那娜納氖乃奶耐奈南男難囊撓腦惱鬧淖呢餒內嫩能妮霓倪泥尼擬你匿膩逆溺蔫拈年碾攆捻念娘釀鳥尿捏聶孽嚙鑷鎳涅您檸獰凝寧擰濘牛扭鈕紐膿濃農弄奴努怒女暖虐瘧挪懦糯諾哦歐鷗毆藕嘔偶漚啪趴爬帕怕琶拍排牌徘湃派攀潘盤磐盼畔判叛乓龐旁耪胖拋咆刨炮袍跑泡呸胚培裴賠陪配佩沛噴盆砰抨烹澎彭蓬棚硼篷膨朋鵬捧碰坯砒霹批披劈琵毗啤脾疲皮匹痞僻屁譬篇偏片騙飄漂瓢票撇瞥拼頻貧品聘乒坪蘋萍平憑瓶評屏坡潑頗婆破魄迫粕剖撲鋪仆莆葡菩蒲埔樸圃普浦譜曝瀑期欺棲戚妻七凄漆柒沏其棋奇歧畦崎臍齊旗祈祁騎起豈乞企啟契砌器氣迄棄汽泣訖掐洽牽扦釬鉛千遷簽仟謙乾黔錢鉗前潛遣淺譴塹嵌欠歉槍嗆腔羌墻薔強搶橇鍬敲悄橋瞧喬僑巧鞘撬翹峭俏竅切茄且怯竊欽侵親秦琴勤芹擒禽寢沁青輕氫傾卿清擎晴氰情頃請慶瓊窮秋丘邱球求囚酋泅趨區蛆曲軀屈驅渠取娶齲趣去圈顴權醛泉全痊拳犬券勸缺炔瘸卻鵲榷確雀裙群然燃冉染瓤壤攘嚷讓饒擾繞惹熱壬仁人忍韌任認刃妊紉扔仍日戎茸蓉榮融熔溶容絨冗揉柔肉茹蠕儒孺如辱乳汝入褥軟阮蕊瑞銳閏潤若弱撒灑薩腮鰓塞賽三叁傘散桑嗓喪搔騷掃嫂瑟色澀森僧莎砂殺剎沙紗傻啥煞篩曬珊苫杉山刪煽衫閃陜擅贍膳善汕扇繕墑傷商賞晌上尚裳梢捎稍燒芍勺韶少哨邵紹奢賒蛇舌舍赦攝射懾涉社設砷申呻伸身深娠紳神沈審嬸甚腎慎滲聲生甥牲升繩省盛剩勝圣師失獅施濕詩尸虱十石拾時什食蝕實識史矢使屎駛始式示士世柿事拭誓逝勢是嗜噬適仕侍釋飾氏市恃室視試收手首守壽授售受瘦獸蔬樞梳殊抒輸叔舒淑疏書贖孰熟薯暑曙署蜀黍鼠屬術述樹束戍豎墅庶數漱恕刷耍摔衰甩帥栓拴霜雙爽誰水睡稅吮瞬順舜說碩朔爍斯撕嘶思私司絲死肆寺嗣四伺似飼巳松聳慫頌送宋訟誦搜艘擻嗽蘇酥俗素速粟僳塑溯宿訴肅酸蒜算雖隋隨綏髓碎歲穗遂隧祟孫損筍蓑梭唆縮瑣索鎖所塌他它她塔獺撻蹋踏胎苔抬臺泰酞太態汰坍攤貪癱灘壇檀痰潭譚談坦毯袒碳探嘆炭湯塘搪堂棠膛唐糖倘躺淌趟燙掏濤滔絳萄桃逃淘陶討套特藤騰疼謄梯剔踢銻提題蹄啼體替嚏惕涕剃屜天添填田甜恬舔腆挑條迢眺跳貼鐵帖廳聽烴汀廷停亭庭挺艇通桐酮瞳同銅彤童桶捅筒統痛偷投頭透凸禿突圖徒途涂屠土吐兔湍團推頹腿蛻褪退吞屯臀拖托脫鴕陀馱駝橢妥拓唾挖哇蛙洼娃瓦襪歪外豌彎灣玩頑丸烷完碗挽晚皖惋宛婉萬腕汪王亡枉網往旺望忘妄威巍微危韋違桅圍唯惟為濰維葦萎委偉偽尾緯未蔚味畏胃喂魏位渭謂尉慰衛瘟溫蚊文聞紋吻穩紊問嗡翁甕撾蝸渦窩我斡臥握沃巫嗚鎢烏污誣屋無蕪梧吾吳毋武五捂午舞伍侮塢戊霧晤物勿務悟誤昔熙析西硒矽晰嘻吸錫犧稀息希悉膝夕惜熄烯溪汐犀檄襲席習媳喜銑洗系隙戲細瞎蝦匣霞轄暇峽俠狹下廈夏嚇掀锨先仙鮮纖咸賢銜舷閑涎弦嫌顯險現獻縣腺餡羨憲陷限線相廂鑲香箱襄湘鄉翔祥詳想響享項巷橡像向象蕭硝霄削哮囂銷消宵淆曉小孝校肖嘯笑效楔些歇蝎鞋協挾攜邪斜脅諧寫械卸蟹懈泄瀉謝屑薪芯鋅欣辛新忻心信釁星腥猩惺興刑型形邢行醒幸杏性姓兄兇胸匈洶雄熊休修羞朽嗅銹秀袖繡墟戌需虛噓須徐許蓄酗敘旭序畜恤絮婿緒續軒喧宣懸旋玄選癬眩絢靴薛學穴雪血勛熏循旬詢尋馴巡殉汛訓訊遜迅壓押鴉鴨呀丫芽牙蚜崖衙涯雅啞亞訝焉咽閹煙淹鹽嚴研蜒巖延言顏閻炎沿奄掩眼衍演艷堰燕厭硯雁唁彥焰宴諺驗殃央鴦秧楊揚佯瘍羊洋陽氧仰癢養樣漾邀腰妖瑤搖堯遙窯謠姚咬舀藥要耀椰噎耶爺野冶也頁掖業葉曳腋夜液一壹醫揖銥依伊衣頤夷遺移儀胰疑沂宜姨彝椅蟻倚已乙矣以藝抑易邑屹億役臆逸肄疫亦裔意毅憶義益溢詣議誼譯異翼翌繹茵蔭因殷音陰姻吟銀淫寅飲尹引隱印英櫻嬰鷹應纓瑩螢營熒蠅迎贏盈影穎硬映喲擁傭臃癰庸雍踴蛹詠泳涌永恿勇用幽優悠憂尤由郵鈾猶油游酉有友右佑釉誘又幼迂淤于盂榆虞愚輿余俞逾魚愉渝漁隅予娛雨與嶼禹宇語羽玉域芋郁吁遇喻峪御愈欲獄育譽浴寓裕預豫馭鴛淵冤元垣袁原援轅園員圓猿源緣遠苑愿怨院曰約越躍鑰岳粵月悅閱耘云鄖勻隕允運蘊醞暈韻孕匝砸雜栽哉災宰載再在咱攢暫贊贓臟葬遭糟鑿藻棗早澡蚤躁噪造皂灶燥責擇則澤賊怎增憎曾贈扎喳渣札軋鍘閘眨柵榨咋乍炸詐摘齋宅窄債寨瞻氈詹粘沾盞斬輾嶄展蘸棧占戰站湛綻樟章彰漳張掌漲杖丈帳賬仗脹瘴障招昭找沼趙照罩兆肇召遮折哲蟄轍者鍺蔗這浙珍斟真甄砧臻貞針偵枕疹診震振鎮陣蒸掙睜征猙爭怔整拯正政幀癥鄭證芝枝支吱蜘知肢脂汁之織職直植殖執值侄址指止趾只旨紙志摯擲至致置幟峙制智秩稚質炙痔滯治窒中盅忠鐘衷終種腫重仲眾舟周州洲謅粥軸肘帚咒皺宙晝驟珠株蛛朱豬諸誅逐竹燭煮拄矚囑主著柱助蛀貯鑄筑住注祝駐抓爪拽專磚轉撰賺篆樁莊裝妝撞壯狀椎錐追贅墜綴諄準捉拙卓桌琢茁酌啄著灼濁茲咨資姿滋淄孜紫仔籽滓子自漬字鬃棕蹤宗綜總縱鄒走奏揍租足卒族祖詛阻組鉆纂嘴醉最罪尊遵昨左佐柞做作坐座'; var 正體中文 = '啊阿埃挨哎唉哀皚癌藹矮艾礙愛隘鞍氨安俺按暗岸胺案骯昂盎凹敖熬翺襖傲奧懊澳芭捌扒叭吧笆八疤巴拔跋靶把耙壩霸罷爸白柏百擺佰敗拜稗斑班搬扳般頒板版扮拌伴瓣半辦絆邦幫梆榜膀綁棒磅蚌鎊傍謗苞胞包褒剝薄雹保堡飽寶抱報暴豹鮑爆杯碑悲卑北輩背貝鋇倍狽備憊焙被奔苯本笨崩繃甭泵蹦迸逼鼻比鄙筆彼碧蓖蔽畢斃毖幣庇痹閉敝弊必辟壁臂避陛鞭邊編貶扁便變卞辨辯辮遍標彪膘表鼈憋別癟彬斌瀕濱賓擯兵冰柄丙秉餅炳病並玻菠播撥缽波博勃搏鉑箔伯帛舶脖膊渤泊駁捕蔔哺補埠不布步簿部怖擦猜裁材才財睬踩采彩菜蔡餐參蠶殘慚慘燦蒼艙倉滄藏操糙槽曹草廁策側冊測層蹭插叉茬茶查碴搽察岔差詫拆柴豺攙摻蟬饞讒纏鏟産闡顫昌猖場嘗常長償腸廠敞暢唱倡超抄鈔朝嘲潮巢吵炒車扯撤掣徹澈郴臣辰塵晨忱沈陳趁襯撐稱城橙成呈乘程懲澄誠承逞騁秤吃癡持匙池遲弛馳恥齒侈尺赤翅斥熾充衝沖蟲崇寵抽酬疇躊稠愁籌仇綢瞅醜臭初出櫥廚躇鋤雛滁除楚礎儲矗搐觸處揣川穿椽傳船喘串瘡窗幢床闖創吹炊捶錘垂春椿醇唇淳純蠢戳綽疵茨磁雌辭慈瓷詞此刺賜次聰蔥囪匆從叢湊粗醋簇促躥篡竄摧崔催脆瘁粹淬翠村存寸磋撮搓措挫錯搭達答瘩打大呆歹傣戴帶殆代貸袋待逮怠耽擔丹單鄲撣膽旦氮但憚淡誕彈蛋當擋黨蕩檔刀搗蹈倒島禱導到稻悼道盜德得的蹬燈登等瞪凳鄧堤低滴迪敵笛狄滌翟嫡抵底地蒂第帝弟遞締顛掂滇碘點典靛墊電佃甸店惦奠澱殿碉叼雕凋刁掉吊釣調跌爹碟蝶疊諜疊丁盯叮釘頂鼎錠定訂丟東冬董懂動棟侗恫凍洞兜抖鬥陡豆逗痘都督毒犢獨讀堵睹賭杜鍍肚度渡妒端短鍛段斷緞堆兌隊對墩噸蹲敦頓囤鈍盾遁掇哆多奪垛躲朵跺舵剁惰墮蛾峨鵝俄額訛娥惡厄扼遏鄂餓恩而兒耳爾餌洱二貳發罰筏伐乏閥法琺藩帆番翻樊礬釩繁凡煩反返範販犯飯泛坊芳方肪房防妨仿訪紡放菲非啡飛肥匪誹吠肺廢沸費芬酚吩氛分紛墳焚汾粉奮份忿憤糞豐封楓蜂峰鋒風瘋烽逢馮縫諷奉鳳佛否夫敷膚孵扶拂輻幅氟符伏俘服浮涪福袱弗甫撫輔俯釜斧脯腑府腐赴副覆賦複傅付阜父腹負富訃附婦縛咐噶嘎該改概鈣蓋溉幹甘桿柑竿肝趕感稈敢贛岡剛鋼缸肛綱崗港杠篙臯高膏羔糕搞鎬稿告哥歌擱戈鴿胳疙割革葛格蛤閣隔鉻個各給根跟耕更庚羹埂耿梗工攻功恭龔供躬公宮弓鞏汞拱貢共鈎勾溝茍狗垢構購夠辜菇咕箍估沽孤姑鼓古蠱骨谷股故顧固雇刮瓜剮寡掛褂乖拐怪棺關官冠觀管館罐慣灌貫光廣逛瑰規圭矽歸龜閨軌鬼詭癸桂櫃跪貴劊輥滾棍鍋郭國果裹過哈骸孩海氦亥害駭酣憨邯韓含涵寒函喊罕翰撼捍旱憾悍焊汗漢夯杭航壕嚎豪毫郝好耗號浩呵喝荷菏核禾和何合盒貉閡河涸赫褐鶴賀嘿黑痕很狠恨哼亨橫衡恒轟哄烘虹鴻洪宏弘紅喉侯猴吼厚候後呼乎忽瑚壺葫胡蝴狐糊湖弧虎唬護互滬戶花嘩華猾滑畫劃化話槐徊懷淮壞歡環桓還緩換患喚瘓豢煥渙宦幻荒慌黃磺蝗簧皇凰惶煌晃幌恍謊灰揮輝徽恢蛔回毀悔慧卉惠晦賄穢會燴彙諱誨繪葷昏婚魂渾混豁活夥火獲或惑霍貨禍擊圾基機畸稽積箕肌饑跡激譏雞姬績緝吉極棘輯籍集及急疾汲即嫉級擠幾脊己薊技冀季伎祭劑悸濟寄寂計記既忌際妓繼紀嘉枷夾佳家加莢頰賈甲鉀假稼價架駕嫁殲監堅尖箋間煎兼肩艱奸緘繭檢柬堿鹼揀撿簡儉剪減薦檻鑒踐賤見鍵箭件健艦劍餞漸濺澗建僵姜將漿江疆蔣槳獎講匠醬降蕉椒礁焦膠交郊澆驕嬌嚼攪鉸矯僥腳狡角餃繳絞剿教酵轎較叫窖揭接皆稭街階截劫節莖睛晶鯨京驚精粳經井警景頸靜境敬鏡徑痙靖竟競淨炯窘揪究糾玖韭久灸九酒廄救舊臼舅咎就疚鞠拘狙疽居駒菊局咀矩舉沮聚拒據巨具距踞鋸俱句懼炬劇捐鵑娟倦眷卷絹撅攫抉掘倔爵桔傑捷睫竭潔結解姐戒藉芥界借介疥誡屆巾筋斤金今津襟緊錦僅謹進靳晉禁近燼浸盡勁荊兢覺決訣絕均菌鈞軍君峻俊竣浚郡駿喀咖卡咯開揩楷凱慨刊堪勘坎砍看康慷糠扛抗亢炕考拷烤靠坷苛柯棵磕顆科殼咳可渴克刻客課肯啃墾懇坑吭空恐孔控摳口扣寇枯哭窟苦酷庫褲誇垮挎跨胯塊筷儈快寬款匡筐狂框礦眶曠況虧盔巋窺葵奎魁傀饋愧潰坤昆捆困括擴廓闊垃拉喇蠟臘辣啦萊來賴藍婪欄攔籃闌蘭瀾讕攬覽懶纜爛濫瑯榔狼廊郎朗浪撈勞牢老佬姥酪烙澇勒樂雷鐳蕾磊累儡壘擂肋類淚棱楞冷厘梨犁黎籬貍離漓理李裏鯉禮莉荔吏栗麗厲勵礫曆利傈例俐痢立粒瀝隸力璃哩倆聯蓮連鐮廉憐漣簾斂臉鏈戀煉練糧涼梁粱良兩輛量晾亮諒撩聊僚療燎寥遼潦了撂鐐廖料列裂烈劣獵琳林磷霖臨鄰鱗淋凜賃吝拎玲菱零齡鈴伶羚淩靈陵嶺領另令溜琉榴硫餾留劉瘤流柳六龍聾嚨籠窿隆壟攏隴樓婁摟簍漏陋蘆盧顱廬爐擄鹵虜魯麓碌露路賂鹿潞祿錄陸戮驢呂鋁侶旅履屢縷慮氯律率濾綠巒攣孿灤卵亂掠略掄輪倫侖淪綸論蘿螺羅邏鑼籮騾裸落洛駱絡媽麻瑪碼螞馬罵嘛嗎埋買麥賣邁脈瞞饅蠻滿蔓曼慢漫謾芒茫盲氓忙莽貓茅錨毛矛鉚卯茂冒帽貌貿麼玫枚梅酶黴煤沒眉媒鎂每美昧寐妹媚門悶們萌蒙檬盟錳猛夢孟瞇醚靡糜迷謎彌米秘覓泌蜜密冪棉眠綿冕免勉娩緬面苗描瞄藐秒渺廟妙蔑滅民抿皿敏憫閩明螟鳴銘名命謬摸摹蘑模膜磨摩魔抹末莫墨默沫漠寞陌謀牟某拇牡畝姆母墓暮幕募慕木目睦牧穆拿哪吶鈉那娜納氖乃奶耐奈南男難囊撓腦惱鬧淖呢餒內嫩能妮霓倪泥尼擬你匿膩逆溺蔫拈年碾攆撚念娘釀鳥尿捏聶孽齧鑷鎳涅您檸獰凝甯擰濘牛扭鈕紐膿濃農弄奴努怒女暖虐瘧挪懦糯諾哦歐鷗毆藕嘔偶漚啪趴爬帕怕琶拍排牌徘湃派攀潘盤磐盼畔判叛乓龐旁耪胖拋咆刨炮袍跑泡呸胚培裴賠陪配佩沛噴盆砰抨烹澎彭蓬棚硼篷膨朋鵬捧碰坯砒霹批披劈琵毗啤脾疲皮匹痞僻屁譬篇偏片騙飄漂瓢票撇瞥拼頻貧品聘乒坪蘋萍平憑瓶評屏坡潑頗婆破魄迫粕剖撲鋪仆莆葡菩蒲埔樸圃普浦譜曝瀑期欺棲戚妻七淒漆柒沏其棋奇歧畦崎臍齊旗祈祁騎起豈乞企啓契砌器氣迄棄汽泣訖掐洽牽扡釺鉛千遷簽仟謙乾黔錢鉗前潛遣淺譴塹嵌欠歉槍嗆腔羌牆薔強搶橇鍬敲悄橋瞧喬僑巧鞘撬翹峭俏竅切茄且怯竊欽侵親秦琴勤芹擒禽寢沁青輕氫傾卿清擎晴氰情頃請慶瓊窮秋丘邱球求囚酋泅趨區蛆曲軀屈驅渠取娶齲趣去圈顴權醛泉全痊拳犬券勸缺炔瘸卻鵲榷確雀裙群然燃冉染瓤壤攘嚷讓饒擾繞惹熱壬仁人忍韌任認刃妊紉扔仍日戎茸蓉榮融熔溶容絨冗揉柔肉茹蠕儒孺如辱乳汝入褥軟阮蕊瑞銳閏潤若弱撒灑薩腮鰓塞賽三三傘散桑嗓喪搔騷掃嫂瑟色澀森僧莎砂殺剎沙紗傻啥煞篩曬珊苫杉山刪煽衫閃陝擅贍膳善汕扇繕墑傷商賞晌上尚裳梢捎稍燒芍勺韶少哨邵紹奢賒蛇舌舍赦攝射懾涉社設砷申呻伸身深娠紳神沈審嬸甚腎慎滲聲生甥牲升繩省盛剩勝聖師失獅施濕詩屍虱十石拾時什食蝕實識史矢使屎駛始式示士世柿事拭誓逝勢是嗜噬適仕侍釋飾氏市恃室視試收手首守壽授售受瘦獸蔬樞梳殊抒輸叔舒淑疏書贖孰熟薯暑曙署蜀黍鼠屬術述樹束戍豎墅庶數漱恕刷耍摔衰甩帥栓拴霜雙爽誰水睡稅吮瞬順舜說碩朔爍斯撕嘶思私司絲死肆寺嗣四伺似飼巳松聳慫頌送宋訟誦搜艘擻嗽蘇酥俗素速粟僳塑溯宿訴肅酸蒜算雖隋隨綏髓碎歲穗遂隧祟孫損筍蓑梭唆縮瑣索鎖所塌他它她塔獺撻蹋踏胎苔擡臺泰酞太態汰坍攤貪癱灘壇檀痰潭譚談坦毯袒碳探歎炭湯塘搪堂棠膛唐糖倘躺淌趟燙掏濤滔縧萄桃逃淘陶討套特藤騰疼謄梯剔踢銻提題蹄啼體替嚏惕涕剃屜天添填田甜恬舔腆挑條迢眺跳貼鐵帖廳聽烴汀廷停亭庭挺艇通桐酮瞳同銅彤童桶捅筒統痛偷投頭透凸禿突圖徒途塗屠土吐兔湍團推頹腿蛻褪退吞屯臀拖托脫鴕陀馱駝橢妥拓唾挖哇蛙窪娃瓦襪歪外豌彎灣玩頑丸烷完碗挽晚皖惋宛婉萬腕汪王亡枉網往旺望忘妄威巍微危韋違桅圍唯惟爲濰維葦萎委偉僞尾緯未蔚味畏胃餵魏位渭謂尉慰衛瘟溫蚊文聞紋吻穩紊問嗡翁甕撾蝸渦窩我斡臥握沃巫嗚鎢烏汙誣屋無蕪梧吾吳毋武五捂午舞伍侮塢戊霧晤物勿務悟誤昔熙析西硒矽晰嘻吸錫犧稀息希悉膝夕惜熄烯溪汐犀檄襲席習媳喜銑洗系隙戲細瞎蝦匣霞轄暇峽俠狹下廈夏嚇掀鍁先仙鮮纖鹹賢銜舷閑涎弦嫌顯險現獻縣腺餡羨憲陷限線相廂鑲香箱襄湘鄉翔祥詳想響享項巷橡像向象蕭硝霄削哮囂銷消宵淆曉小孝校肖嘯笑效楔些歇蠍鞋協挾攜邪斜脅諧寫械卸蟹懈泄瀉謝屑薪芯鋅欣辛新忻心信釁星腥猩惺興刑型形邢行醒幸杏性姓兄兇胸匈洶雄熊休修羞朽嗅鏽秀袖繡墟戌需虛噓須徐許蓄酗敘旭序畜恤絮婿緒續軒喧宣懸旋玄選癬眩絢靴薛學穴雪血勳熏循旬詢尋馴巡殉汛訓訊遜迅壓押鴉鴨呀丫芽牙蚜崖衙涯雅啞亞訝焉咽閹煙淹鹽嚴研蜒巖延言顔閻炎沿奄掩眼衍演豔堰燕厭硯雁唁彥焰宴諺驗殃央鴦秧楊揚佯瘍羊洋陽氧仰癢養樣漾邀腰妖瑤搖堯遙窯謠姚咬舀藥要耀椰噎耶爺野冶也頁掖業葉曳腋夜液一壹醫揖銥依伊衣頤夷遺移儀胰疑沂宜姨彜椅蟻倚已乙矣以藝抑易邑屹億役臆逸肄疫亦裔意毅憶義益溢詣議誼譯異翼翌繹茵蔭因殷音陰姻吟銀淫寅飲尹引隱印英櫻嬰鷹應纓瑩螢營熒蠅迎贏盈影穎硬映喲擁傭臃癰庸雍踴蛹詠泳湧永恿勇用幽優悠憂尤由郵鈾猶油遊酉有友右佑釉誘又幼迂淤于盂榆虞愚輿余俞逾魚愉渝漁隅予娛雨與嶼禹宇語羽玉域芋郁籲遇喻峪禦愈欲獄育譽浴寓裕預豫馭鴛淵冤元垣袁原援轅園員圓猿源緣遠苑願怨院曰約越躍鑰嶽粵月悅閱耘雲鄖勻隕允運蘊醞暈韻孕匝砸雜栽哉災宰載再在咱攢暫贊贓髒葬遭糟鑿藻棗早澡蚤躁噪造皂竈燥責擇則澤賊怎增憎曾贈紮喳渣劄軋鍘閘眨柵榨咋乍炸詐摘齋宅窄債寨瞻氈詹粘沾盞斬輾嶄展蘸棧占戰站湛綻樟章彰漳張掌漲杖丈帳賬仗脹瘴障招昭找沼趙照罩兆肇召遮折哲蟄轍者鍺蔗這浙珍斟真甄砧臻貞針偵枕疹診震振鎮陣蒸掙睜征猙爭怔整拯正政幀癥鄭證芝枝支吱蜘知肢脂汁之織職直植殖執值侄址指止趾只旨紙志摯擲至致置幟峙制智秩稚質炙痔滯治窒中盅忠鍾衷終種腫重仲衆舟周州洲謅粥軸肘帚咒皺宙晝驟珠株蛛朱豬諸誅逐竹燭煮拄矚囑主著柱助蛀貯鑄築住注祝駐抓爪拽專磚轉撰賺篆樁莊裝妝撞壯狀椎錐追贅墜綴諄準捉拙卓桌琢茁酌啄著灼濁茲咨資姿滋淄孜紫仔籽滓子自漬字鬃棕蹤宗綜總縱鄒走奏揍租足卒族祖詛阻組鑽纂嘴醉最罪尊遵昨左佐柞做作坐座'; /** * 默認是否正體中文:true為正體;false簡體。 HTTP * 頭讀取:Request.ServerVariables("http_accept_language") * * @return {Boolean} */ function getClientLanguage() { var s = navigator.userLanguage || navigator.language; switch (s.toLowerCase()) { case 'zh-cn': return false; case 'zh-tw': return true; default: return null; } } /** * 轉換對象,使用遞歸,逐層剝到文本 * @param {HTMLElement} obj 從document.body開始, */ function translate(el, coverntFn){ el = el.childNodes; var node; for (var i = 0, j = el.length; i < j; i++) { node = el.item(i); // || (node == $$.big5.el) if (("||BR|HR|TEXTAREA|".indexOf("|" + node.tagName + "|")) > 0)continue; if(node.title){ node.title = coverntFn(node.title); }else if(node.alt){ node.alt = coverntFn(node.alt); }else if(node.tagName == "INPUT" && node.value != "" && node.type != "text" && node.type != "hidden"){ node.value = coverntFn(node.value); }else if(node.nodeType == 3){ node.data = coverntFn(node.data); }else{ arguments.callee(node, coverntFn); } } } function translateText(text, isBig5) { var str = [], _char, charIndex, result; for (var i = 0, j = text.length; i < j; i++) { _char = text.charAt(i); charIndex = isBig5 ? 簡化中文.indexOf(_char) : 正體中文.indexOf(_char); result = isBig5 ? 正體中文.charAt(charIndex) : 簡化中文.charAt(charIndex); str.push(charIndex != -1 ? result : _char); } return str.join(''); } var traditionalized = translateText.delegate(null, true), simplized = translateText.delegate(null, false); var cookieName = 'ChineseType'; // 轉換為正體中文(繁體中文) window.toChinese = function(el) { el.addCls('selected'); document.querySelector(".simpleChinese").removeCls('selected'); translate(document.body, traditionalized) Cookie.set(cookieName, true); } // 轉換為簡體中文 window.toSimpleChinese = function(el) { el.addCls('selected'); document.querySelector(".Chinese").removeCls('selected'); translate(document.body, simplized); Cookie.set(cookieName, false); } var Cookie = { set: function (name, val) { var exp = new Date(); exp.setDate(exp.getDate() + 600 * 1000); document.cookie = name + "=" + escape(val) + ";expires=" + exp.toGMTString(); }, del: function (name) { document.cookie = name + "=;expires=" + (new Date(0)).toGMTString(); }, get: function (name) { var cookieArray = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)")); if (cookieArray != null) return unescape(cookieArray[2]); else return null; } }; var value = Cookie.get(cookieName); if( value == null && getClientLanguage() || value == 'true') { // 沒有檢查出 cookie 則為第一次運行。 // cookie保留了用戶選擇的正體,將原來的簡化字轉換為默認的正體! window.toChinese(document.querySelector(".Chinese")); } else if(value == null && !getClientLanguage() || value == 'false') { // 雖然第一次運行,但對方正是簡體中文,不用轉。 // 原來已經是簡體,無須轉換。 } })();
然后綁定按鈕事件,我們簡單地使用 onclick 綁定來全局函數 window.toChinese(el)、window.toSimpleChinese(el)。el 是按鈕元素。
// 轉換為正體中文(繁體中文) window.toChinese = function(el) { el.addCls('selected'); document.querySelector(".simpleChinese").removeCls('selected'); translate(document.body, traditionalized) Cookie.set(cookieName, true); } // 轉換為簡體中文 window.toSimpleChinese = function(el) { el.addCls('selected'); document.querySelector(".Chinese").removeCls('selected'); translate(document.body, simplized); Cookie.set(cookieName, false); }
js 代碼中比較長的兩個字符串變量分別是簡體中文和繁體中文,他們之間的切換沒有什么神奇之處,只是所有文本經過這些變量一一對應即可。當然這里只是常用的繁簡對應,而且是機械的,有些特殊的翻譯需要聯系上下文的,那種高級轉換不在此討論之列。
首先我們看看變量函數 translate,執行它會通過遞歸遍歷完整個 DOM(可以說比較笨的方法)
/** * 轉換對象,使用遞歸,逐層剝到文本 * @param {HTMLElement} obj 從document.body開始, */ function translate(el, coverntFn){ el = el.childNodes; var node; for (var i = 0, j = el.length; i < j; i++) { node = el.item(i); // || (node == $$.big5.el) if (("||BR|HR|TEXTAREA|".indexOf("|" + node.tagName + "|")) > 0)continue; if(node.title){ node.title = coverntFn(node.title); }else if(node.alt){ node.alt = coverntFn(node.alt); }else if(node.tagName == "INPUT" && node.value != "" && node.type != "text" && node.type != "hidden"){ node.value = coverntFn(node.value); }else if(node.nodeType == 3){ node.data = coverntFn(node.data); }else{ arguments.callee(node, coverntFn); } } }
轉換 DOM 內容,我們第一時間想到的可能是 el.innerHTML,但其實大可不必,而且遞歸的時候 innerHTML 不太方便。不妨再想想看,我們其實只需要對文本轉換即可,DOM API 能夠判斷 文本節點(node.nodeType == 3),然后可讀可寫的 data 屬性就是設置文本的(textNode.data),由此我們就可以完成到繁體的切換。節點其他屬性 title、alt、value 亦是同樣原理。另外,if (("||BR|HR|TEXTAREA|".indexOf("|" + node.tagName + "|")) > 0)continue; 是排除某些節點的方法,其思路比較特別,可以多觀摩下。
至于 coverntFn 是一函數參數。通過根函數 translateText(text, isBig5) 新生成兩個函數 :var traditionalized = translateText.delegate(null, true), simplized = translateText.delegate(null, false);
function translateText(text, isBig5) { var str = [], _char, charIndex, result; for (var i = 0, j = text.length; i < j; i++) { _char = text.charAt(i); charIndex = isBig5 ? 簡化中文.indexOf(_char) : 正體中文.indexOf(_char); result = isBig5 ? 正體中文.charAt(charIndex) : 簡化中文.charAt(charIndex); str.push(charIndex != -1 ? result : _char); } return str.join(''); } var traditionalized = translateText.delegate(null, true), simplized = translateText.delegate(null, false);
Function.prototype.delegate() 方法如下。
/** * 函數委托 參見 http://blog.csdn.net/zhangxin09/article/details/8508128 * @return {Function} */ Function.prototype.delegate = function () { var self = this, scope = this.scope, args = arguments, aLength = arguments.length, fnToken = 'function'; return function(){ var bLength = arguments.length, Length = (aLength > bLength) ? aLength : bLength; // mission one: for (var i = 0; i < Length; i++) if (arguments[i])args[i] = arguments[i]; // 拷貝參數 args.length = Length; // 在 MS jscript下面,arguments作為數字來使用還是有問題,就是length不能自動更新。修正如左: // mission two: for (var i = 0, j = args.length; i < j; i++) { var _arg = args[i]; if (_arg && typeof _arg == fnToken && _arg.late == true) args[i] = _arg.apply(scope || this, args); } return self.apply(scope || this, args); }; };
繁簡的轉換基本如此了。值得一提的是,瀏覽器其實替我們識別語言環境的變量,主要是 navigator.userLanguage || navigator.language。這個變量在服務端也可以獲取,頭讀取:Request.ServerVariables("http_accept_language")。
/** * 默認是否正體中文:true為正體;false簡體。 HTTP * 頭讀取:Request.ServerVariables("http_accept_language") * * @return {Boolean} */ function getClientLanguage() { var s = navigator.userLanguage || navigator.language; switch (s.toLowerCase()) { case 'zh-cn': return false; case 'zh-tw': return true; default: return null; } }
最后我們還通過 cookie 來保存語言狀態。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。