项目复盘:通过动态脚本,实现按需加载语言包
csdh11 2024-12-02 16:39 4 浏览
大家好,我是前端西瓜哥,是一名前端开发。
最近做了一个将按需加载语言包的需求,有不少收获,这里记录一下。
改造前的项目
原来项目是将所有的语言包合并在一起,放到一个 JSON 文件里然后被引入。
打包后的脚本里,有完整的语言包的代码,导致打包文件非常大。理论上用户只会使用一种语言,其他的语言没有加载的必要。
目前来说项目只支持两种语言,每个语言有文案 4000 多条。如果还是使用全量加载的话,以后支持的语言每多一个,打包后的文件就要膨胀一圈。
做语言包的拆分还是很有必要的。它可以减少加载资源的大小,减少首次页面加载时间,提高用户体验。
实现方案的选择
实现按需加载语言包的方式很多,我了解到的有三种:
- 后端渲染:在请求时将单个语言包嵌入到 HTML 里
- 动态 import:使用 ES6的 动态 import 语法
- 动态脚本:在脚本里创建一个 script,添加到 DOM 树上
后端渲染的方案,其实是最快捷的
// 下面这一个 script 是后端渲染的
<script>window.i18n = { 'apple': '苹果' /* ... */ }</script>
<script src="app.js"></script>
请求 HTML 时,后端做渲染工作,给 HTML 加上语言包的内容。
前端没有什么改造的工作量,但问题是不能利用缓存。但这个问题其实也可以解决,就是后端生成好语言包 js 文件,将嵌入语言包内容的方式改为 cdn 引入的方式,可以利用好缓存。
但这让模板引擎的逻辑变得很重,cdn 上传到哪里,如何维护也是个问题。
动态 import 方案
import('lang/zh-CN.js').then(() => {
ReactDOM.render();
});
使用 React 等框架打包出来单页面应用的文件通常很大,下载需要不少时间。
动态 import 必须在脚本整个下载完后,再执行,所以这是一个串行下载的逻辑。
如果可以的话,我们希望语言包可以和业务代码同时下载。此外,更重要的一点是,在动态 import 前,我们不能调用获取文案的方法 getText。
我在改造项目代码时,发现在我动态 import 语言包并 ReactDOM.render() 之前,有些模块文件调用了getText 方法,因为它们作为枚举指直接暴露出来,没有用函数封装,被 import 时就直接执行了。
语言包都没加载,你执行 getText 是拿不到文案的,这个方案我果断放弃。
动态脚本方案
<script>
(function(){
// 语言包 js 文件内容为:window.i18n = { key1: value1 };
const i18nLangCDNs = {
"zh-CN": "/lang/zh-CN.1268ec6019c7a7bb7b27d1ecdadc3948.js",
"en-US": "/lang/en-US.e6c246ecf2b64be936a116706cdd6611.js",
};
let lang = getLang();
const script = document.createElement('script');
script.async = false;
script.src = i18nLangCDNs[lang];
document.querySelector('head').appendChild(script);
});
</script>
这种方案利用了脚本里创建脚本的方式。能在更前面的位置加载语言包脚本。
优点是我们可以不需要做后端渲染的工作,让选择语言包的逻辑交给前端。但涉及到前端工程化,需要写插件改变原来的加载脚本形式。
我们的项目使用了 webpack,如果用这个方案,就需要写一个 webpack 插件去改造 HtmlWebpackPlugin 的构建流程。
目前来说,方案 1 和 方案 3 都是不错的。
但考虑到我们公司的前后端是分离的,后端的代码实现对我来说其实是黑盒,我没有权限也没有能力去写后端代码。而项目是前端项目,最好还是让前端来掌控维护。所以我最终选择了方案 3。
方案1 和方案 2 的更具体介绍,可以看我的这篇文章:前端国际化,该如何实现按需加载语言包?
改造过程
原来项目打包后的 html 文件大致如下。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 业务代码,语言包也在里面 -->
<script src="app.js"></script>
</body>
</html>
app.js 里有全量语言包的内容。
改造后的 html 文件如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
(function(){
// 语言包 js 文件内容为:window.i18n = { key1: value1 };
const i18nLangCDNs = {
"zh-CN": "/lang/zh-CN.1268ec6019c7a7bb7b27d1ecdadc3948.js",
"en-US": "/lang/en-US.e6c246ecf2b64be936a116706cdd6611.js",
};
let lang = getLang();
const script = document.createElement('script');
script.async = false;
script.src = i18nLangCDNs[lang];
document.querySelector('head').appendChild(script);
});
</script>
</head>
<body>
<!-- 为保持执行顺序,业务代码也需要改为动态加载形式 -->
<script>
<!-- app.js 文件已移除语言包 -->
['app.js'].forEach(function(src) {
const script = document.createElement('script');
script.async = false;
script.src = src;
document.body.appendChild(script);
});
</script>
</body>
我们语言包将 app.js 从中提取出来,并且分为多个语言包放到 js 文件,如 zh-CN.js、en-US.js,在 app.js 之前执行。
let lang = getLang();
const script = document.createElement('script');
script.async = false;
script.src = i18nLangCDNs[lang];
document.querySelector('head').appendChild(script);
我们先确认用户使用的语言是什么。
如果我们不支持持久化设置,可以通过 navigator.language 或前端的其他地方获取。
但通常用户可以设置语言,这个语言标识就要后端给,再请求一次用户信息可太离谱了,所以这里还是需要后端给我们往 html 里嵌入用户选择的语言。然后我们从语言 cdn 列表里选我们需要的语言。
script 元素默认会将 async 设置为 true,效果是脚本下载完立即执行。需要将其改为 false,保证多个动态脚本顺序执行。
文件名使用了哈希,是为了解决浏览器缓存问题。
执行后,就会将语言包文案暴露在全局变量中。
业务代码 app.js 也得改成动态加载形式,如果原来的非动态写法,执行时机就会早于语言包脚本。
这里涉及到了 script 的执行时机,具体规则可以看我的这篇文章:script 的三种加载模式:默认加载、defer、async
原来的写法
<script src="app.js"></script>
改造后
<script>
<!-- app.js 文件已移除语言包 -->
['app.js'].forEach(function(src) {
const script = document.createElement('script');
script.async = false;
script.src = src;
document.body.appendChild(script);
});
</script>
这样我们就能保证先执行语言包脚本,再执行 app.js。
app.js 中的业务代码执行时,使用 getText 方法就能正常通过 key 获取到对应的文案。
这里 app.js 改为动态的写法后,需要脚本解析执行后才下载脚本,可以考虑加个 link preload 提前下载脚本。
link 的 preload 作用可以看我的这篇文章。
期间遇到的问题
思路并不复杂,但改造过程中做了很多工作,遇到了不少问题。这里简单列举一下,不展开讲了,到时候会考虑另写文章讨论。
- 我们项目的语言包是维护在在线表格上的,每次会通过脚本拉取数据,然后处理成 JSON 文件。我需要再写一个脚本来处理这个 JSON 文件,将其分成多个语言包,并生成功 TS 类型文件
- 使用了 monorepo,我专门分了一个 i18n 的包。
- 最难的是开发一个 Webpack 插件,需要做到拷贝特定文件夹下的语言包,加上内容哈希,放到构建目录下。这些带有哈希的名字要保存下来,通过 HtmlWebpack的钩子转换为内嵌 script 形式添加到 html 上。此外,还要将原来的打包文件 app.js 转换为动态加载的形式。
- 开发环境还是要全量加载语言包,方便测试。一个原因是 devServer 无法读取到使用 copy 的文件,需要额外用 write-file-webpack-plugin,但项目用的 create-react-app 不太好改造。
- 改造 getText 获取文案的方法,需要考虑开发和生产环境的不同
- 我们还有个中间层的 nextjs 项目,我们的语言包要兼容该项目,所以里面还写了判断环境的逻辑,在 global 或 window 上挂载全局变量。
- 测试用例和 CI 补上一行引入语言包的逻辑。
- ...
结尾
行文有点仓促,想到什么写什么,希望对你做按需加载语言方案有一定的帮助。
我是啥都写写的前端西瓜哥,欢迎关注我。
相关推荐
- Micheal Nielsen's神经网络学习之二
-
依然是跟着MichaelNielsen的神经网络学习,基于前一篇的学习,已经大概明白了神经网络的基本结构和BP算法,也能通过神经网络训练数字识别功能,之后我试验了一下使用神经网络训练之前的文本分类,...
- CocoaPods + XCTest进行单元测试 c单元测试工具
-
在使用XCTest进行单元测试时,我们经常会遇到一些CocoaPods中的开源框架的调用,比如“Realm”或“Alamofire”在测试的时候,如果配置不当,会导致“frameworknotfo...
- Java基础知识回顾第四篇 java基础讲解
-
1、&和&&的区别作为逻辑运算符:&(不管左边是什么,右边都参与运算),&&(如果左边为false,右边则不参与运算,短路)另外&可作为位运算符...
- 项目中的流程及类似业务的设计模式总结
-
说到业务流程,可能是我做过的项目中涉及业务最多的一个方面了。除了在流程设计之外,在一些考核系统、产业审批、还有很多地方,都用到相似的设计思路,在此一并总结一下。再说到模式,并不是因为流行才用这个词,而...
- 联想三款显示器首批获得 Eyesafe Certified 2.0 认证
-
IT之家7月31日消息,据外媒报道,三款全新联想显示器是全球首批满足EyesafeCertified2.0的设备。据报道,联想获得EyesafeCertified2.0认证的显...
- maven的生命周期,插件介绍(二) 一个典型的maven构建生命周期
-
1.maven生命周期一个完整的项目构建过程通常包括清理、编译、测试、打包、集成测试、验证、部署等步骤,Maven从中抽取了一套完善的、易扩展的生命周期。Maven的生命周期是抽象的,其中的具体任务都...
- 多线程(3)-基于Object的线程等待与唤醒
-
概述在使用synchronized进行线程同步中介绍了依赖对象锁定线程,本篇文章介绍如何依赖对象协调线程。同synchronized悲观锁一样,线程本身不能等待与唤醒,也是需要对象才能完成等待与唤醒的...
- jquery mobile + 百度地图 + phonegap 写的一个"校园助手"的app
-
1jquerymobile+百度地图+phonegap写的一个"校园助手"的app,使用的是基于Flat-UI的jQueryMobile,请参考:https://github.com/...
- Apache 服务启动不了 apache系统服务启动不了
-
{我是新手,从未遇到此问题,请各位大大勿喷}事由:今天早上上班突然发现公司网站出现问题。经过排查,发现是Apache出现问题。首先检查配置文件没有出问题后,启动服务发现Apache服务能启动,但是没法...
- 健康债和技术债都不能欠 公众号: 我是攻城师(woshigcs)
-
在Solr4.4之后,Solr提供了SolrCloud分布式集群的模式,它带来的主要好处是:(1)大数据量下更高的性能(2)更好扩展性(3)更高的可靠性(4)更简单易用什么时候应该使用Sol...
- Eye Experience怎么用?HTC告诉你 eyebeam怎么用
-
IT之家(www.ithome.com):EyeExperience怎么用?HTC告诉你HTC上周除了发布HTCDesireEYE自拍机和HTCRE管状运动相机之外,还发布了一系列新的智能手机...
- Android系统应用隐藏和应用禁止卸载
-
1、应用隐藏与禁用Android设置中的应用管理器提供了一个功能,就是【应用停用】功能,这是针对某些系统应用的。当应用停用之后,应用的图标会被隐藏,但apk还是存在,不会删除,核心接口就是Packag...
- 计算机软件技术分享--赠人玫瑰,手遗余香
-
一、Netty介绍Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty...
- Gecco爬虫框架的线程和队列模型 爬虫通用框架
-
简述爬虫在抓取一个页面后一般有两个任务,一个是解析页面内容,一个是将需要继续抓取的url放入队列继续抓取。因此,当爬取的网页很多的情况下,待抓取url的管理也是爬虫框架需要解决的问题。本文主要说的是g...
- 一点感悟(一) 初识 初读感知的意思
-
时间过得很快,在IT业已从业了两年多。人这一辈子到底需要什么,在路边看着人来人往,大部分人脸上都是很匆忙。上海真是一个魔都,它有魅力,有底蕴,但是一个外地人在这里扎根置业,真的是举全家之力,还贷3...
- 一周热门
-
-
Boston Dynamics Founder to Attend the 2024 T-EDGE Conference
-
IDC机房服务器托管可提供的服务
-
详解PostgreSQL 如何获取当前日期时间
-
新版腾讯QQ更新Windows 9.9.7、Mac 6.9.25、Linux 3.2.5版本
-
一文看懂mysql时间函数now()、current_timestamp() 和sysdate()
-
流星蝴蝶剑:76邵氏精华版,强化了流星,消失了蝴蝶
-
PhotoShop通道
-
查看 CAD文件,电脑上又没装AutoCAD?这款CAD快速看图工具能帮你
-
WildBit Viewer 6.13 快速的图像查看器,具有幻灯片播放和编辑功能
-
IDC机房服务器托管或租用可提供的一系列服务
-
- 最近发表
-
- Micheal Nielsen's神经网络学习之二
- CocoaPods + XCTest进行单元测试 c单元测试工具
- Java基础知识回顾第四篇 java基础讲解
- 项目中的流程及类似业务的设计模式总结
- 联想三款显示器首批获得 Eyesafe Certified 2.0 认证
- maven的生命周期,插件介绍(二) 一个典型的maven构建生命周期
- 多线程(3)-基于Object的线程等待与唤醒
- jquery mobile + 百度地图 + phonegap 写的一个"校园助手"的app
- Apache 服务启动不了 apache系统服务启动不了
- 健康债和技术债都不能欠 公众号: 我是攻城师(woshigcs)
- 标签列表
-
- serv-u 破解版 (19)
- huaweiupdateextractor (27)
- thinkphp6下载 (25)
- mysql 时间索引 (31)
- mydisktest_v298 (34)
- sql 日期比较 (26)
- document.appendchild (35)
- 头像打包下载 (61)
- oppoa5专用解锁工具包 (23)
- acmecadconverter_8.52绿色版 (39)
- oracle timestamp比较大小 (28)
- f12019破解 (20)
- np++ (18)
- 魔兽模型 (18)
- java面试宝典2019pdf (17)
- beamoff下载 (17)
- unity shader入门精要pdf (22)
- word文档批量处理大师破解版 (36)
- pk10牛牛 (22)
- server2016安装密钥 (33)
- mysql 昨天的日期 (37)
- 加密与解密第四版pdf (30)
- pcm文件下载 (23)
- jemeter官网 (31)
- iteye (18)