找回密码
 会员注册
查看: 8|回复: 0

前端体积优化之i18nkey压缩

[复制链接]

16

主题

0

回帖

49

积分

新手上路

积分
49
发表于 2024-9-30 17:45:31 | 显示全部楼层 |阅读模式
背景在推进国际化的进程中,涌现出很多方案可以帮大家实现国际化文案定义以及使用。在飞书前端架构中,国际化文案已经做到了按需引入及按需加载,只不过随着业务的发展,国际化文案数量逐渐增多。再来看代码中的文案部分,key 长度越来越长,这部分都属于无用代码,如果能够缩短,可以节省部分代码体积,加快 js 在浏览器中运行的速度。如何做?通过压缩 i18nkey 的方式,将 i18n 的 key 从字母压缩为短字符串。 目前业界中为了提升 webpack 打包速度,发展出很多利用多进程进行 js 编译的方案。飞书前端为了提高 webpack 编译速度,大量使用了thread-loader进行并发编译,i18n 扫描则采用了 babel 插件进行扫描和统计,那如何在 babel 扫描的过程中将扫描结果收集起来,如何将运行时的 key 更换为更短的 key,并且能够按照文件归类,实现按需加载呢?思路在 webpack 编译之前,先拿到当前业务下载的文案列表,将列表中所有的 key 进行编码,编码后的长度应该越短越好在 babel loader 扫描的过程中,将用到的文案上报,并将引入文案时使用的 key,替换为短编码在扫描完成后,生成文案的部分,使用编码后的短字符串,作为文案的 key,打包进文案文件中具体代码编码方式将下载的所有 i18n 的 key 进行一次编码映射,通过 key 在数组中的 index,做一个 26 进制转换,再把转换后的字符串中的数字填充为剩余的未用到的字母,保证 key 中无数字,可获得一个不超过 5 位的短 key。constNUMBER_MAP={0:'q',1:'r',2:'s',3:'t',4:'u',5:'v',6:'w',7:'x',8:'y',9:'z',};consti18nKeys=Object.keys(resources['zh-CN']).reduce((allbject,key:string,index:number)=>{//将i18n的key重新编码,编码成26进制,然后用字母替换掉所有数字。//因为变量名称不能用数字开头,所以需要替换掉所有数字all[key]=index.toString(26).replace(/\d/g,(s)=>NUMBER_MAP[s]);returnall;},{});最初的设想中如果有从某个 enum 中引入 key 的行为,可以将 enum 的成员名字一起缩短,所以采用了替换所有数字的方式,保证短 key 不会以数字开头,后来在开发过程中发现没有这种用法,但是编码方式还是保留下来了。扫描方式借助 babel plugin 强大的 ast api,可以轻松完成 i18n key 的扫描和替换。exportdefaultfunctionbabelI18nPlugin(options,args:{i18nKeys:{[key:string]:string}}){consti18nKeys=args.i18nKeys;return{visitor:{StringLiteraltree,module)=>{const{node,parentPath:{node:parent,scope,type}}=tree;const{filename}=module;if(!shouldAnalyse(filename)){return;}conststringValue=node.value;if(stringValue&i18nKeys.hasOwnProperty(stringValue)){if(/***飞书前端中使用了__Text和_t的全局方法来获得对应的文案内容,所以在这里限定了只有在全局方法*__Text和_t中传递的第一个参数为字符串时,才将字符串修改为短key*/type==='CallExpression'&['__t','__Text','__T'].includes(parent.callee.name)&!scope.hasBinding(parent.callee.name)){node.value=i18nKeys[stringValue];/***通过在source中写入一个特殊注释的方式将key标记在代码中,*交给下一步的webpack来收集*/tree.addComment('leading',`${COMMENT_PREFIX}${i18nKeys[stringValue]}`);}else{/***当匹配到的字符串并不是通过_t和__Text使用的场景,依然上报长key,保证代码稳定性*/tree.addComment('leading',`${COMMENT_PREFIX}${stringValue}`);}}},MemberExpressiontree,{filename})=>{if(!shouldAnalyse(filename)){return;}const{node}=tree;constmemberName=node.property.name;if(memberName&i18nKeys.hasOwnProperty(memberName)){tree.addComment('leading',`${COMMENT_PREFIX}${memberName}`);}},}};}如果扫描到了 i18n 相关的字符串字段,将在原地添加一个注释,用来标记当前模块使用到的 key,这种方式可以让扫描结果落在代码中,使得扫描的操作可以被 cache-loader 缓存,进一步提升构建速度。收集过程通过 babel-loader 的模块都会被标记上使用到的 i18n 的 key 和替换后的短 key,在 webpack 的 parse 阶段只需要遍历文件的所有注释即可拿到模块内用到的所有 i18n 的 key。exportdefaultclassChunkI18nPluginimplementsPlugin{staticfileCache=newMap>();constructor(privatei18nConfig:I18nBundleConfig){}publicapply(compiler:Compiler){compiler.hooks.compilation.tap('ChunkI18nPlugin',(compilation,{normalModuleFactory})=>{consthandler=(parser)=>{//在parser中hookprogram钩子parser.hooks.program.tap('ChunkI18nPlugin',(ast,comments)=>{constfile=parser.state.module.resource;if(!ChunkI18nPlugin.fileCache.has(file)){ChunkI18nPlugin.fileCache.set(file,newSet());}constkeySet=ChunkI18nPlugin.fileCache.get(file);//拿到module的所有注释,扫描其中包含的i18n信息,缓存到一个map中comments.forEach(({value}:{value:string})=>{constmatcher=value.match(/\s*@i18n\s*(.*)/);if(matcher.groups.keys){constkeys=matcher.groups.keys.split('');(keys||[]).forEach(keySet.add.bind(keySet));}});});};//监听normalModuleFactory的parser的hooksnormalModuleFactory.hooks.parser.for('javascript/auto').tap('DefinePlugin',handler);normalModuleFactory.hooks.parser.for('javascript/dynamic').tap('DefinePlugin',handler);normalModuleFactory.hooks.parser.for('javascript/esm').tap('DefinePlugin',handler);});}...}有什么不足?按照模块收集到的 key 是基于源文件扫描到的所有的 key。实际上我们可能存在一些较大的工具方法模块,或者组件模块,并不会用到全部的代码(部分代码会被 treeshaking 机制删掉),后续优化方向可以探索如何只扫描用到的代码中的 key,进一步压缩打包后的总体积。最终收益在一段时间的灰度测试后,最终方案上线运行,飞书前端大约 11000 条 key 的情况下,所有单页前端代码体积总计下降 7.2MB。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-14 20:06 , Processed in 0.480937 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表