国际化第一步:实现分析js,vue文件提取中文,ast分析,vscode插件开发

2,014 阅读7分钟

前言

这篇文章更多是炫耀成分,会讲核心技术突破点,但是代码实现和技术思路太多了。我花了两周的时间不断的分析vue解析思路,ast语法树。翻看各个代码库的源码和各种官方文档。中间有太多的突破点了。

文章会主要讲解实现的思路部分,相信对于大部分的大佬技术到了一定程度缺的不是编码能力而是思路。俗话说师傅领进门,修行靠个人。

领进门这一步太重要了了

先上代码:github.com/ht-sauce/vs…

功能:

将会把vue,js,ts文件提取中文代码改为$t()方式
生成parrot-extract-out文件夹,翻译提取内容在该文件夹中

1、前置知识和库

AST:astexplorer.net/

AST定义:github.com/estree/estr…

eslint:eslint.org/docs/latest…

vue-eslint-parser解析器:www.npmjs.com/package/vue…

2、算错也不算错的方向babel和@vue/compiler-sfc

特别复杂的处理方向:不知道算不算正确的方式

工具开发初期都知道vue在生成可执行代码之前都是通过@vue/compiler-sfc进行解析,并且生成js文件运行的。但是这里最大的错误也是这里,也许是我学艺不精导致的问题。至少我能找到的源码库都是自己写反解析逻辑实现vue文件的反向生成的。

举例:

文章:VUE 国际化自动提取和转换

我搞了个可以全自动化国际化的工具...

前端国际化自动工具-国际化文案配置系统

代码库:

i18n-scripts:github.com/AlbertLin09…

**i18n-parser:**github.com/wood3n/i18n…

**di18n:**github.com/didi/di18n

这所有的我前期找到的文章全部都使用的@vue/compiler-sfc或者vue-template-compiler实现vue文件的解析然后自己写了一段超级复杂的反解析逻辑进行生成vue文件

利用vue的解析库将文件分为template部分和script部分调用不同的解析器进行解析,得到ast逻辑进行处理

我的第一版代码也是基于此进行实现,可以参考我的代码库的

分支:@vue/compiler-sfc方向错误,留存代码

当时我已经完成了template的分析和提取,也不算完全完成。因为template部分存在纯js代码书写部分,这部分我当时想着是遇到这段定义走babel分析,再二次处理进行提取再反向生成代码。但是我最后放弃了,我看了众多源码发现大家都自己写反解析逻辑。我也要这么干总感觉是错误的。并且我没有从vue官方的@vue/compiler-sfc找到关于template根据ast生成template部分的代码。

意识到错误了,但是又不想放弃代码

1、翻看vue源码和create-vue在全面转向vite,vite内部解析不再使用babel。

2、从vuecli部分看babel的解析,只有正向vue生成js文件的逻辑,没有反向逻辑

最后无意间翻看到一篇文章,对方已经用@vue/compiler-sfc实现文件提取,但是提到转向使用eslint-plugin-vue进行处理

我想到vue也是可以用eslint进行文件处理的,并且eslint是支持反向生成文件代码的。那么是否可以呢。首先我也使用了eslint-plugin-vue,但是定义了非常多的规则,这些规则对我来说是没用的,任何一个规则报错都会导致解析的终止和失败

然后翻eslint-plugin-vue源码,vue官方上来说使用了自己的解析器vue-eslint-parser

那么理论上是可行的

3、eslint加vue-eslint-parser实现vue文件解析和提取

主要知识点,配置eslint,自定义规则

1、核心配置文件

import { ESLint } from 'eslint'
import ChineseExtract, { meta } from '../plugins/chinese-extract/imort'
const tsParser = require('@typescript-eslint/parser')
const espree = require('espree')
const vueParser = require.resolve('vue-eslint-parser')

// eslint配置提取
export async function analysis(url: string) {
  // console.log(vueParser)
  const eslint = new ESLint({
    fix: true, // 是否自动修复
    plugins: { [meta.name]: ChineseExtract }, // 加载自定义的插件
    overrideConfig: {
      // 加载插件自定义的配置
      // 原理:根据已经加载的插件去读取插件下面的配置信息
      extends: ['plugin:parrot/extract'],
      // 添加vue和ts解析功能
      parser: vueParser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: {
          jsx: true,
        },
        parser: {
          js: espree,
          jsx: espree,
          ts: tsParser,
          tsx: tsParser,
        },
      },
      rules: {
        'prettier/prettier': 'off',
      },
    },
  })

  // 检查文件
  const results = await eslint.lintFiles([url])
  // console.log(results)
  // 输出回原文件
  await ESLint.outputFixes(results)
}

细节:

1、本地运行是没有问题的,但是插件和解析器在vscode环境下是不一样的需要改为绝对路径导入

const vueParser = require.resolve('vue-eslint-parser')

2、extends: ['plugin:parrot/extract']导入方式是必须的,否则规则解析是不成功的

2、编写eslint插件,并且自定义规则

import { ESLint, Rule } from 'eslint'
import * as ESTree from 'estree'
import { PrivateIdentifier } from 'estree'
import type { AST } from 'vue-eslint-parser'
import { entryWordBar } from '../../store/term-bank'
import { ASTType } from '../../tool/ast'
import { unmatchedIdentifier } from '../../tool/string'
import { replaceText } from './replace'
import { FileType, ReplaceType } from '../../store/types'
import { globalStatus } from '../../store/global-status'

export const meta = {
  name: 'eslint-plugin-parrot',
  version: '1.0.0',
}

export default {
  rules: {
    // 实际规则名称parrot/chinese-extract
    'chinese-extract': {
      meta: {
        /*
         "problem"表示规则正在标识将导致错误或可能导致混淆行为的代码。开发人员应将此视为高度优先解决的问题。
        "suggestion"意味着规则正在识别可以以更好的方式完成的事情,但如果不更改代码就不会发生错误。
        "layout"意味着规则主要关心空格、分号、逗号和括号,程序的所有部分决定代码的外观而不是代码的执行方式。这些规则适用于 AST 中未指定的部分代码。
                * */
        type: 'suggestion',
        // 定义提示信息文本 error-name为提示文本的名称 定义后我们可以在规则内部使用这个名称
        messages: {
          // 'error-name': '这是一个错误的命名',
        },
        docs: {
          description: '检测使用了中文并进行提取',
        },
        // 标识这条规则是否可以修复,假如没有这属性,即使你在下面那个create方法里实现了fix功能,eslint也不会帮你修复
        fixable: 'code',
        // 这里定义了这条规则需要的参数
        // 比如我们是这样使用带参数的rule的时候,rules: { myRule: ['error', param1, param2....]}
        // error后面的就是参数,而参数就是在这里定义的
        schema: [],
      },
      create(context: Rule.RuleContext): Rule.RuleListener {
        // console.log(1111, context.parserServices)
        // 原始ast方式
        // return {
        //   // 在ReturnStatement节点上
        //   ReturnStatement(node) {},
        //   // 在开始分析代码路径时
        //   onCodePathStart(codePath, node) {
        //     // console.log(codePath, node)
        //   },
        //   // 在分析代码路径结束时
        //   onCodePathEnd(codePath, node) {},
        //   // onCodePathSegmentStart(segment, node) {},
        //   // onCodePathSegmentEnd(segment, node) {},
        //   // onCodePathSegmentLoop(fromSegment, toSegment, node) {},
        // }
        // vue解析器提供的方式
        return context.parserServices.defineTemplateBodyVisitor(
          // <template>部分走这里
          /* 存在的节点类型,ast原生类型未列出
            VAttribute: ["key", "value"],
            VDirectiveKey: ["name", "argument", "modifiers"],
            VDocumentFragment: ["children"],
            VElement: ["startTag", "children", "endTag"],
            VEndTag: [],
            VExpressionContainer: ["expression"],
            VFilter: ["callee", "arguments"],
            VFilterSequenceExpression: ["expression", "filters"],
            VForExpression: ["left", "right"],
            VIdentifier: [],
            VLiteral: [],
            VOnExpression: ["body"],
            VSlotScopeExpression: ["params"],
            VStartTag: ["attributes"],
            VText: [],*/
          {
            TemplateElement(node: ESTree.TemplateElement) {
              const entryStatus = entryWordBar(node.value.raw)
              replaceText({
                node,
                entryStatus,
                context,
                replaceType: ReplaceType.vueTemplate,
              })
            },
            Literal(node: Rule.Node): void {
              const parent = node?.parent as ESTree.CallExpression
              if (parent && parent.type === ASTType.CallExpression) {
                if (
                  parent.callee.type === ASTType.Identifier &&
                  unmatchedIdentifier(parent.callee.name)
                )
                  return
              }
              const entryStatus = entryWordBar((node as ESTree.Literal).value as string)
              replaceText({
                node,
                entryStatus,
                context,
                replaceType: ReplaceType.vueTemplate,
              })
            },
            VLiteral(node: AST.VLiteral): void {
              const entryStatus = entryWordBar(node.value)
              // 父级节点需要改为冒号方式,传递父节点
              replaceText({
                node: node.parent,
                entryStatus,
                context,
                replaceType: ReplaceType.vueTemplate,
              })
            },
            VText(node: AST.VText): void {
              // console.log(node)
              const entryStatus = entryWordBar(node.value)
              replaceText({
                node,
                entryStatus,
                context,
                replaceType: ReplaceType.vueTemplate,
              })
            },
          },
          // Event handlers for <script> or scripts. (optional)
          // js,ts部分会走这里
          {
            // // 在开始分析代码路径时
            // onCodePathStart(codePath: string, node: any) {
            //   console.log(codePath, node)
            // },
            // // 最大的节点,最先出现的部分
            // Program(node: AST.ESLintProgram): void {
            //   console.log(node)
            // },
            Literal(node: Rule.Node): void {
              // console.log(node)
              const parent = node?.parent as ESTree.CallExpression
              if (parent && parent.type === ASTType.CallExpression) {
                if (
                  parent.callee.type === ASTType.MemberExpression &&
                  unmatchedIdentifier((parent.callee.property as PrivateIdentifier).name)
                )
                  return
              }
              const entryStatus = entryWordBar((node as ESTree.Literal).value as string)
              const { fileType } = globalStatus
              // 默认为js文件处理方式
              let replaceType = ReplaceType.js
              // 不同文件下的判断处理
              if (fileType === FileType.vue) replaceType = ReplaceType.vueOptions
              replaceText({ node, entryStatus, context, replaceType })
            },
            TemplateElement(node: ESTree.TemplateElement) {
              const entryStatus = entryWordBar(node.value.raw)
              replaceText({
                node,
                entryStatus,
                context,
                replaceType: ReplaceType.js,
              })
            },
          },
          // Options. (optional)
          {
            templateBodyTriggerSelector: 'Program:exit',
          },
        )
      },
    },
  },
  configs: {
    extract: {
      plugins: ['parrot'], // 插件的前缀名称
      rules: {
        'parrot/chinese-extract': 'error',
      },
    },
  },
} as ESLint.Plugin

致谢

本文主要是提供一个思路,代码的讲解和解析基本全无,但是对于没有思路的大家会是一个绝佳的方式。

该项目已经正式投产

这鬼掘金的文本编辑器现在怎么这么丑啊