tiny-engine/docs/extension-capabilities-tuto.../code-output-function/custom-page-code-output-plu...

16 KiB
Raw Permalink Blame History

自定义页面出码插件

页面出码是整体应用出码中的核心组成部分。关于页面出码也有很多的定制化需求,比如 vue 出码可以选择 composition api 的风格、可以选择 options 的风格,可以选择 return jsx 的出码风格。为了满足二次开发用户自定义页面出码的需求。我们提供两种自定的方式,下面是具体介绍:

自定义插件替换页面出码与区块出码

第一种方式,假如用户想出码成 vue options 风格的页面,与官方的出码存在较大差异,则官方建议自定义插件。替换官方的页面出码插件与区块出码插件。 例如:

import { generateApp } from '@opentiny/tiny-engine-dsl-vue'

const customOptionStylePlugin = () => {
  return {
    name: '',
    description: '',
    run(schema, context) {
      // ... 自定义出码逻辑
      const res = '' // ... 这里自定义 option style 的 vue 文件

      const file = {
        fileType: 'vue',
        fileName: 'fileName.vue',
        path: blockBasePath,
        fileContent: res
      }

      this.addFile(file)
    }
  }
}

const instance = generateApp({
  customPlugins: {
    // 替换官方页面出码插件
    page: customOptionStylePlugin(),
    // 替换官方区块出码插件
    block: customOptionStylePlugin(),
  }
})

const res = await instance.generate(appSchema)

基于官方出码自定义

第二种方式,假如用户想在官方出码的基础上进行一些增量或者细节调整,则可以传入 hook 函数到官方插件中。进行出码的细节或者增量调整。

官方页面出码流程

在说明如何传入 hook 到官方插件中实现细节调整之前,我们不妨先看看官方页面的出码流程。对整体页面出码流程有大致的印象。

单页面出码流程图:

页面出码流程

单页面出码流程说明

输入页面 schema

调用 genSFCWithDefaultPlugin(schema) 函数,传入页面 schema

处理生成 template

遍历传入的页面 schema递归处理 schema.children得到 template 内容

// 大致逻辑
function genTemplate(schemWithRes, globalHooks, config) {
  schema.schema.children.map(() => {
    // 存储中间态的数据
    const optionData = {
      schema: schemaItem,
      voidElement: false,
      componentName: componentName ?? component ?? '',
      prefix: [],
      attributes: [],
      children: [],
      suffix: []
    }

    for (const hookItem of [...genTemplateHooks, genTemplate]) {
      // 使用 genTemplateHooks 处理 componentName、attributes、children
      // 数组最后是 genTemplate 函数,即调用自身递归处理 children
    }

    // 拼接得到 jsx比如 <div class="container"></div>
    return `
${optionData.prefix.join('')}${startTag}${optionData.children.join('')}${endTag}${optionData.suffix.join('')}`
  })
}

处理完成最终得到如 <template><div></div></template> 的template 内容

生成 script

从流程图可以看到,我们按顺序调用指定的 hook 生成依赖引入、props 声明、emit 声明、state声明、方法声明、生命周期。最终得到单页面中的 script

<script setup>
import * as vue from 'vue'
/*...*/

const props = defineProps({ ...})

const emits = defineEmits([...])

const state = reactive({ ... })

/** ...method */

/** 生命周期 */
</script>
生成 style

生成 style 的内容很简单,我们只需简单拼接 css 字符串即可,最终得到 style 内容,如:

<style scoped>
//... TinyEngine 生成的 css 字符串
</style>
返回最终结果得到vue单页面文件内容

最终我们拼接得到的 template、script、style最终得到单文件字符串

官方页面出码能力详解

得知了官方页面出码插件的出码流程之后,让我们看看我们能够用他来做些什么。

生成 template

转换 componentName

比如将内置的 Text 组件转换成 html 的 span 标签:

function handleComponentName(optionData) {
  if (optionData.componentName === 'Text') {
    optionData.componentName = 'span'
  }
}
处理 attributes

以官方的 TinyGrid 组件为例,我们可以处理其中的 mock 属性,处理插槽组件的特殊 props。 假如我们有如下的 schema 片段:

{
  "componentName": "TinyGrid",
  "props": {
    // data 和 fetchData 同时存在tinyGrid 会优先使用 data
    // 但是 data 可能仅只有 Mock数据实际生成代码时
    "data": {...},
    "fetchData": {...},
    "columns": [
      {
        "editor": {
          // 这里的 component 当前只是一个字符串,实际出码过程中,我们需要变成 TinySelect 的引用
          // 并且在 script 中声明引入依赖
          "component": "TinySelect"
          // ... 其他属性
        }
      }
    ]

  },
  "children": []
}

则我们可以使用如下的 hooks 进行处理:

const handleTinyGrid = (optionData, globalHooks, config) => {
  const { componentName, props } = optionData.schema

  // 同时存在 data 和 fetchData 的时候,删除 data
  if (componentName === 'TinyGrid' && props?.data && props?.fetchData) {
    delete props.data
  }

  // 处理 TinyGrid 插槽
  if (componentName !== 'TinyGrid' || !Array.isArray(props?.columns)) {
    return
  }

  // 处理 TinyGrid 组件 editor 插槽组件使用 opentiny/vue 组件的场景,需要在 import 中添加对应Tiny组件的引入
  props.columns.forEach((item) => {
    if (item.editor?.component?.startsWith?.('Tiny')) {
      const name = item.editor?.component

      // 使用 globalHooks 往 script 中注入依赖引入
      globalHooks.addImport('@opentiny/vue', {
        destructuring: true,
        exportName: name.slice(4),
        componentName: name,
        package: '@opentiny/vue'
      })

      // 处理 component 组件,变成引用
      item.editor.component = {
        type: 'JSExpression',
        value: name
      }
    }

    if (typeof item.slots === 'object') {
      Object.values(item.slots).forEach((slotItem) => handleTinyGridSlots(slotItem?.value, globalHooks, config))
    }
  })
}

// 传递到页面和区块出码插件
const instance = generateApp({
  pluginConfig: {
    page: { sfcConfig: { genTemplate: [handleTinyGrid] } }
    block: { sfcConfig: { genTemplate: [handleTinyGrid] } }
  }
})
处理children

假如我们的 schema 片段有特殊的 children那么我们可以提前处理而不需要等到递归处理

比如:我们有如下的 schema 片段:

{
  "componentsName": "div",
  "props": {},
  // children 是一个表达式
  "children": {
    "type": "JSExpression",
    "value": "should render ? 'aa' : 'bbb'"
  }
}

则我们可以使用函数处理该 children

const handleExpressionChildren = (schemaData = {}, globalHooks, config) => {
  const { children, schema } = schemaData
  const type = schema?.children?.type
  const isJSX = config.isJSX
  const prefix = isJSX ? '{' : '{{'
  const suffix = isJSX ? '}' : '}}'

  // 如果是 js expression 类型
  if (type === JS_EXPRESSION) {
    specialTypeHandler[JS_RESOURCE](schema.children, globalHooks, config)

    // 直接生成 children 字符串
    children.push(
      `${prefix} ${schema.children?.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') || ''} ${suffix}`
    )

    // 删除 schem.children 属性,避免后续 hooks 重复处理 children
    delete schema.children
    return
  }
}

// 传递到页面和区块出码插件
const instance = generateApp({
  pluginConfig: {
    page: { sfcConfig: { genTemplate: [handleExpressionChildren] } }
    block: { sfcConfig: { genTemplate: [handleExpressionChildren] } }
  }
})

globalHooks使用

相信细心的你已经看到了,在上述的例子中,我们可能会频繁的使用 globalHooks接下来让我们看看 globalHooks 的相关能力。

globalHooks 在 genTemplate、parseScript、genScript 等钩子函数中都可以使用。可以往 script 中注入相关引用,可以往 style 中注入 css。

插入声明语句

我们可以往 script 中插入声明语句

globalHooks.addStatement({
  // position 标识插入位置
  position: 'AFTER_IMPORT',
  // 插入的 javascript 语句
  value: 'const test = "hello world"',
  // 语句的 唯一 key可以避免重复插入如果 key 重复,会导致插入失败
  key: 'test'
})

同样,我们可以使用 globalHooks.getStatements() 获取当前所有的语句

插入、获取方法声明
// 插入方法,第一个参数为 唯一 key唯一 key 冲突则插入不成功,建议使用函数名作为 唯一 key
globalHooks.addMethod('testfun', 'function testfun() { console.log("插入方法成功") }')

// 获取schema以及注册的方法
globalHooks.getMethods()
增加、获取状态变量
globalHooks.addState(key: any, value: any): boolean

// 示例:
// 第一个参数 key 为唯一 key避免冲突第二个参数为增加的 状态变量字符串,为实际向状态变量注入的 key 和 value
globalHooks.addState('test', 'test: "hello world"')

// 注入成功后出码示例:
const state = reactive({
  //... 其他状态变量省略
  test: "hello world"
})

获取状态变量:

// 返回使用 globalHooks.addState 方法注入的 state
globalHooks.getState()
注入、获取依赖
interface IConfig {
  destructuring: boolean; // 是否解构
  exportName: string; // 导出名称
  componentName: string; // 别名
}
// 插入依赖声明
globalHooks.addImport(packageName: string, config: IConfig)

// 获取当前所有的 import
globalHooks.getImport(): IConfig[]

比如,我们想要注入指定的依赖 @vueuse/core

globalHooks.addImport('@vueuse/core', {
  destructuring: true,
  exportName: 'useLocalStorage',
  componentName: 'useLocalStorageAlias'
})

则会生成声明语句:import { useLocalStorage as useLocalStorageAlias } from '@vueuse/core'

设置、获取 script 标签配置

在生成 script 标签的时候,我们可以有:<script setup lang="jsx">,即 setup 和 lang 两个属性配置。如果我们想要设置这两个 config则可以使用该 api比如

globalHooks.setScriptConfig({
  lang: 'jsx', // 或者是 'tsx'
  setup: true // 默认为 true
})

// 获取当前 script 的配置
globalHooks.getScriptConfig()
设置、获取 css 配置

同样的css 的 <style> 可以设置 langscoped 两个属性。

globalHooks.setStyleConfig({
  scoped: true, // 默认为 true
  lang: 'less', // 默认无
})

// 获取当前 style config
globalHooks.getStyleConfig()
添加CSS

我们可以往 css styles 中插入 css

globalHooks.addCss(cssString)

处理 script相关能力

经过 template 的处理之后,我们已经得到了一部分的 statement、import、state、methods。现在我们可以结合 schema 原有的内容,拼装生成我们的 script 内容。拼装过程中,会经历 parseScript 阶段和 拼装 script 的阶段。

parseScript 阶段

该阶段提供使用 globalHooks 处理的最后时机,比如:

// 自定义处理函数
function parseScript(optionData, globalHooks) {
  // 可以使用上述 globalHooks 的能力注入 statement 或者 依赖引入
  globalHooks.addStatement({...})
}

// 传递到页面和区块出码插件
const instance = generateApp({
  pluginConfig: {
    page: { sfcConfig: { parseScript: [parseScript] } }
    block: { sfcConfig: { parseScript: [parseScript] } }
  }
})
拼接 script顺序拼装 script

该部分官方已经内置插件进行组装,原则上不建议用户自行定义。但我们仍然暴露了插槽函数,提供用户替换生成。如:

const instance = generateApp({
  pluginConfig: {
    page: { sfcConfig: { genScript: {
      GEN_IMPORT: function (schema, globalHooks) {
        // 自定义生成 import 语句
        return '....'
      },
      GEN_PROPS: function (schema, globalHooks) {
        // 自定义生成 props 声明语句
        return '....'
      },
      GEN_EMIT: function (schema, globalHooks) {
        // 自定义生成 emit 语句
        return '...'
      }
    } } }
    block: { sfcConfig: { genScript: {
      // ...
    } } }
  }
})

具体可供自定义的插槽为:

  • GEN_IMPORT 生成 import 依赖声明语句
  • GEN_PROPS 生成 props 声明
  • GEN_EMIT 生成 emit 声明
  • GEN_STATE 生成 emit 声明
  • GEN_METHOD 生成 method 声明
  • GEN_LIFECYCLE 生成 lifecycle 声明

页面出码插件 API 说明


interface IConfig {
  pluginConfig: {
    page: {
      pageBasePath: string; //  默认为 './src/views'
      sfcConfig: ISFCConfig;
    },
    block: {
      blockBasePath: string; // 默认为 './src/components'
      sfcConfig: ISFCConfig;
    }
    //... 其余省略
  }
  //... 其余省略
}

function generateApp(config: IConfig): CodeGenInstance

interface ISFCConfig {
  blockRelativePath: string; // 声明 block 依赖引用时的相对路径,默认为 '../components/'
  blockSuffix: string; // 默认为 '.vue'
  scriptConfig: { lang?: 'jsx' | 'tsx'; setup?: boolean };
  styleConfig: { scoped?: boolean; lang?: 'less' | 'scss' };
  hooks: {
    // 对单个 schema 片段进行校验,如果返回 false则该 schema 片段不会被转化成代码
    templateItemValidate: Array<(schemaData: IOptionData, globalHooks: IGlobalHooks, config) => boolean>;
    // 可以处理 componentName、props、children
    genTemplate: Array<(schemaData: IOptionData, globalHooks: IGlobalHooks, config) => void>;
    parseScript: Array<(schema: IPageSchema, globalHooks: IGlobalHooks, config) => void>;
    genScript: {
      GEN_IMPORT?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
      GEN_PROPS?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
      GEN_EMIT?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
      GEN_STATE?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
      GEN_METHOD?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
      GEN_LIFECYCLE?: (schema: IPageSchema, globalHooks: IGlobalHooks) => string;
    }
  }
}

// 页面 schema
interface IPageSchema {
  state: Record<string, any>;
  methods: Record<string, any>;
  componentName: 'Page';
  css: string;
  lifeCycles: Record<string, any>;
  children: Array<ISchema>;
  props: Record<string, any>;
}

// schema 片段
interface ISchema {
  componentName: string;
  props: Record<string, any>;
  children: Array<ISchema>
}

interface IOptionData {
  schema: ISchema;
  voidElement: boolean; // 是否是自闭合标签
  componentName: string; // 生成的 componentName
  prefix: string[]; // 标签前缀,如要生成条件渲染 jsx 时,为 { render && <div></div> },则此时前缀则为 ["{", "render &&"]
  attributes: string[]; // 往 tag 上注入的属性,如 [':visible="true"', 'v-if="shouldRender"']
  children: string[]; // 递归生成的children 字符串
  suffix: string[]; // 配合 prefix 可拼接生成 jsx
}

interface IGlobalHooks {
  addStatement(statement: { key?: string; value: { value: string; position: IInsertPosition } }): boolean;
  getStatements(): Record<string, any>;
  addMethods(key: string, value: any): boolean;
  getMethods(): Record<string, any>;
  addState(key, value): boolean;
  getState(): Record<string, any>;
  addImport(packageName: string, config: { destructuring: boolean; exportName: string; componentName: string; });
  getImport(): Record<string, any>;
  setScriptConfig(config: { lang?: 'jsx' | 'tsx'; setup: boolean }): void; 
  getScriptConfig(): { lang: 'jsx' | 'tsx' | ''; setup: boolean };
  setStyleConfig(config: { lang?: 'jsx' | 'tsx'; scoped: boolean }): void; 
  getStyleConfig(): { lang: 'jsx' | 'tsx' | ''; scoped: boolean };
  addCss(css: string): void;
}

type IInsertPosition = 'AFTER_IMPORT' | 'BEFORE_PROPS' | 'AFTER_PROPS' | 'BEFORE_EMIT' | 'AFTER_EMIT' | 'BEFORE_STATE' | 'AFTER_STATE' | 'BEFORE_METHODS' | 'AFTER_METHODS'