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

简介: 在一个比较大的项目里面(有国际化需求的),国际化的支持是一个必不可少的;那如何落地就得具体问题具体分析了,这里说说我遇到过并落地的一个改造方案;说说项目背景,是一个迭代多年的产研类项目(整个系统是围绕react生态去研发的),历史包袱挺多;多种第三方库并存,也有iframe的场景以及自研的插件机制系统(现代沙盒隔离那一套);方案仅供参考,哈!

网络异常,图片无法展示
|


前言


在一个比较大的项目里面(有国际化需求的),国际化的支持是一个必不可少的;

那如何落地就得具体问题具体分析了,这里说说我遇到过并落地的一个改造方案;

说说项目背景,是一个迭代多年的产研类项目(整个系统是围绕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维护的场景需要具体设计一套跟随代码标品化的唯一标识


效果图


早期效果图


网络异常,图片无法展示
|

目录
相关文章
|
3月前
|
前端开发 JavaScript Java
前端限制打包文件数量
前端限制打包文件数量
180 65
|
3月前
|
缓存 前端开发 JavaScript
探索前端性能优化:从加载速度到用户体验的全面升级
探索前端性能优化:从加载速度到用户体验的全面升级
58 0
|
2月前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
126 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
26天前
|
前端开发 数据可视化 搜索推荐
深入剖析极态云优雅的前端框架设计方案(上)
最近在体验极态云,这款低代码软件开发产品,发现其前端框架设计方案很优雅很强大! 在接下来的学习过程中,我将持续输出自己对极态云前端框架设计方案的深入理解,包括具体的使用技巧、优势分析以及可能的应用场景等方面的内容,希望能为大家提供有价值的参考。
|
29天前
|
缓存 前端开发 JavaScript
前端性能优化:提升网页加载速度的10个技巧
【10月更文挑战第25天】在互联网时代,网页加载速度直接影响用户体验和搜索引擎排名。本文介绍了10个提升网页加载速度的技巧,包括减少HTTP请求、启用压缩、使用CDN、延迟加载非关键资源、优化图片、减少重定向、使用浏览器缓存、优化CSS和JavaScript、异步加载JavaScript以及代码分割。通过这些方法,可以显著提高网页性能,改善用户体验。
100 5
|
2月前
|
缓存 前端开发 JavaScript
如何优化前端资源
如何优化前端资源
|
2月前
|
缓存 前端开发 UED
前端 8 种图片加载优化方案梳理
本文首发于微信公众号“前端徐徐”,详细探讨了现代网页设计中图片加载速度优化的重要性及方法。内容涵盖图片格式选择(如JPEG、PNG、WebP等)、图片压缩技术、响应式图片、延迟加载、CDN使用、缓存控制、图像裁剪与缩放、Base64编码等前端图片优化策略,旨在帮助开发者提升网页性能和用户体验。
245 0
|
2月前
|
JSON 前端开发 JavaScript
前端模块打包器的深度解析
【10月更文挑战第13天】前端模块打包器的深度解析
|
2月前
|
存储 前端开发 JavaScript
前端模块化打包工具的深度解析
【10月更文挑战第13天】前端模块化打包工具的深度解析
|
2月前
|
前端开发 JavaScript 开发工具
从零开始:构建、打包并上传个人前端组件库至私有npm仓库的完整指南
从零开始:构建、打包并上传个人前端组件库至私有npm仓库的完整指南
290 0

热门文章

最新文章