阅读 532

基于解析AST方案实现VUE组件中文提取和转换

前言

在介绍 AST 大法之前,先一起简单了解一下 AST 相关的基础知识。

什么是 AST

在编译原理中,编译过程一般从词法分析(lexical analysis)开始,由编译器的scanner或者tokenizer按照语言的特殊标记符号,例如定义的关键词等,对源代码的字符串的每个字符进行分析并组合,最后形成一个个单词tokentoken就是 AST 的节点,它们之间相互关联形成 AST。

JS-AST

关于 AST 的知识这里简单描述一下,目前主流 JS 编译器例如@babel/parser定义的 AST 节点都是根据estree/estree: The ESTree Spec (github.com)规范来的,可以在AST explorer在线演示。

babel 提供的工具库

babel提供了一系列操作 JS AST 的工具库,诸如以下几个

@babel/parser

@babel/parser是 babel 的核心工具之一,原来叫 Babylon,babel6 以后迁移到了 babel 的 menorepo 架构里,作为单独的一个 package 维护。

@babel/parser提供两种解析代码的方法:

  • babelParser.parse(code, [options]):解析生成的代码含有完整的 AST 节点,包含FileProgram层级。

  • babelParser.parseExpression(code, [options]):解析单个 js 语句,这里需要注意的是parseExpression生成的 AST 不完整,所以使用@babel/traverse必须提供scope属性,限定 AST 节点遍历的范围。

 import parser from '@babel/parser';    const code = `function square(n) {    return n * n;  }`;    parser.parse(code); 复制代码

@babel/types

@babel/types用于判断节点类型,生成 AST 节点等操作,例如生成一个函数调用表达式:

 import * as t from '@babel/types';    t.callExpression(t.identifier('xxx'), ...arguments); 复制代码

@babel/traverse

@babel/traverse提供遍历 JS AST 节点的方法,用它来遍历指定类型的节点非常方便,主要是其提供的Visitor模式大大简化了递归的过程。

所谓Visitor模式就是 babel 提供了一个visitor对象来访问指定类型的 AST 节点,例如下面的例子,定义一个visitor,其内部有一个StringLiteral的方法,那么意思就是visit StringLiteral,也就是访问所有字符串类型节点的时候都会触发调用这个定义的StringLiteral方法。

 import babelTraverse, { Visitor } from '@babel/traverse';    const MyVisitor: Visitor = {    StringLiteral() {      console.log('这是一个字符串');    },  };    babelTraverse(ast, MyVisitor); 复制代码

并且由于 babel 采用的深度优先遍历的算法,所以在每个类型的 AST 节点内部还具有两种访问方向 —— enterexit,即进入和离开。

 const MyVisitor: Visitor = {    Identifier: {      enter() {        console.log('Entered!');      },      exit() {        console.log('Exited!');      },    },  }; 复制代码

1638285683847.gif

@babel/generator

@babel/generator就负责将 AST 转换生成代码,同时支持定义生成的部分代码的风格,例如分号结尾、双引号和单引号的使用等。

template-AST

对于 VUE SFC 内部模板语法解析得到的 AST 和 JS 的 AST 区别很大,主要是没有像babel/traverse等处理工具,有的只是@vue/compiler-sfc或者@vue/compiler-dom这样的解析工具。 在 VUE SFC 内部主要存在以下几张类型的 AST 节点:

image-20211130224316277.png

template

template也就是解析<template>内部得到的 AST,其内部的 AST 节点主要分为两种类型propschildrenchildren也就是各种 AST 节点,而props就是元素内部传递的各种指令或者 html 元素的属性。

 export enum NodeTypes {    ROOT = 0,    // 元素节点,包括template元素    ELEMENT = 1,    // 文本类型,包括代码里的一切空白字符,例如换行,空格等    TEXT = 2,    // 注释    COMMENT = 3,    // 表达式,包括模板字符串等    SIMPLE_EXPRESSION = 4,    // 插值    INTERPOLATION = 5,    // 普通属性    ATTRIBUTE = 6,    // 指令的值    DIRECTIVE = 7,    COMPOUND_EXPRESSION = 8,    IF = 9,    IF_BRANCH = 10,    FOR = 11,    TEXT_CALL = 12,    VNODE_CALL = 13,    JS_CALL_EXPRESSION = 14,    JS_OBJECT_EXPRESSION = 15,    JS_PROPERTY = 16,    JS_ARRAY_EXPRESSION = 17,    JS_FUNCTION_EXPRESSION = 18,    JS_CONDITIONAL_EXPRESSION = 19,    JS_CACHE_EXPRESSION = 20,    JS_BLOCK_STATEMENT = 21,    JS_TEMPLATE_LITERAL = 22,    JS_IF_STATEMENT = 23,    JS_ASSIGNMENT_EXPRESSION = 24,    JS_SEQUENCE_EXPRESSION = 25,    JS_RETURN_STATEMENT = 26,  } 复制代码

script

script就是解析<script>标签内部 JS 得到的内容,只不过@vue/compiler-dom或者@vue/compiler-sfc没有将这部分直接解析成 AST,也就是我们还需要利用@babel/parser去解析。

scriptSetup

scriptSetup是 VUE3 以后组件的写法,会在<script>标签内部添加setup属性,这样解析得到的代码就会包含在scriptSetup中而不是在script中。

styles

styles也就是多个<style>标签内部的 CSS 代码。其余的还有customBlockscssVars这些都包含的一样的属性,也就是contentattrsattrs就是标签内部的属性,content就是标签内部的所有格式化代码。

方案细节

整体流程

tvt.png

中文 unicode 码点范围

利用 unicode 码点值来检测代码中是否包含中文字符:

  • 4E00~9FA5是基本汉字

  • 9FA6~9FFF是补充汉字

  • 其他乱七八糟奇形怪状的汉字暂不考虑

 /[\u{4E00}-\u{9FEF}]/gu; 复制代码

编译 SFC

在 VUE SFC 里只需要处理templatescript部分的 AST 来提取中文字符。可以使用@vue/compiler-sfc编译 VUE SFC 文件,可以同时提取出templatescript部分的代码。

template需要处理的文本分为两种情况:

  • 属性字符串

    • 指令内部的 JS 表达式,需要按照 JS 代码解析

    • 普通属性内部的文本字符串,如果包含中文字符,则全部提取

  • 文本子元素

    • 普通的文本子元素,如果包含中文字符,则全部提取

    • 双大括号的文本插值,也要按照 JS 代码解析

script里的代码全部按照 JS 代码解析生成 AST,这样无论是template还是script最终复杂的部分就是针对 JS 代码生成的 AST 的解析。

遍历 JS AST

通过遍历StringLiteral或者TemplateLiteral两种类型的节点就能覆盖所有中文字符的情况。

const visitor: Visitor = {   StringLiteral: {     exit: (path) => {       if (hasChineseCharacter(path.node.extra?.rawValue as string)) {         const locale = (path.node.extra?.rawValue as string).trim();         const key = generateHash(locale);         this.locales[key] = locale;         // 如果是在template内部的JS表达式,使用插值语法         if (!script) {           path.replaceWith(             t.callExpression(t.identifier("$t"), [t.stringLiteral(key)])           );         } else {           path.replaceWith(             t.callExpression(               t.memberExpression(                 t.identifier(this.importVar),                 t.identifier("t")               ),               [t.stringLiteral(key)]             )           );         }       }     },   },   TemplateLiteral: {     exit: (path) => {       // 检测模板字符串内部是否含有中文字符       if (         path.node.quasis.some((q) => hasChineseCharacter(q.value.cooked))       ) {         // 生成替换字符串,注意这里不需要过滤quasis里的空字符串         const replaceStr = path.node.quasis         .map((q) => q.value.cooked)         .join("%s");         const key = generateHash(replaceStr);         this.locales[key] = replaceStr;         let importVar = this.importVar;         // 模板语法使用vue-i18n注入的对象         if (!script) {           importVar = "$i18n";         }         if (path.node.expressions?.length) {           path.replaceWith(             t.callExpression(               t.memberExpression(                 t.identifier(importVar),                 t.identifier("tExtend")               ),               [                 t.stringLiteral(key),                 t.arrayExpression(path.node.expressions as t.Expression[]),               ]             )           );         } else {           // 如果没有内插JS表达式,则使用vue-i18n的简单函数,只填充文案的key           if (script) {             path.replaceWith(               t.callExpression(                 t.memberExpression(                   t.identifier(importVar),                   t.identifier("t")                 ),                 [t.stringLiteral(key)]               )             );           } else {             path.replaceWith(               t.callExpression(t.identifier("$t"), [t.stringLiteral(key)])             );           }         }       }     },   }, }; 复制代码

生成 key 值

目前的解决方案是利用 Nodejs 内部的 hash 函数根据提取的中文字符生成 hash 值,来保证不重复保存相同的中文文案。(预计后续会继续支持以目录为层级的key生成策略,或者提供自定义key生成方法)

 'use strict';    const { createHash } = require('crypto');    export function generateHash(char) {    const hash = createHash('md5');    hash.update(char);    return hash.digest('hex');  } 复制代码

代码转换

一般来说,vue-i18n的使用在<template>内部主要通过$t这样注入的方法,同时每个 VUE 组件中也都会包含一个$i18n对象,那么为了能够对在模板字符串内部的中文字符进行转换。那么我们对$i18n拓展出一个tExtend方法,用于处理在模板字符串内部中文字符的转换情况。

 import Vue from "vue";  import VueI18n from "vue-i18n";  import cn from "./cn.json";    Vue.use(VueI18n);  // 通过选项创建 VueI18n 实例  const i18n = new VueI18n({    locale: "cn", // 设置地区    fallbackLocale: "cn",    messages: {      cn,    },  });    /**   * 转换模板字符串内部%s字符的方法   */  i18n.tExtend = (key, values) => {    let result = i18n.t(key);      if (Array.isArray(values) && values.length) {      values.forEach((v) => {        result = result.replace(/%s/, v);      });    }    return result;  };    export default i18n; 复制代码

例如如下 SFC 内部的插值语法中包含一个 JS 模板字符串如下:

 <template>    <div>      {{ `你的钱包余额:${money}` }}    </div>  </template>    <script>  export default {    data() {      return {        money: 10,      };    },  };  </script> 复制代码

tvt提取的中文为:

 {    "9ef86bfdc5f84d52634c2732a454e3f8": "你的钱包余额:%s"  } 复制代码

自动转换的结果为:

 <div>    {{ $i18n.tExtend('9ef86bfdc5f84d52634c2732a454e3f8', [money]) }}  </div>


作者:0²
链接:https://juejin.cn/post/7038191513880231973

 伪原创工具 SEO网站优化  https://www.237it.com/ 


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐