前言 📖
近期,在团队内推自动化表单,主要是为了去掉后台项目中繁多的表单代码。众所周知,表单一直都是后台代码的一个痛点,因为它的代码就是一个字 “长”...所以,作为一名 21 世纪的前端工程师,我们要时刻反省如何提效(能不写代码就不写代码)。
自动化表单的主要设计理念是围绕一个渲染器,通过配置对象来生成对应的表单。那么,这个时候就遇到了一个问题,对象和 UI 之间是脱离的,这就好比很多人习惯用 template
的方式写「Vue」,而不是更好性能的 render
函数,因为前者更加语义化~
那么,有办法实现语义化吗?答案是:当然可以。我们可以规定一个简易的「模版」语法,通过编译「模版」生成对应的 AST 抽象语法树,它的本质也是对象。那么,这个时候刚好“牛头对上马嘴了”,渲染器再基于这个 AST 来渲染表单,从而完成「模版」到 AST 到表单的转化过程~
并且,提及「模版」语法,我想大家立马会想起「Vue」的「模版」(template)语法。所以,今天我们也将借助「Vue」的核心编译能力 compiler-core
来玩转模版编译!
本次文章将分为以下三个部分进行:
- 了解「Monorepo」以及它在 Vue3 中的运用。
- 了解
compiler-core
的内部运行原理,掌握模版编译基础。 - 开搞,玩转模版编译(乞丐版国际化)。
正文开始~
一、Monorepo 以及它在 Vue3 中的运用 👏
首先,我们先来了解一下什么是「Monorepo」,维基百科上对它的介绍:
———— In revision control systems, a monorepo is a software development strategy where code for many projects is stored in the same repository.
简单理解,「Monorepo」指一种将多个项目放到一个仓库的一种管理项目的策略。当然,这只是概念上的理解。而对于实际开发中的场景,「Monorepo」的使用通常是通过 yarn 的 workspaces
工作空间,又或者是 lerna 这种第三方工具库来实现。使用「Monorepo」的方式来管理项目会给我们带来以下这些好处:
- 只需要一个仓库,就可以便捷地管理多个项目。
- 可以管理不同项目中的相同第三方依赖,做到依赖的同步更新。
- 可以使用其他项目中的代码,清晰地建立起项目间的依赖关系。
「Vue3」正是采用的 yarn 的 workspaces
工作空间的方式管理整个项目,而 workspaces
的特点就是在 package.json 中会有这么两句不同于普通项目的声明:
{
"private": true,
"workspaces": [
"packages/*"
]
}
可以看到,packages 文件目录下根据「Vue3」实现所需要的能力划分了不同的项目。并且,这里的 compiler-core 目录则是我们本小节要介绍的 compiler-core
。所以,packages 下的项目结构会是这样:
那么,了解什么是「Monorepo」以及其在「Vue3」中的运用后,接下来我们开始了解 compiler-core
的内部运行原理~
二、compiler-core 的内部运行原理 🔧
compiler-core
负责「Vue3」中核心编译相关的能力,这包括解析(parse)模板、转化 AST 抽象语法树(transform)、代码生成(generate)等三个过程,它们之间的工作流如下图所示:
可以看到,「Vue3」会先解析模版生成对应的 AST 抽象语法树,其次再 transform
抽象语法树,对 AST 做一些特殊处理,例如打上 shapeFlag
和 patchFlag
等操作,最后,如果 generate
根据抽象语法树来生成对应的可执行代码,即 render
函数。
不知道什么是shapeFlag
或patchFlag
的同学可以看这两篇文章:《compile 和 runtime 结合的 patch 过程》 、《从编译过程,理解静态节点提升》
那么,在「Vue3」源码层面,它们都是运行在 baseCompiler
方法中:
// packages/compiler-core/src/compiler.ts
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
const ast = isString(template) ? baseParse(template, options) : template
...
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
可以看到 baseCompiler
进行模版编译相关操作的也就是 baseParse
、transform
、generate
这三个方法,它们也分别对应着上面所说的三个阶段。那么,接下来我们将会借助这三者来玩转的模版编译!
三、开搞,玩转模版编译 🚀
既然要玩转模版编译,那么我们就搞点有趣的(骚操作)。我们来实现一个栗子,通过它渲染模版,我们会对文字内容做替换操作,即乞丐版国际化。
3.1 乞丐版国际化 🚄
我们定义一个函数它会根据 key
返回指定的语言 lang
下的文字:
function getWords(key, lang = "EN") {
const map = new Map([
["CN", {
hi: "你好",
}],
["EN", {
hi: "hello"
}]
])
return map.get(lang)[key]
}
然后,我们需要对「模版」中出现的 hi
字符串转化为特殊语言下的文字。这里我们需要借助 compiler-core
的提供的四个方法:
baseParse
解析「模版」生成 AST 抽象语法树。getBaseTransformPreset
用于创建基础的transform
函数(需要注意它是必须的)。transform
转化 AST 抽象语法树,可以实现对 AST 节点的替换、删除操作。generate
根据转化后的 AST 抽象语法树生成render
函数。
const compiler = require("@vue/compiler-core");
function render(template, lang = "CN") {
const ast = compiler.baseParse(template)
const transform = (rootNode) => {
if (rootNode.type === 2) {
rootNode.content = getWords(rootNode.content)
}
}
const prefixIdentifiers = true
const [nodeTransforms, directiveTransforms] = compiler.getBaseTransformPreset(
prefixIdentifiers
)
compiler.transform(ast, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
myTransfrom
],
})
const render = compiler.generate(ast)
return render.code
}
3.2 开箱使用,体验整个过程 😍
我们直接定义一个模版字符串,并将该模版字符串作为参数传给到上面定义好的 render
函数。
const template = `<div>hi</div>`
const renderStr = render(template)
这里我们打印一下生成的 render
函数字符串 renderStr
:
'const _Vue = Vue\n' +
'\n' +
'return function render(_ctx, _cache) {\n' +
' with (_ctx) {\n' +
' const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue\n' +
'\n' +
' return (_openBlock(), _createBlock("div", null, "你好"))\n' +
' }\n' +
'}'
然后,我们执行这一段代码生成的 HTML 会是这样:
<div>你好<div>
如果,我们在调用前面定义的 render
函数时,传入的 lang
为 EN
,那么输出的 HTML 的会是这样:
<div>hello<div>
结语 🔚
文中介绍的使用 compiler-core
玩转模版编译的栗子只是极简的,如果要具体要具体到业务场景,那就要 fork 一份 compiler-core
来处理一些自定义的操作,这样生成的 AST 才更加贴合我们自己的需求,这期间应该需要一些时间去理解 compiler-core
中更加底层的东西。
所以,这也是为什么文章标题是【前端进阶】的缘故,因为本次介绍的内容涉及到编译的场景,它的最佳演变是形成一种自己规定「模版语法」,你也可以称之为简易版的「DSL」~最后,如果文章中存在表达不当或错误的地方,欢迎各位同学提 Issue~
❤️ 爱心三连击
写作不易,可以的话麻烦点个赞,这会成为我坚持写作的动力,奥力给!!!
我是五柳,喜欢创新、捣鼓源码,专注于 Vue3 源码、Vite 源码、前端工程化等技术领域分享,欢迎关注我的「微信公众号:Code center」。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。