新聞中心
大家好,我是前端西瓜哥,是一名前端開(kāi)發(fā)。

創(chuàng)新互聯(lián)擁有十多年成都網(wǎng)站建設(shè)工作經(jīng)驗(yàn),為各大企業(yè)提供成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)服務(wù),對(duì)于網(wǎng)頁(yè)設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、重慶App定制開(kāi)發(fā)、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、程序開(kāi)發(fā)、網(wǎng)站優(yōu)化(SEO優(yōu)化)、微網(wǎng)站、申請(qǐng)域名等,憑借多年來(lái)在互聯(lián)網(wǎng)的打拼,我們?cè)诨ヂ?lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了很多網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、網(wǎng)絡(luò)營(yíng)銷經(jīng)驗(yàn),集策劃、開(kāi)發(fā)、設(shè)計(jì)、營(yíng)銷、管理等網(wǎng)站化運(yùn)作于一體,具備承接各種規(guī)模類型的網(wǎng)站建設(shè)項(xiàng)目的能力。
最近做了一個(gè)將按需加載語(yǔ)言包的需求,有不少收獲,這里記錄一下。
改造前的項(xiàng)目
原來(lái)項(xiàng)目是將所有的語(yǔ)言包合并在一起,放到一個(gè) JSON 文件里然后被引入。
打包后的腳本里,有完整的語(yǔ)言包的代碼,導(dǎo)致打包文件非常大。理論上用戶只會(huì)使用一種語(yǔ)言,其他的語(yǔ)言沒(méi)有加載的必要。
目前來(lái)說(shuō)項(xiàng)目只支持兩種語(yǔ)言,每個(gè)語(yǔ)言有文案 4000 多條。如果還是使用全量加載的話,以后支持的語(yǔ)言每多一個(gè),打包后的文件就要膨脹一圈。
做語(yǔ)言包的拆分還是很有必要的。它可以減少加載資源的大小,減少首次頁(yè)面加載時(shí)間,提高用戶體驗(yàn)。
實(shí)現(xiàn)方案的選擇
實(shí)現(xiàn)按需加載語(yǔ)言包的方式很多,我了解到的有三種:
- 后端渲染:在請(qǐng)求時(shí)將單個(gè)語(yǔ)言包嵌入到 HTML 里。
- 動(dòng)態(tài) import:使用 ES6的 動(dòng)態(tài) import 語(yǔ)法。
- 動(dòng)態(tài)腳本:在腳本里創(chuàng)建一個(gè) script,添加到 DOM 樹(shù)上。
后端渲染的方案,其實(shí)是最快捷的
// 下面這一個(gè) script 是后端渲染的
請(qǐng)求 HTML 時(shí),后端做渲染工作,給 HTML 加上語(yǔ)言包的內(nèi)容。
前端沒(méi)有什么改造的工作量,但問(wèn)題是不能利用緩存。但這個(gè)問(wèn)題其實(shí)也可以解決,就是后端生成好語(yǔ)言包 js 文件,將嵌入語(yǔ)言包內(nèi)容的方式改為 cdn 引入的方式,可以利用好緩存。
但這讓模板引擎的邏輯變得很重,cdn 上傳到哪里,如何維護(hù)也是個(gè)問(wèn)題。
動(dòng)態(tài) import 方案
import('lang/zh-CN.js').then(() => {
ReactDOM.render();
});使用 React 等框架打包出來(lái)單頁(yè)面應(yīng)用的文件通常很大,下載需要不少時(shí)間。
動(dòng)態(tài) import 必須在腳本整個(gè)下載完后,再執(zhí)行,所以這是一個(gè)串行下載的邏輯。
如果可以的話,我們希望語(yǔ)言包可以和業(yè)務(wù)代碼同時(shí)下載。此外,更重要的一點(diǎn)是,在動(dòng)態(tài) import 前,我們不能調(diào)用獲取文案的方法 getText。
我在改造項(xiàng)目代碼時(shí),發(fā)現(xiàn)在我動(dòng)態(tài) import 語(yǔ)言包并 ReactDOM.render() 之前,有些模塊文件調(diào)用了getText 方法,因?yàn)樗鼈冏鳛槊杜e指直接暴露出來(lái),沒(méi)有用函數(shù)封裝,被 import 時(shí)就直接執(zhí)行了。
語(yǔ)言包都沒(méi)加載,你執(zhí)行 getText 是拿不到文案的,這個(gè)方案我果斷放棄。
動(dòng)態(tài)腳本方案
這種方案利用了腳本里創(chuàng)建腳本的方式。能在更前面的位置加載語(yǔ)言包腳本。
優(yōu)點(diǎn)是我們可以不需要做后端渲染的工作,讓選擇語(yǔ)言包的邏輯交給前端。但涉及到前端工程化,需要寫(xiě)插件改變?cè)瓉?lái)的加載腳本形式。
我們的項(xiàng)目使用了 webpack,如果用這個(gè)方案,就需要寫(xiě)一個(gè) webpack 插件去改造 HtmlWebpackPlugin 的構(gòu)建流程。
目前來(lái)說(shuō),方案 1 和 方案 3 都是不錯(cuò)的。
但考慮到我們公司的前后端是分離的,后端的代碼實(shí)現(xiàn)對(duì)我來(lái)說(shuō)其實(shí)是黑盒,我沒(méi)有權(quán)限也沒(méi)有能力去寫(xiě)后端代碼。而項(xiàng)目是前端項(xiàng)目,最好還是讓前端來(lái)掌控維護(hù)。所以我最終選擇了方案 3。
方案1 和方案 2 的更具體介紹,可以看我的這篇文章:前端國(guó)際化,該如何實(shí)現(xiàn)按需加載語(yǔ)言包?
改造過(guò)程
原來(lái)項(xiàng)目打包后的 html 文件大致如下。
Document
app.js 里有全量語(yǔ)言包的內(nèi)容。
改造后的 html 文件如下:
Document
我們語(yǔ)言包將 app.js 從中提取出來(lái),并且分為多個(gè)語(yǔ)言包放到 js 文件,如 zh-CN.js、en-US.js,在 app.js 之前執(zhí)行。
let lang = getLang();
const script = document.createElement('script');
script.async = false;
script.src = i18nLangCDNs[lang];
document.querySelector('head').appendChild(script);
我們先確認(rèn)用戶使用的語(yǔ)言是什么。
如果我們不支持持久化設(shè)置,可以通過(guò) navigator.language 或前端的其他地方獲取。
但通常用戶可以設(shè)置語(yǔ)言,這個(gè)語(yǔ)言標(biāo)識(shí)就要后端給,再請(qǐng)求一次用戶信息可太離譜了,所以這里還是需要后端給我們往 html 里嵌入用戶選擇的語(yǔ)言。然后我們從語(yǔ)言 cdn 列表里選我們需要的語(yǔ)言。
script 元素默認(rèn)會(huì)將 async 設(shè)置為 true,效果是腳本下載完立即執(zhí)行。需要將其改為 false,保證多個(gè)動(dòng)態(tài)腳本順序執(zhí)行。
文件名使用了哈希,是為了解決瀏覽器緩存問(wèn)題。
執(zhí)行后,就會(huì)將語(yǔ)言包文案暴露在全局變量中。
業(yè)務(wù)代碼 app.js 也得改成動(dòng)態(tài)加載形式,如果原來(lái)的非動(dòng)態(tài)寫(xiě)法,執(zhí)行時(shí)機(jī)就會(huì)早于語(yǔ)言包腳本。
這里涉及到了 script 的執(zhí)行時(shí)機(jī),具體規(guī)則可以看我的這篇文章:script 的三種加載模式:默認(rèn)加載、defer、async。
原來(lái)的寫(xiě)法
改造后
這樣我們就能保證先執(zhí)行語(yǔ)言包腳本,再執(zhí)行 app.js。
app.js 中的業(yè)務(wù)代碼執(zhí)行時(shí),使用 getText 方法就能正常通過(guò) key 獲取到對(duì)應(yīng)的文案。
這里 app.js 改為動(dòng)態(tài)的寫(xiě)法后,需要腳本解析執(zhí)行后才下載腳本,可以考慮加個(gè) link preload 提前下載腳本。
link 的 preload 作用可以看我的這篇文章。
期間遇到的問(wèn)題
思路并不復(fù)雜,但改造過(guò)程中做了很多工作,遇到了不少問(wèn)題。這里簡(jiǎn)單列舉一下,不展開(kāi)講了,到時(shí)候會(huì)考慮另寫(xiě)文章討論。
- 我們項(xiàng)目的語(yǔ)言包是維護(hù)在在線表格上的,每次會(huì)通過(guò)腳本拉取數(shù)據(jù),然后處理成 JSON 文件。我需要再寫(xiě)一個(gè)腳本來(lái)處理這個(gè) JSON 文件,將其分成多個(gè)語(yǔ)言包,并生成功 TS 類型文件。
- 使用了 monorepo,我專門(mén)分了一個(gè) i18n 的包。
- 最難的是開(kāi)發(fā)一個(gè) Webpack 插件,需要做到拷貝特定文件夾下的語(yǔ)言包,加上內(nèi)容哈希,放到構(gòu)建目錄下。這些帶有哈希的名字要保存下來(lái),通過(guò) HtmlWebpack的鉤子轉(zhuǎn)換為內(nèi)嵌 script 形式添加到 html 上。此外,還要將原來(lái)的打包文件 app.js 轉(zhuǎn)換為動(dòng)態(tài)加載的形式。
- 開(kāi)發(fā)環(huán)境還是要全量加載語(yǔ)言包,方便測(cè)試。一個(gè)原因是 devServer 無(wú)法讀取到使用 copy 的文件,需要額外用 write-file-webpack-plugin,但項(xiàng)目用的 create-react-app 不太好改造。
- 改造 getText 獲取文案的方法,需要考慮開(kāi)發(fā)和生產(chǎn)環(huán)境的不同。
- 我們還有個(gè)中間層的 nextjs 項(xiàng)目,我們的語(yǔ)言包要兼容該項(xiàng)目,所以里面還寫(xiě)了判斷環(huán)境的邏輯,在 global 或 window 上掛載全局變量。
- 測(cè)試用例和 CI 補(bǔ)上一行引入語(yǔ)言包的邏輯。
當(dāng)前文章:項(xiàng)目復(fù)盤(pán):通過(guò)動(dòng)態(tài)腳本,實(shí)現(xiàn)按需加載語(yǔ)言包
網(wǎng)頁(yè)鏈接:http://m.fisionsoft.com.cn/article/cdcsijd.html


咨詢
建站咨詢
