乐趣区

关于前端:前端进阶使用-Vue3-的-compliercore-玩转模版编译

前言 ????

近期,在团队内推自动化表单,次要是为了去掉后盾我的项目中繁多的表单代码。家喻户晓,表单始终都是后盾代码的一个痛点,因为它的代码就是一个字 “长”… 所以,作为一名 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」~最初,如果文章中存在表白不当或谬误的中央,欢送各位同学提 Issue~

❤️ 爱心三连击

写作不易,能够的话麻烦点个赞,这会成为我保持写作的能源,奥力给!!!

我是五柳,喜爱翻新、捣鼓源码,专一于 Vue3 源码、Vite 源码、前端工程化等技术畛域分享,欢送关注我的「微信公众号:Code center」。

退出移动版