利用自定义 babel 插件在 vue2 项目提取多语言词条

406 阅读1分钟

背景:本项目是一个 nuxt2 技术栈,结合 vue-i18n 的一个海外项目,某一天突然有一个需求需要提取项目用到的多语言词条

需求分析

  1. 哪些文件包含多语言词条?答案: .vue 文件, .js 文件
  2. js 文件比较好提取,我们直接通过关键函数(this.$i18n.tthis.$t)的 CallExpression 名字来提取
  3. .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
)

效果截图:

image.png

image.png

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;
});