Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用 Vue3 的 complier-core 玩转模版编译 #18

Open
WJCHumble opened this issue Feb 20, 2021 · 0 comments
Open

使用 Vue3 的 complier-core 玩转模版编译 #18

WJCHumble opened this issue Feb 20, 2021 · 0 comments
Labels

Comments

@WJCHumble
Copy link
Owner

前言

近期,在团队内推自动化表单,主要是为了去掉后台项目中繁多的表单代码。众所周知,表单一直都是后台代码的一个痛点,因为它的代码就是一个字 “长”...所以,作为一名 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 做一些特殊处理,例如打上 shapeFlagpatchFlag 等操作,最后,generate 根据抽象语法树来生成对应的可执行代码,即 render 函数。

不知道什么是 shapeFlagpatchFlag 的同学可以看这两篇文章:《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 进行模版编译相关操作的也就是 baseParsetransformgenerate 这三个方法,它们也分别对应着上面所说的三个阶段。那么,接下来我们将会借助这三者来玩转的模版编译!

三、开搞,玩转模版编译

既然要玩转模版编译,那么我们就搞点有趣的(骚操作)。我们来实现一个栗子,通过它渲染模版,我们会对文字内容做替换操作,即乞丐版国际化。

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 函数时,传入的 langEN,那么输出的 HTML 的会是这样:

<div>hello<div>

结语

文中介绍的使用 compiler-core 玩转模版编译的栗子只是极简的,如果要具体要具体到业务场景,那就要 fork 一份 compiler-core 来处理一些自定义的操作,这样生成的 AST 才更加贴合我们自己的需求,这期间应该需要一些时间去理解 compiler-core 中更加底层的东西。

所以,这也是为什么文章标题是【前端进阶】的缘故,因为本次介绍的内容涉及到编译的场景,它的最佳演变是形成一种自己规定「模版语法」,你也可以称之为简易版的「DSL」。

@WJCHumble WJCHumble added the Vue3 label Feb 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant