背景:本项目是一个 nuxt2 技术栈,结合 vue-i18n 的一个海外项目,某一天突然有一个需求需要提取项目用到的多语言词条
需求分析
- 哪些文件包含多语言词条?答案: .vue 文件, .js 文件
- js 文件比较好提取,我们直接通过关键函数(
this.$i18n.t、this.$t)的 CallExpression 名字来提取 - .vue 文件我们先编译成为 .js 文件,在使用关键函数(
_vm.$t)的 CallExpression 名字来提取
代码实现(找到整个项目的 .vue 文件, .js 文件)
// getAllFilePath 拿到 整个项目的 .vue 文件, .js 文件
const fs = require('fs');
const path = require('path');
const filePathList = [];
const getAllFilePath = (releativepath) => {
const files = fs.readdirSync(releativepath);
files.forEach(function(filename) {
const filedir = path.join(releativepath, filename);
const stats = fs.statSync(filedir);
const isFile = stats.isFile();
const isDir = stats.isDirectory();
if (isFile && (filedir.endsWith('.js') || filedir.endsWith('.vue'))) {
console.log(filedir, 'filedirfiledirfiledir')
// 包含 'client/static/ 目录的不需要收集
// 包含 client/config 目录的不需要收集
if(!filedir.includes('/client/static/') && !filedir.includes('/client/config/')){
filePathList.push(filedir)
}
}
if (isDir) {
getAllFilePath(filedir); //递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
})
}
getAllFilePath('/Users/xxx/Documents/yyy/client')
console.log(filePathList);
借助 自定义 babel 插件识别多语言词条
//
const fs = require('fs');
const path = require('path');
const babel = require('@babel/core');
const { assemble, createDefaultCompiler } = require('@vue/component-compiler');
const babelPluginRecordMultilingualEntries = require('babel-plugin-record-multilingual-entries');
const compileVueFile = (content, filename = 'test.vue') => {
const compiler = createDefaultCompiler();
const descriptor = compiler.compileToDescriptor(filename, content);
const result = assemble(compiler, filename, descriptor, {});
return result;
}
// result -> 记录总的多语言词条 -> { [filename]: ['词条1', '词条2', '词条3'] }
const result = {}
// checkFileObj -> 记录需要二次确认的多语言词条文件 (部分多语言是变量拼接的,此时就需要二次确认一下 例如: this.$t('aa' + item.name) )
let checkFileObj = {}
// 记录的词条信息的文件
const writerPath = path.resolve(__dirname, './aaa.js');
const getURLByPath = (pathList) => {
// 每次脚本执行清空上一次执行 记录的 信息
let streamData = fs.createWriteStream(writerPath,{ flags: 'w' })
let logger = new console.Console(streamData);
logger.log('');
return pathList.forEach((item) => {
const itemPath = item;
let resultList = [];
if (fs.existsSync(itemPath)) {
let sourceCode = fs.readFileSync(itemPath).toString();
let compiledCode = null;
if (itemPath.endsWith('.vue')) {
compiledCode = compileVueFile(sourceCode).code;
}
// 编译 纯 js 文件
if (itemPath.endsWith('.vue') || itemPath.endsWith('.js')) {
babel.transformSync(compiledCode ?? sourceCode, {
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
],
plugins: [[babelPluginRecordMultilingualEntries, { resultList, checkFileObj, writerPath }]],
// filename 必须传, 记录每个文件的多语言词条的 key
filename: itemPath
})
}
} else {
throw new Error(`此文件路径不存在, 请检查路径 在执行${itemPath} `)
}
result[item] = resultList;
})
}
getURLByPath(filePathList)
console.log(
checkFileObj,
'resultObj -> 输出结果 -> 引用的组件为:',
result
)
效果截图:
babel plugin 实现逻辑(在编译阶段 一边编译一边通过 stream 写入文件了, 并且就一个 stream实例,结束自动释放内存) 仓库地址 欢迎star
const fs = require('fs');
const path = require('path');
// 单列模式获取 streamData 只创建一次 writeStream
const getStreamDataWrapper = () => {
let streamData;
return (options) => {
if(!streamData){
streamData = fs.createWriteStream(options.writerPath,{
flags:'a'
});
return streamData;
}
return streamData;
}
}
let getStreamData = getStreamDataWrapper();
const defaultMultilingualList = ['_vm.$t', 'this.$t', 'this.$i18n.t'];
const recordLanByBabelPlugin = ({ types }, options) => {
let streamData = '';
if(options.writerPath){
streamData = getStreamData(options);
}
const cusCompiledMultilingualList = options.cusCompiledMultilingualList;
let multilingualList = [];
if(cusCompiledMultilingualList?.length === 0){
// throw new Error('使用了该插件,但是 传入的 cusCompiledMultilingualList 为空数组, 请检查 cusCompiledMultilingualList')
multilingualList = defaultMultilingualList;
}
multilingualList = cusCompiledMultilingualList || defaultMultilingualList;
return {
visitor: {
CallExpression(astPath, state) {
const calleeName = astPath.get('callee').toString();
if(defaultMultilingualList.indexOf(calleeName) !== -1){
const filename = state.file.opts.filename;
const firstArgsNodeAst = astPath.get('arguments.0');
if(types.isStringLiteral(firstArgsNodeAst)){
const firstArgsNodeAstVal = firstArgsNodeAst.node.value;
options.resultList.push(firstArgsNodeAstVal);
}else{
options.resultList.push(firstArgsNodeAst.toString());
options.checkFileObj[filename] = true;
}
// 存在 writerPath 路径, 用流的方式 直接写入到该文件
if(options.writerPath){
let logger = new console.Console(streamData);
logger.log(firstArgsNodeAst.toString());
}
}
}
}
}
};
module.exports = recordLanByBabelPlugin;
// 执行结束,释放内存
process.on('beforeExit', function(code) {
console.log('执行结束,释放内存');
getStreamData = null;
});