在一个比较大的项目里面(有国际化需求的),国际化的支持是一个必不可少的; 那如何落地就得具体问题具体分析了,这里说说我遇到过并落地的一个改造方案;
说说项目背景,是一个迭代多年的产研类项目(整个系统是围绕react生态去研发的),历史包袱挺多; 多种第三方库并存,也有iframe的场景以及自研的插件机制系统(现代沙盒隔离那一套);
方案仅供参考,哈!
从以下个方面入手语言包覆盖
语言资源必须集中化维护!(所以我们之前花了些时间做了整个系统的统一)
为什么采用重载?因为会比较彻底和正确响应; 上面说到了,这是一个新老技术融合的项目,不纯粹!
重载有两个非常大的好处
为什么用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在这个流程中,更多的是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!!(这是一个可以保证跟随代码一起变的标识)
那么怎么跟随标品走呢?这里就用到html-webpack-plugin的动态注入变量来; 在构建的时候,把当前代码的git commit hash 注入到env,再写进入代码!
为什么要写进去? 写进去的好处不仅仅作为缓存策略的标识, 更重要的是你给客户定位也能快速通过这个hash 反向查这个工单的版本!!!
方案没有完美之说,方案的设计要结合现状做调整,权衡; 中间可能会存在很多过渡措施,但是会随着时间一步步的统一,去包袱!
仅以此文章给今年画上一个句号, 提前祝各位小伙伴新春快乐,万事如意,虎虎生威! 有不对之处请留言,谢谢阅读!