项目复盘:通过动态脚本,实现按需加载语言包

简介: 前端西瓜哥

大家好,我是前端西瓜哥,是一名前端开发。

最近做了一个将按需加载语言包的需求,有不少收获,这里记录一下。

改造前的项目

原来项目是将所有的语言包合并在一起,放到一个 JSON 文件里然后被引入。

打包后的脚本里,有完整的语言包的代码,导致打包文件非常大。理论上用户只会使用一种语言,其他的语言没有加载的必要。

目前来说项目只支持两种语言,每个语言有文案 4000 多条。如果还是使用全量加载的话,以后支持的语言每多一个,打包后的文件就要膨胀一圈。

做语言包的拆分还是很有必要的。它可以减少加载资源的大小,减少首次页面加载时间,提高用户体验。

实现方案的选择

实现按需加载语言包的方式很多,我了解到的有三种:

  1. 后端渲染:在请求时将单个语言包嵌入到 HTML 里
  2. 动态 import:使用 ES6的 动态 import 语法
  3. 动态脚本:在脚本里创建一个 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 补上一行引入语言包的逻辑。
  • ...

结尾

行文有点仓促,想到什么写什么,希望对你做按需加载语言方案有一定的帮助。

我是啥都写写的前端西瓜哥,欢迎关注我。

相关文章
|
7月前
|
JavaScript Java 测试技术
基于小程序的汽车保养系统+springboot+vue.js附带文章和源代码设计说明文档ppt
基于小程序的汽车保养系统+springboot+vue.js附带文章和源代码设计说明文档ppt
53 0
|
7月前
|
Android开发
Autox.js 脚本开发环境搭建,从案例到打包apk(详细流程)
Autox.js 脚本开发环境搭建,从案例到打包apk(详细流程)
1946 0
|
JavaScript 应用服务中间件 nginx
vuecli3打包项目上线之后报错怎么使用本地的sourcemap文件定位调试?
vuecli3打包项目上线之后报错怎么使用本地的sourcemap文件定位调试?
174 0
|
7月前
|
JavaScript Java 测试技术
基于小程序的学生公寓电费系统+springboot+vue.js附带文章和源代码设计说明文档ppt
基于小程序的学生公寓电费系统+springboot+vue.js附带文章和源代码设计说明文档ppt
39 2
|
7月前
|
JavaScript Java 测试技术
基于小程序的家政项目小程序+springboot+vue.js附带文章和源代码设计说明文档ppt
基于小程序的家政项目小程序+springboot+vue.js附带文章和源代码设计说明文档ppt
50 0
|
4月前
|
开发工具
如何修改Vscode查看源代码管理版本变动文件的查看方式
这篇文章介绍了如何在VSCode中通过源代码管理插件修改查看源代码版本变动文件的方式,提供了树形视图和列表视图两种查看方法,并说明了如何通过设置选项来切换查看方式,帮助用户根据自己的喜好更高效地查看和管理代码变动。
如何修改Vscode查看源代码管理版本变动文件的查看方式
|
5月前
|
小程序
跨端技术问题之页面或组件样式在小程序、小程序插件和小程序分包中有什么差异
跨端技术问题之页面或组件样式在小程序、小程序插件和小程序分包中有什么差异
|
7月前
|
JavaScript Java 测试技术
基于小程序的外卖点餐+springboot+vue.js附带文章和源代码设计说明文档ppt
基于小程序的外卖点餐+springboot+vue.js附带文章和源代码设计说明文档ppt
42 2
|
7月前
|
JavaScript Java 测试技术
基于小程序的一次性环保餐具销售系统+springboot+vue.js附带文章和源代码设计说明文档ppt
基于小程序的一次性环保餐具销售系统+springboot+vue.js附带文章和源代码设计说明文档ppt
47 0
|
7月前
|
JavaScript Java 测试技术
基于springboot+vue.js的常规应急物资管理系统附带文章和源代码设计说明文档ppt
基于springboot+vue.js的常规应急物资管理系统附带文章和源代码设计说明文档ppt
43 0