几分钟优化你的国际化

4,825 阅读3分钟

前言

本文针对国际化翻译数据多,并存放在服务器的情况,对优化进行了思考,并写了了一个插件github.com/jmx16449196…,供大家参考或使用。抛砖引玉,如果有需要改进的地方,希望大家指正。

需求背景

我司单页面应用中的国际化功能存在两个可优化的点:

  • 国际化翻译数据存在了数据库中。每次切换语种或重新加载页面,都需要请求一次接口,接口大概会耽误1s,十分影响用户体验,特别是首屏加载。

  • 由于所有数据都存放在数据库,所以首屏加载时,请求接口前都没有翻译数据可用。

优化方案

针对第一个问题,使用缓存

针对第二个问题,我把翻译数据分为了两份:

静态翻译数据:写死在js中,用于存在一些基础的翻译数据,即使断网了,用户也可以看到正常的页面。

动态翻译数据:从接口获得,这部分数据可以让用户通过相关配置页面去修改。

优化后的页面在进入时,可以分为以下阶段:

image

关键代码

// 创建实例并调用
const translateManager = new TranslateManager({/*传参*/})
translateManager.update('en', (res) => {
    // 触发相应的更新视图的逻辑
    // 例如在vue中,使用了vue-i18n,你可以
    i18n.setLocaleMessage(res.data);
});


class TranslateManager {
  STORAGE_KEY: string
  expireTime: number
  requestFn: Function | null
  staticTranslateData: object
  
  /**
   * 构造函数
   * @param params 
   */
  constructor(params: constructorParams) {
    this.expireTime = params.expireTime || Infinity; // 前端缓存有效时间
    this.STORAGE_KEY = params.storageKey || 'TranslateManager';
    this.requestFn = params.requestFn;
    this.staticTranslateData = params.staticTranslateData || {};
  }
  setRequestFn(fn: Function) {
    this.requestFn = fn;
  }
  /**
   * 设置缓存
   * @param {String} language 
   */
  setCache(language: string, data: object) {
    // 请求完成后缓存
    const storageData = jsonParse(localStorage.getItem(this.STORAGE_KEY)) || {};
    storageData[language] = data;
    storageData['time'] = new Date().getTime();
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(storageData));
  }
  /**
   * 从缓存中读取
   * @param language 语种
   */
  getCache(language: string) {
    const storageData = jsonParse(localStorage.getItem(this.STORAGE_KEY)) || {};
    if (storageData && storageData['time'] && storageData[language]) {
      const time = storageData['time'];
      const now = new Date().getTime();
      // 缓存只有两小时
      if (now - time <= this.expireTime) {
        return storageData[language] || null;
      }
    }
    return null;
  }
  /**
   * 获取后端的静态国际化数据
   */
  getDynamicTranslateData(language: string) {
    if (!this.requestFn) {
      console.error('请先执行setRequestFn!');
      return Promise.reject();
    }
    return this.requestFn({language}).then((res: object) => {
      this.setCache(language, res);
      return res;
    });
  }
  /**
   * 获取前后端交集后的国际化数据
   * @param {*} language 
   */
  getMergeTranslateData(language: string) {
    // 没缓存就发起请求
    return this.getDynamicTranslateData(language).then((res: any) => {

      const data = this.staticTranslateData[language] || {};
      // 部分国际化写在了前端,把前端的国际化文件合并到后端返回的数据中
      merge(res, data)
      if (isEmptyResult(res)) {
        return Promise.reject(new Error('locale empty !!'))
      }
      return res;
    })
  }
  /**
   * 主方法
   * @param locale 语种
   * @param callback 回调函数,用于订制自己触发的渲染逻辑
   */
  update(locale: string, callback: Function) {
    let staticData = this.staticTranslateData[locale];
    const cacheData = this.getCache(locale) || {};
    // 使用静态数据和缓存数据的并集,触发第一次视图更新
    merge(cacheData, staticData);
    callback(cacheData, 'first');

    // 返回静态和动态数据的合集
    return this.getMergeTranslateData(locale).then((res: object) => {
      // 获取数据是否和动态数据不一致
      if (res && !isEqual(res, cacheData)) {
        callback(res, 'second')
      }
    })
  }
}

延伸

使用了上面的做法,已经很好的达到了预期效果。就在写这篇文章的时候,突然发现还有继续深入优化的方向:可以把动态数据划分不同的模块,分步加载。根据自身业务的逻辑,比如可以根据用户权限。当我用户权限低只可以看主页时,我只加载主页模块的翻译数据即可;当我用户权限高,并进入了相应页面,此时再去下载对应的模块数据。

总结

本次提供的优化方案思路并不难,只是实现起来需要些时间,我根据本次优化的思路造了个轮子,使用十分方便,内含使用demo和注释,欢迎star:github.com/jmx16449196…。如果觉得本文对你有所帮助,求点赞一下,谢谢~

更多前端技术分享请订阅微信公众号“前端技术干货分享圈”