前端多语资源打包及加载的一个可行性方案

Posted crper

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端多语资源打包及加载的一个可行性方案相关的知识,希望对你有一定的参考价值。

前言

在一个比较大的项目里面(有国际化需求的),国际化的支持是一个必不可少的;
那如何落地就得具体问题具体分析了,这里说说我遇到过并落地的一个改造方案;

说说项目背景,是一个迭代多年的产研类项目(整个系统是围绕react生态去研发的),历史包袱挺多;
多种第三方库并存,也有iframe的场景以及自研的插件机制系统(现代沙盒隔离那一套);

方案仅供参考,哈!

方案

基础信息(技术栈)

  • 构建工具流:Gulp 4 + Webpack 4
  • 第三方库(lib)
    • moment
    • dayjs
    • gantt
    • ckeditor
  • react 标准全家桶

聚焦点

整合所有i18n资源,集中打包,前置加载(页面头部-C端渲染);
而且我们这边不考虑IE,聚焦现代化的浏览器~

从以下个方面入手语言包覆盖

  • 业务层面全部用i18next作为字段文案维护;
    • 所有非第三方库自身,都可以算作是业务层面
  • 组件库提供语言包端字段映射对象
  • 第三方微微魔改
    • 没有多语支持或者版本太老旧不好升级的
      • 初始化时机篡改原型链
    • 对于支持 切换的,比如moment,dayjs,ck
      • 把对应的需要的语言对象构建好,丢给他们自己初始化即可!

语言资源必须集中化维护!(所以我们之前花了些时间做了整个系统的统一)

语言切换时机

  • 页面加载过程中阻塞加载语言包,再继续后面的初始化逻辑
  • 语言切换采用重载(reload)方案

为什么采用重载?因为会比较彻底和正确;
上面说到了,这是一个新老技术融合的项目,不纯粹!

重载有两个非常大的好处

  • 从接口层发出语言标识,在进入用户界面时候数据就能拉到正确的响应数据(不同语言的response)
  • 其次语言资源可以按需加载(也能非常正确的初始化)

流程图

gulp

为什么用gulp?gulp 在一些场景很好用(比如一些静态资源的转换,迁移等等);
一股脑的丢webpack这类其实会带来很多构建开销;
​所以语言文件用gulp watch实时去监听,产物打到特定的位置就好了;

这边的语言资源是作为一个npm模块来维护的,如图

locale下面就是不同语种,watch整个目录即可!
比如这个task就是构建语言产物的,这个导出再并入gulp stream即可!(仅供参考)

import  resolve  from 'path';
import  src, dest, parallel, watch  from 'gulp';
import  accessSync, constants, statSync, readdirSync  from 'fs';
import gulpEsbuild from 'gulp-esbuild';
import  getDevModeAndParams  from '../../utils';

function checkDirExist(checkPath) 
  try 
    accessSync(checkPath, constants.R_OK | constants.W_OK);
    console.log(`$checkPath 路径gulp能读写`);
   catch (err) 
    console.error(`$checkPath 无法尝试访问,请先检测是否存在`, err);
    process.exit(1);
  


function getLocaleDirName(path) 
  if (!path) throw new Error('path no exist');
  try 
    const localeDirName = [];
    const localeGulpTaskName = [];
    const readList = readdirSync(path);
    for (const item of readList) 
      const fullPath = resolve(path, item);
      const stats = statSync(fullPath);
      if (stats.isDirectory()) 
        localeDirName.push(item);
        localeGulpTaskName.push(`$item_build_locale_task`);
      
    
    return 
      localeDirName,
      localeGulpTaskName,
    ;
   catch (error) 
    console.log(
      '%c 🍇 error: ',
      'font-size:20px;background-color: #7F2B82;color:#fff;',
      '找不到语言文件',
      error
    );
  


function localeBuild(srcPath, DestDirPath, outputName) 
  return () => 
    const inputFile = resolve(srcPath, 'index.js');
    const isRelease = getDevModeAndParams('release', true);
    const esbuildPipe = () => 
      return gulpEsbuild(
        incremental: !isRelease,
        outfile: `$outputName.js`,
        bundle: true,
        charset: 'utf8',
        format: 'iife',
        minify: !isRelease,
        sourcemap: false,
        platform: 'browser',
        loader: 
          '.js': 'js',
        ,
      );
    ;
    return src(inputFile).pipe(esbuildPipe()).pipe(dest(DestDirPath));
  ;


export function langBuild() 
  const SrcDirPath = resolve(
    process.cwd(),
    'node_modules',
    '@ones-ai',
    'lang/locale'
  );
  const DestDirPath = resolve(process.cwd(), 'dest/locale');
  checkDirExist(SrcDirPath);
  const  localeDirName  = getLocaleDirName(SrcDirPath);
  const tasksFunction = (srcPath, destPath) =>
    localeDirName.map((localeKey) =>
      localeBuild(resolve(srcPath, localeKey), destPath, localeKey)
    );

  const watchLocaleBuild = (cb) => 
    watch(
      [`$SrcDirPath/**/*.js`],
      parallel(...tasksFunction(SrcDirPath, DestDirPath))
    );
    cb();
  ;

  const isDevWatch = getDevModeAndParams('release', true)
    ? []
    : [watchLocaleBuild];
  const taskQueue = [...tasksFunction(SrcDirPath, DestDirPath), ...isDevWatch];
  return parallel(...taskQueue);


webpack

webpack在这个流程中,更多的是gulp 和webpack及页面的联动打通;
包括注入一些变量,打包产物结构调整等等~~

当然gulp 启动,webpack 启动都要手动介入也是不合理的;
所以在封装的CLI里面已经打通了!

工程

index.tpl的可能不是很清楚,我再辅助一个伪代码截图,就很清晰了

<!DOCTYPE html>
<html lang="en">
  <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>
      // 这里是通过html-webpack-plugin 插件注入的变量
      window.htmlInjectTplParams =<%= htmlInjectTplParams %>
    </script>
    <!-- 这里动态去获取相关的语言标识 -->
    <script
      src="locale-resource-loader/index.js"
      id="locale-source-loader"
    ></script>
    <script>
      // 通过document.write跟随文档流初始化标准的scripts(会同步阻塞!)
      document.write(
        '<script src="' + window.I18N_LOCALE_SOURCE_URL + '"><\\/script>'
      );
    </script>
  </head>

  <body>
	<!-- 这里再去初始化react 工程相关的请求及数据 -->
  </body>
</html>

唯一标识根据你们业务设计取吧,
这边是cookie -> localStorage -> navigator.language ->defaultLang

function getCookie(name) 
  const cookie = `; $document.cookie`;
  const parts = cookie.split(`; $name=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return;


const isValidLang = (lang) => 
  const validLang = ['zh','en','de','ja'];
  if (!lang) 
    return false;
  
  for (let index = 0; index < validLang.length; index++) 
    const supportLang = validLang[index];
    if (lang === supportLang) 
      return true;
    
  
  return false;
;

function loadJS(FILE_URL, getContainer = 'body', async) 
  let scriptEle = document.createElement('script');

  scriptEle.setAttribute('src', FILE_URL);
  scriptEle.setAttribute('type', 'text/javascript');
  if (async !== undefined) 
    scriptEle.setAttribute('async', async);
  

  const container = document.querySelector(getContainer);

  container.parentNode.insertBefore(scriptEle, container.nextElementSibling);
  const 
    htmlInjectTplParams:  isRelease ,
   = window;
  // success event
  scriptEle.addEventListener('load', () => 
    if (!isRelease) 
      console.info(`$FILE_URL 资源已加载`);
    
  );
  // error event
  scriptEle.addEventListener('error', () => 
    if (!isRelease) 
      console.error(`$FILE_URL 资源加载失败`);
    
  );


// 获取当前locale语言标识
const getLocaleKey = () => 
  let lang = 'zh';
  const getValidLang = [
    getCookie('language'),
    localStorage.getItem('language'),
    navigator.language.slice(0, 2),
  ]
    .filter(Boolean)
    .filter((item) => isValidLang(item));

  return getValidLang.length === 0 ? lang : getValidLang[0];
;

const getLocaleUrl = (lang, isAbsolute = false) => 
  const 
    htmlInjectTplParams:  isRelease, commit ,
   = window;
  return `$isAbsolute ? '/' : ''locale/$lang.js?version=$
    isRelease ? commit : new Date().getTime()
  `;
;
const localeUrl = getLocaleUrl(getLocaleKey());
window.I18N_LOCALE_SOURCE_URL = localeUrl;
// 异步加载JS,有缓存后无法正确阻塞
// loadJS(localeUrl, '#locale-source-loader', false);

缓存策略

肯定有人会想到一个资源缓存到问题(静态资源可以通过query来做资源缓存加载[disk cache]),
没有缓存策略是不可行的,不然每次都去拉取全新的资源(也是一笔额外的网络开销);
就这个玩意

而固定的标识(不能跟随标品变也是不合理的),因为后续迭代有新增文案等等!!
这个问题其实好解决,因为我们现在大多数开发的代码工作流基本围绕Git搞的!

没错,就是git commit hash!!(这是一个可以保证跟随代码一起变的标识)

构建(开发模式)

  • 开发模式下,query用的时间戳,只要重载就全新拉,问题不大

产物(生产模式)

  • 这里用的是git commit hash

那么怎么跟随标品走呢?这里就用到html-webpack-plugin的动态注入变量来;
在构建的时候,把当前代码的git commit hash 注入到env,再写进入代码!

为什么要写进去?
写进去的好处不仅仅作为缓存策略的标识,
更重要的是你给客户定位也能快速通过这个hash 反向查这个工单的版本!!!

优缺点

优点

  • 因为是reload,所以切换语言会很彻底
    • 从接口到页面,链路重新走了一遍,很干净
  • 因为语言资源是挂载在window上,可以通过一些手段派发给其他
    • 微前端体系
    • iframe

待改善

  • 开发模式
    • gulp watch后我没有让其自动reload
      • 因为字段的变更不是高频操作!
      • 业务自身的变更也会出发webpack热更新,部分场景也会自动reload页面
  • 生产模式
    • 资源包大小的问题,目前是全量字段打进去,体积还算可以接受
      • 单个语种一万多个字段压缩后的体积大概在1m出头
        • 等真到了一定程度(字段量),减少体积的手段
          • 可以选择字节压缩编码那种方案,时间换空间,初始化过程再拼装
  • 缓存策略依赖Git commit hash , 对于非git维护的场景需要具体设计一套跟随代码标品化的唯一标识

效果图

早期效果图

结语

方案没有完美之说,方案的设计要结合现状做调整,权衡;
中间可能会存在很多过渡措施,但是会随着时间一步步的统一,去包袱!

仅以此文章给今年画上一个句号,
提前祝各位小伙伴新春快乐,万事如意,虎虎生威!

有不对之处请留言,谢谢阅读!

以上是关于前端多语资源打包及加载的一个可行性方案的主要内容,如果未能解决你的问题,请参考以下文章

前端多语资源打包及加载的一个可行性方案

前端多语资源打包及加载的一个可行性方案

vc实现多语言资源

前端优化方案有哪些?

前端通用框架可行性研究报告之弹窗

vue页面首次加载缓慢原因及解决方案