Skip to content

插件设计

@tc-i18n 在解析阶段提供了丰富的钩子用于插件开发,基本每一个解析阶段都可以做自定义编辑。

遵循规范

命名

命名采用 plugin- + 功能说明 形式,采用 小写+短横线,例如 plugin-do-something

结构

插件通常以类的方式提供,插件的入口文件需要导出一个 class 实例。

ts
type PluginOptions = {
    code: string,
    sourceType: string,
    scene?: string,
    config: Tci18nConfig,
};

enum SOURCE_TYPE {
    JS = 'js',
    TS = 'ts',
    JSX = 'jsx',
    TSX = 'tsx',
    VUE = 'Vue',
    HTML = 'html',
    WXML = 'wxml',
    WXS = 'wxs',
}

interface Plugin {
    name: string;  // 插件名称
    exts?: string[];  // 插件需要应用在哪些文件类型上
    sourceTypeScope?: string[] | ((options: PluginOptions) => void | boolean | Record<string, SOURCE_TYPE | undefined>);  // 解析的新文件类型需要按照哪种文件类型进行解析
    apply: (transformer: Hooks, options: PluginOptions) => void; // 插件的执行方法
}

export default class PluginDoSomething implements Plugin {
    ...
}

解析阶段

@tc-i18n 目前支持解析 .html, .js, .ts, .jsx, .tsx, .Vue, .wxml, .wxs 文件,但最终的浏览器能使用的文件只有 .html, .js, 所以将上面的文件类型分成了三类解析器进行解析。

  • .html, .wxml : 按照 HTML 解析器进行解析
  • .js, .ts, .jsx, .tsx : 按照 JS 解析器进行解析
  • .Vue : 按照 Vue 解析器进行解析

内部也提供了 parseHTML, parseJS, parseVUE 三个解析器。

TIP

由于 .Vue 文件是 html + js + css 三种文件混合在一起的,所以 parseVUE 解析器也会需要用到 parseHTML, parseJS 两个解析器,所以存在相互使用。

每个解析器中都提供了 hooks 属性用来存储解析过程,每次解析都分为三个阶段:

  • parse: 解析前的钩子
  • parsing: 解析中的钩子
  • parsed: 解析后的钩子

每个阶段又包含自己的解析步骤,另外不同的解析器,三个阶段的解析步骤不同,具体如下:

parseHTML.hooks

ts
type Hook = {
    parse: Notify<{
        parseCode: (ast: parse5.ParseRootNode) => HookReturn | void;  // 解析html代码前的钩子
        ignoreLines: (lines: string[]) => number[];  // 解析代码中注释行
    }>,
    parsing: {
        parseHTML: Notify<{
            parseNode: (node: parse5.ParseChildNode) => HookReturn | void;  // 解析html节点时
            parseAttrs: (node: parse5.ParseTagNode) => HookReturn | void;  // 解析html当前节点全部属性时
            parseAttr: (node: parse5.ParseTagNode, attr: parse5.ParseAttribute) => HookReturn | void;  // 解析html当前节点具体属性时
            parseInnerText: (node: parse5.ParseTextNode) => HookReturn | void; // 解析html当前节点文本内容时
        }>,  // 解析html中的html代码时
        parseJS: ParseJSType.Hook  // 解析html中的js代码时,具体见 parseJS
    },
    parsed: Notify<{
        parseCode: (code: string) => string;  // 解析html代码后的钩子
    }>,
};

parseJS.hooks

ts
type Hook = {
    parse: Notify<{
        ignoreLines: (comments: ParseResult['comments']) => number[];  // 解析代码中注释行
    }>,
    parsing: Notify<{
        TemplateLiteral: (path: babel.NodePath<babel.types.TemplateLiteral>, t: typeof TType) => void | HookReturn;
        StringLiteral: (path: babel.NodePath<babel.types.StringLiteral>, t: typeof TType) => void | HookReturn;
        ImportDeclaration: (path: babel.NodePath<babel.types.ImportDeclaration>, t: typeof TType) => void | HookReturn;
        ExpressionStatement: (path: babel.NodePath<babel.types.ExpressionStatement>, t: typeof TType) => void | HookReturn;
        Identifier: (path: babel.NodePath<babel.types.Identifier>, t: typeof TType) => void | HookReturn;
        DirectiveLiteral: (path: babel.NodePath<babel.types.DirectiveLiteral>, t: typeof TType) => void | HookReturn;
        ObjectExpression: (path: babel.NodePath<babel.types.ObjectExpression>, t: typeof TType) => void | HookReturn;
        CallExpression: (path: babel.NodePath<babel.types.CallExpression>, t: typeof TType) => void | HookReturn;
        ArrowFunctionExpression: (path: babel.NodePath<babel.types.ArrowFunctionExpression>, t: typeof TType) => void | HookReturn;
        ObjectMethod: (path: babel.NodePath<babel.types.ObjectMethod>, t: typeof TType) => void | HookReturn;
        FunctionDeclaration: (path: babel.NodePath<babel.types.FunctionDeclaration>, t: typeof TType) => void | HookReturn;
        JSXElement: (path: babel.NodePath<babel.types.JSXElement>, t: typeof TType) => void | HookReturn;
        JSXText: (path: babel.NodePath<babel.types.JSXText>, t: typeof TType) => void | HookReturn;
        JSXAttribute: (path: babel.NodePath<babel.types.JSXAttribute>, t: typeof TType) => void | HookReturn;
        makeI18nFunction: (args: Array<babel.types.ArgumentPlaceholder | babel.types.SpreadElement | babel.types.Expression>, t: typeof TType) => void | HookReturn;
    }>,
    parsed: Notify<{
        parseCode: (code: string) => string;  // 解析js代码后的钩子
    }>,
}

parseVUE.hooks

ts
type Hook = {
    parse: Notify<{
        parseSFC: (sfc: vue3Compiler.SFCDescriptor) => void;
        parseCode: (sourceCode: string) => string;
        parseTemplate: (template: vue3Compiler.SFCTemplateBlock) => void;
        parseScript: (script: vue3Compiler.SFCScriptBlock) => void;
    }>,
    parsing: {
        template: ParseHTMLType.Hook,  // 解析vue中的html代码时, 具体见 parseHTML
        script: ParseJSType.Hook  // 解析vue中的js代码时, 具体见 parseJS
    },
    parsed: Notify<{
        parseCode: (code: string, sourceCode: string) => string;
        parseTemplate: (template: vue3Compiler.SFCTemplateBlock) => void;
        parseScript: (script: vue3Compiler.SFCScriptBlock) => void;
    }>,
}

插件编写

插件执行

插件的执行主要是在 apply 函数中取得当前 transformer 解析器示例后,通过 tap 函数订阅不同解析步骤去开发。

ts
export default class PluginDoSomething implements Plugin {
    apply(transformer: tci18nPlugin.Transformer, options: tci18nPlugin.PluginOptions) {}
}

PluginOptions 为插件的配置参数,包含 tci18n.config.json 配置信息。

每次解析新文件时都会创建的transformer实例,并传入 apply 中,也分为三个阶段

  • beforeTransform: 转换文件前的钩子
  • transforming: 转换文件中的钩子,该阶段可以获取到 parseHTML, parseJS, parseVUE 解析器。
  • afterTransform: 转换文件后的钩子

插件主要是在 transforming 阶段,获取到回调回来的 hooks 进行开发。

ts
export default class PluginDoSomething implements Plugin {
    apply(transformer: tci18nPlugin.Transformer, options: tci18nPlugin.PluginOptions) {
        transformer.transforming.tap('transformCode', (hooks) => {
            hooks.parseVUE.parsing.template.parse.tap('ignoreLines', (lines) => {
                // do something..;
            });
            hooks.parseHTML.parse.tap('ignoreLines', (lines) => {
                // do something..;
            });
            hooks.parseVUE.parse.tap('parseCode', (sourceCode) => {
                // do something..
            });
            hooks.parseVUE.parse.tap('parseSFC', (sfc) => {
                // do something..
            });
            hooks.parseVUE.parsed.tap('parseCode', (code, sourceCode) => {
                // do something..
            });
        });
    }
}

hooks 使用

所有 hooks 采用 发布订阅模式 , 通过 tap 函数订阅具体的钩子函数名字并传入回调函数来控制解析器中具体步骤的执行。

每个解析器中的所有的 Notify 类型都支持 tap 函数订阅,并传入回调函数。比如以 parseVUEparse 解析前为例,想对 SFC 结构做自定义,可以使用:

ts
parseVUE.parse.tab('parseSFC', (sfc) => {
    // do something..
})

TIP

hooks 支持链式调用,可以精准定位到具体某个具体码解析。

js
// vue文件解析template中的html代码时候,修改忽略注释行的钩子
hooks.parseVUE.parsing.template.parse.tap('ignoreLines', (lines) => {
    // do something..;
});

查看所有hooks

Plugin类

Plugin.name

  • type: string

插件名称,参考命名规范

Plugin.exts

  • type: string[]

用于指定插件需要应用的文件类型,@tc-i18n 默认支持的 .js, .jsx, .ts, .tsx, .Vue, .wxs, .wxml 类型文件,如需解析其他文件类型,可用该配置自行开发解析逻辑插件。

Plugin.sourceTypeScope

  • type: string[] | Function

指定文件的解析类型,@tc-i18n 支持 html, javascript, Vue 三种类型的代码,如果插件解析的文件不属于默认类型文件,可通过该配置指定文件安装哪种类型进行解析。

Plugin.apply

  • type: (transformer: Hooks, options: PluginOptions) => void

插件执行函数,当 @tc-i18n 解析文件时,会执行该函数,并传入当前 transformer 实例和插件配置参数。