由babel产生的一系列联想

作者 likaiqiang 日期 2021-09-25
由babel产生的一系列联想

起因

上半年有幸参加了某搜索引擎的技术“选拔”,虽然结果令人难以接受,但是有幸知道了babel这份大餐,还有ast。

babel原名6to5,我们平时写的各种高级语法,会通过babel 先parse成一颗抽象语法树,然后通过babel plugin对这棵树进行增删改,最后generate成浏览器上可识别的代码。

咋一听,还挺高大上,感觉离实际业务挺远,实际上是很有用的知识。(一些在业务层通过笨拙的方法解决的问题,就可以提升到“构建层”来解决了)

本来的问题

新公司,新起点,新同事,新业务。新的业务与之前最大的不同在于它是支持多语言的,而且不止一种,除了要处理阿拉伯语rtl布局以外,最大的问题是如何在代码里面“优雅”的处理让人头疼的多语言code。

大概是这么一种感受:

项目是用vue(暂时)写的,因为有国际化,也使用了i18n,项目里有份多语言配置文件,大概长这个样子。

export default{
en:{
key1:'xxx1',
key2:'xxx2',
key3:'xxx3'
//...
},
hi:{
key1:'xxx1',
key2:'xxx2',
key3:'xxx3'
//...
},
ar:{
key1:'xxx1',
key2:'xxx2',
key3:'xxx3'
//...
}
// ...
}

每次加新需求,就不得不写好多this.$t(‘key1’) this.$t(‘key2’) this.$t(‘keyn’) 这样的代码。到没什么技术难度,但是让人厌烦。

由babel联想到的解决方案

某一刻,突发奇想。我可不可以在做需求时只写英文,然后在编译时由构建工具基于那份配置文件生成i18n(this.$t(‘keyn’))这样的代码。

过程

一开始我的眼里只有babel,在经过无数次失败以后,我开始反省为什么我在babel plugin traverse函数里拦截不到jsx语法。最后真是想锤死自己,vue的模板语法根本就不通过babel,vue-loader的作用仅仅是把单文件文件分成至少3部分,只有script部分被babel转义,编译template的是vue-template-compiler。

当时我有点怀念render函数,vue也可以那样写,只不过大多数人喜欢template。

在搞明白vue-loader的工作原理以后,发现要干涉vue SFCDescriptor,在不改vue-loader源码的情况下,只能通过webpack plugin实现。可以参考这个人的思路

看他的代码,在plugin阶段拿到的是generate后的代码,他用babel 把这堆代码parse成ast,识别某些错误语法,然后抛错。
首先这样做确实满足了他的需求,但是我这边需要遍历ast,然后才是generate,感觉时机有点不对,还是要在webpack loader那里下功夫。
其次这样做有点画蛇添足的感觉。

思来想去,在不改vue-loader源码的前提下,针对.vue文件再写个loader算是一种解决方案,就叫vue-loader-i18n。所以就会产出这样的代码

const {parse,compileTemplate} = require('@vue/component-compiler-utils')
const { getOptions } = require('loader-utils');

function traverseTemplateAst(ast){
// 遍历ast
}

const plugin = function (source){
const options = getOptions(this)
const sfc = parse({
source,
compiler: require('vue-template-compiler'),
needMap: false
})
const templateSfc = compileTemplate({
source: sfc.template.content,
compiler: require('vue-template-compiler')
})
traverseTemplateAst(templateSfc.ast)
//...
}

module.exports = plugin

这样又有一个问题,我遍历完ast,怎么样把ast“反转为”单文件语法(就是.vue文件原始语法),这样vue-loader才能识别。

vue官方貌似没有提供这样的api。幸好,江山代有才人出 ,这位老兄的思路不错,值得借鉴,在这个包的加持下,就可以写出这样的代码

const {parse,compileTemplate} = require('@vue/component-compiler-utils')
const { getOptions } = require('loader-utils');
const TemplateGenertor = require('vue-template-transformer')

function traverseTemplateAst(ast){
// 遍历ast
}

class TemplateTransform extends TemplateGenertor{

}
const newTemplateTranformer = new TemplateTransform()

function generatorBlockCode(ast){
let code = `<${ast.type}`
for(let key in ast.attrs){
code += ` ${key}=${ast.attrs[key]}`
}
code += `>${ast.content}</${ast.type}>`
return code
}

const plugin = function (source){
const options = getOptions(this)
const sfc = parse({
source,
compiler: require('vue-template-compiler'),
needMap: false
})
const templateSfc = compileTemplate({
source: sfc.template.content,
compiler: require('vue-template-compiler')
})
traverseTemplateAst(templateSfc.ast)
const { code } = newTemplateTranformer.generate(templateSfc.ast)
sfc.template.content = code

const templateCode = generatorBlockCode(sfc.template)
const scriptCode = generatorBlockCode(sfc.script)
const styleCode = sfc.styles.map(style=>{
return generatorBlockCode(style)
}).join('\n')

const blockCode = sfc.customBlocks.map(block=>{
return generatorBlockCode(block)
}).join('\n')
return templateCode + '\n' + scriptCode + '\n' + styleCode + '\n' + blockCode
}

至此,功能算是完成了。但是实际在使用过程中发现了一些问题:

  1. vue-template-transformer不是官方的插件,某些语法不识别,比如
    <template>
    <Component :type.sync="type"></Component>
    </template>

它不识别带有sync修饰符的prop,只能改为v-model。翻插件的源码,发现两个问题。

问题1
问题2

  1. 目前这种基于webpack-loader的实现方式效率会慢很多。毕竟多遍历了几次。

最后的解决方案

还是改源码比较好,这样就不用考虑上面两个问题。有个 专门干这个的。

最终的伪代码

// vue-template-compiler/build.js#4831
//...
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
traverseTemplateAst(ast)
var code = generate(ast, options);
//...

看到没有,官方维护的parse与generate,只不过没有暴露出来。