首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端多语资源打包及加载的一个可行性方案

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

作者头像
CRPER
发布2022-03-08 15:15:09
8620
发布2022-03-08 15:15:09
举报

前言

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

说说项目背景,是一个迭代多年的产研类项目(整个系统是围绕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维护的场景需要具体设计一套跟随代码标品化的唯一标识 效果图 早期效果图
在这里插入图片描述
在这里插入图片描述

结语

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

仅以此文章给今年画上一个句号, 提前祝各位小伙伴新春快乐,万事如意,虎虎生威! 有不对之处请留言,谢谢阅读! ​

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022/01/30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 方案
    • 基础信息(技术栈)
      • 语言切换时机
        • 流程图
          • gulp
          • webpack
          • 工程
        • 缓存策略
          • 构建(开发模式)
          • 产物(生产模式)
        • 优缺点
          • 优点
      • 结语
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档