npm package: @ga23187/vue3-json-schema-form
Description: 课程地址：https://coding.imooc.com/class/466.html
Latest version: 0.1.0

课程地址：https://coding.imooc.com/class/466.html

> 源码地址:：https://github.com/BestVue3/vue3-jsonschema-from
>
> 类型开源项目：https://github.com/lljj-x/vue-json-schema-form

# 项目初始化

## 开始

> 借助vue-cli5

![image-20230226015636836](images/image-20230226015636836.png)

初始目录

![image-20230226015934257](images/image-20230226015934257.png)

## prettier

> 一个代码格式化工具

- 上面项目创建时已经选择了`prettier`，在node_modules中已经存在依赖了，现在只需要在`vscode`中也装一下这个插件方便在开发时也格式化就行了。

- 然后在项目根目录创建`prettierrc`文件

  ```
  {
    "semi": false, // 是否使用分号
    "singleQuote": true, // 是否使用单引号
    "arrowParens": "always", // 匿名函数单个参数是否需要带括号
    "trailingComma": "all",
    "endOfLine": "auto" // 不让prettier检测文件每行结束的格式
  }
  ```

- 然后配置vscode保存触发代码格式化

  - 设置中的`Editor`下的`format on Save`
  - 注意vscode配置分用户与工作目录下，二种权限，一般使用工作目录下的权限

- 记得重启一下vscode，不然eslint可能会提示有问题，估计是配置的prettier配置文件没有读取到eslint下

## eslint

> 补充
>
> vue-cli5生成的.eslintrc.js配置文件中缺少vue3的宏定义相关的api即`defineProps`这些
>
> 需要添加
>
> ```
>  env: {
>   node: true,
>   'vue/setup-compiler-macros': true,// 这个
> 
>  },
> ```
>
> 参考:[【Vue3】解决‘defineProps‘ is not defined报错](https://blog.csdn.net/harmsworth2016/article/details/122771816)

## TS组件定义

Component接口

defineComponent函数

接收4种调用方式

```ts
import { defineComponent,ref } from 'vue'

export default defineComponent(function HelloWorld() {
  const msg = ref('Hello world')
  return { msg }
})
```

```ts
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  components: {
    HelloWorld,
  },
})
```

定义Props类型

```ts
import type { DefineComponent } from 'vue'

type myComponents = DefineComponent<{ a: string }>
```



## 提取props定义

提取之前，required是有效的

![image-20230226220559655](images/image-20230226220559655.png)

提取之后，无效了

![image-20230226220711331](images/image-20230226220711331.png)

解决办法：`as const`

![image-20230226220853717](images/image-20230226220853717.png)

原因: vue源码中这一段

![image-20230226221636698](images/image-20230226221636698.png)



## `h`函数

```ts
import { createApp, defineComponent, h } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
// import App from './App.vue'
// import img from './assets/logo.png'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const img = require('./assets/logo.png')

const App = defineComponent({
  render() {
    return h(
      'div',
      {
        id: 'app',
      },
      [
        h('img', {
          alt: 'Vue logo',
          //   src: './assets/logo.png', // 在template中的src引入会经过webpack的loader处理，这里直接写字符串是不出处理的
          src: img,
        }),
        h(HelloWorld, {
          msg: 'Hhhh',
          age: 123,
        }),
      ],
    )
  },
})
createApp(App).mount('#app')
```

`template`最终也是被编译为`render`函数

`h`函数实现

![image-20230226224231274](images/image-20230226224231274.png)



## `setup`运用和意义

### setup返回一个对象

```ts
<script lang="ts">
import {
  computed,
  defineComponent,
  reactive,
  ref,
  toRefs,
  watchEffect,
} from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export default defineComponent({
  name: 'App',
  components: {
    HelloWorld,
  },
  // setup(props, { slots, attrs, emit }) {
  //   console.log(props, slots, attrs, emit)
  //   const state = reactive({
  //     name: 'zyq',
  //   })
  //   setInterval(() => {
  //     state.name += 1
  //   }, 1000)
  //   return {
  //     ...toRefs(state), // 解构会丢失响应式，借助toRefs方法重新响应式
  //   }
  // },
  setup(props, { slots, attrs, emit }) {
    console.log(props, slots, attrs, emit)
    const name = ref('zyq')

    // setInterval(() => {
    //   name.value += 1
    // }, 1000)

    const computedName = computed(() => {
      return name.value + 2
    })

    watchEffect(() => {
      console.log(name.value) // 只会监听里面出现过的响应式对象
    })

    return {
      name,
      computedName,
    }
  },
})
</script>
```

### setup返回一个`render`

```ts
import { createApp, defineComponent, h, reactive, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
// import App from './App.vue'
// import img from './assets/logo.png'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const img = require('./assets/logo.png')

const App = defineComponent({
  setup() {
    const state = reactive({
      name: 'zyq',
    })
    const number = ref(1)

    setInterval(() => {
      state.name += 1
      number.value += 1
    }, 1000)

    // 无效（setup只会执行一次，所以在这里只会执行一次，而render函数是会随着响应式数据的更新重新执行的，所以下面的numberX会重新赋值）
    // const numberX = number.value

    // 返回一个函数 闭包 可以访问外层函数定义的变量
    return () => {
      // 有效
      const numberX = number.value
      return h(
        'div',
        {
          id: 'app',
        },
        [
          h('img', {
            alt: 'Vue logo',
            //   src: './assets/logo.png',
            src: img,
          }),
          //   h('p', state.name + '|' + number.value),
          h('p', state.name + '|' + numberX),
        ],
      )
    }
  },
})
```



## 使用jsx开发vue组件

### JSX支持

安装

```
npm install @vue/babel-plugin-jsx -D
```

`babel.config.js`添加如下配置

```
{
  "plugins": ["@vue/babel-plugin-jsx"]
}
```

### 使用

JSX的文件需要以`jsx`或`tsx`结尾。

新建一个`App.tsx`文件

```tsx
import { defineComponent, reactive, ref } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const img = require('./assets/logo.png')

export default defineComponent({
  setup() {
    const state = reactive({
      name: 'zyq',
    })
    const number = ref(1)

    setInterval(() => {
      state.name += 1
      number.value += 1
    }, 1000)
    // 无效（setup只会执行一次，所以在这里只会执行一次，而render函数是会随着响应式数据的更新重新执行的，所以下面的numberX会重新赋值）
    // const numberX = number.value

    return () => {
      // 有效
      const numberX = number.value
      return (
        <div id="app">
          <img src={img} alt="Vue logo" />
          <p>{state.name + '|' + numberX}</p>
        </div>
      )
    }
  },
})
```

vue2--->vue3 的jsx变化

拍平了属性

下图就式rfc中关于vue2到vue3的变化

![image-20230227000228012](images/image-20230227000228012.png)



### 特色

比单文件组件`更灵活`

```tsx
import HelloWorld from './components/HelloWorld.vue'

export default defineComponent({
  setup() {
    const renderHellow = (age: number) => {
      return <HelloWorld age={age} />
    }
    return () => {
      return (
        <div id="app">
          {/* jsx可以让eslint知道age是必填的 */}
          {/* <HelloWorld age={12} /> */}
          {renderHellow(12)}
        </div>
      )
    }
  },
})
```

> 解决eslint报错了，但是**vscode没有提示HelloWorld必填属性为空**
>
> 原因是因为所有的单文件组件定义都是在`shims-vue.d.ts`中定义的，ts就检测不出来你使用错误了。
>
> 解决办法就是把`HelloWorld`改为jsx文件

### 使用指令

```tsx
export default defineComponent({
  setup() {
    const inputValue = ref('')
    return () => {
      console.log(inputValue.value, 'inputValue.value')
      return (
        <div id="app">
          <input type="text" v-model={inputValue.value} />
        </div>
      )
    }
  },
})
```



# JsonSchema

> 一种规范，用于定义JSON数据，校验JSON数据。不同语言有不同的实现方式。
>
> [官方定义](https://json-schema.org/)
>
> [JSON Schema 规范（中文版）](https://json-schema.apifox.cn/)

这里我们使用[ajv](https://ajv.js.org/)这个库。注意我学习的课程的版本是`6`，目前最新的是`8`。

## 安装`ajv`

```bash
npm install ajv
```

>   "ajv": "^8.12.0"

> 当前这个项目使用的版本不是最新的，所以需要指定下版本，不然后续学习可能会需要调整代码，
>
> 需要指定为
>
>   "ajv": "6.12.4"
>
> 不过下面的简单用法学习记录还是用的最新版本的

## 简单使用`ajv`

新建一个文件夹`schema-tests`，下面创建一个`test.js`文件

```js
const Ajv = require('ajv')
// 实例化
const ajv = new Ajv() // options can be passed, e.g. {allErrors: true}
// 定义的schema
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string' },
    pets: {
      type: 'array',
      items: {
        type: 'string',
      },
    },
    petss: {
      type: 'array',
      items: [
        {
          type: 'string',
        },
        {
          type: 'number',
        },
      ],
      minItems: 2,// 最新版本ajv 不添加这2个属性
      additionalItems: false,//会出现strict mode: "items" is 2-tuple, but minItems or maxItems/additionalItems are not specified or different at path "#/properties/petss"
    },
  },
  required: ['foo'],
  additionalProperties: true,
}
// 编译
const validate = ajv.compile(schema)
// 需要校验的数据
const data = {
  foo: 1,
  bar: '1',
  pets: ['1', '2'],
  petss: ['1', 2],
}
// 校验
const valid = validate(data)
// 校验不通过 打印下
if (!valid) console.log(validate.errors)
```

## format

> 最新版本 Ajv does not include any formats, they can be added with [ajv-formats (opens new window)](https://github.com/ajv-validator/ajv-formats)plugin.

安装

```bash
npm i ajv-formats
```

>   "ajv-formats": "^2.1.1"

使用

```js
// ESM/TypeScript import
import Ajv from "ajv"
import addFormats from "ajv-formats"
// Node.js require:
const Ajv = require("ajv")
const addFormats = require("ajv-formats")

const ajv = new Ajv()
addFormats(ajv)

const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', format: 'email' },
  },
}
```

## 自定义format

> 通过ajv的方法来拓展，注意不是JsonSchema的规范

```js
const Ajv = require('ajv')
// 实例化
const ajv = new Ajv()
// 自定义format
ajv.addFormat('test', (data) => {
  console.log(data, '--------')
  return data === 'hh'
})

const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', format: 'test' },
  },
}
```

## 自定义关键字

Ajv 允许以下4种方式来定义关键字

- code generation function (used by all pre-defined keywords)
- validation function
- compilation function
- macro function

### validation function

```js
const Ajv = require('ajv')

// 实例化
const ajv = new Ajv() // options can be passed, e.g. {allErrors: true}
// 自定义关键字
ajv.addKeyword({
  keyword: 'test',
  validate: (schema, data) => {
    console.log(schema, data)
    return true
  },
})
// 定义的schema
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', test: 'schema' },
  },
  required: ['foo'],
  additionalProperties: true,
}
// 编译
const validate = ajv.compile(schema)
// 需要校验的数据
const data = {
  foo: 1,
  bar: 'bar',
}
// 校验
const valid = validate(data)
// 校验不通过 打印下
if (!valid) console.log(validate.errors)
```

### compilation function

编译阶段就会执行

```js
const Ajv = require('ajv')

// 实例化
const ajv = new Ajv() 
// 自定义关键字  
ajv.addKeyword({
  keyword: 'test',
  compile(param, parentSchema) {
    console.log(param, parentSchema)
    return () => true
  },
  metaSchema: {
    // 定义test关键字接收值的定义
    type: 'array',
    items: [{ type: 'number' }, { type: 'number' }],
    minItems: 2,
    additionalItems: false,
  },
})
// 定义的schema
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', test: 'schema' },
  },
  required: ['foo'],
  additionalProperties: true,
}
// 编译
const validate = ajv.compile(schema)
```

### macro function

相对于起了个别名，实际的校验规则会与return的合并

```js
// ...
ajv.addKeyword({
  keyword: 'test',
  macro: (data) => {
    console.log(data)
    return {
      minLength: 10,
    }
  },
})
// ...
```

## 错误信息语言修改

安装`ajv-i18n`

```bash
npm install ajv-i18n
```

>  "ajv-i18n": "^4.2.0"

使用

```js
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
const localize = require('ajv-i18n')
// ....
// 校验不通过 打印下
if (!valid) {
  localize.zh(validate.errors) // 只需要这里使用下对应的语言就可以了。
  console.log(validate.errors)
}
```

> 老版本的自定义关键字的错误信息语言是不支持的，最新版本的可以了。

## 自定义错误信息

安装

```bash
npm install ajv-errors
```

> "ajv-errors": "^3.0.0"

使用

> 注意new Ajv({ allErrors: true })中需要开启allErrors

```js
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
const localize = require('ajv-i18n')

// 实例化
const ajv = new Ajv({ allErrors: true })
// Ajv option allErrors is required
require('ajv-errors')(ajv /*, {singleError: true} */)
// 拓展format
addFormats(ajv)
// 自定义关键字
ajv.addKeyword({
  keyword: 'test',
  macro: (data) => {
    console.log(data)
    return {
      minLength: 10,
    }
  },
})
// 定义的schema
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: {
      type: 'string',
      //   test: 'test',
      minLength: 10,
      errorMessage: '会替换当前整个错误信息',
      //   errorMessage: {
      //     type: '必须是字符串',
      //     minLength: '长度不能小于10',
      //   },
    },
  },
  required: ['foo'],
  additionalProperties: true,
}
// 编译
const validate = ajv.compile(schema)
// 需要校验的数据
const data = {
  foo: 1,
  bar: 1,
}
// 校验
const valid = validate(data)
// 校验不通过 打印下
if (!valid) {
  //   localize.zh(validate.errors) //  好像会和自定义error冲突
  console.log(validate.errors)
}
```

## 总结

后续需要按照课程来，所以需要指定按照以下的版本来学习，因为使用最新的，需要修改用法，等有空再改，先学思路。

>  "ajv": "6.12.4"
>
>  "ajv-i18n": "3.5.0"
>
> "ajv-errors": "1.0.1"

# 实现组件库的主流程

- 确定组件的接口与定义（即props）
  - schema
  - value
  - local
  - onChange
  - uiSchema
  - 等等
- 开发入口组件的实现
- 开发基础渲染实现

## API设计

```jsx
<JsonSchemaForm
    schema={schema}
    value={value}
    onChange={onChange}
    locale={locale}
    contextRef={someRef}
    uiSchema={uiSchema}></JsonSchemaForm>
```

### schema

json schema对象，用来定义数据，同时也是我们定义表单的依据

### value

表单的数据结果，你可以从外部改变这个value，在表单被编辑的时候，会通过`onChange`透出 value

需要注意的是，因为vue使用的是可变数据，如果每次数据变化我们都去改变`value`的对象地址，那么会导致整个表单都需要重新渲染，这会导致性能降低。
从实践中来看，我们传入的对象，在内部修改其 field 的值基本不会有什么副作用，所以我们会使用这种方式来进行实现。也就是说，如果`value `是一个对象，那么从`JsonSchemaForm`内部修改的值，并不会改变`value`对象本身。我们仍然会触发`onChange`，因为可能在表单变化之后，使用者需要进行一些操作。

### onChange

在表单值有任何变化的时候会触发该回调方法，并把新的值进行返回

### locale

语言,使用`ajv-i18n`指定错误信息使用的语言1

### contextRef

你需要传入一个 vue3的`Ref`对象，我们会在这个对象上挂载`doValidate`方法，你可以通过

```ts
<JsonSchemaForm contextRef={yourRef} />
  
const yourRef=ref({})
onMounted(()=>{
    yourRef.value.doValidate()
})
```

### uiSchema

对表单的展现进行一些定制，其类型如下:

```ts
export interface VueJsonSchemaConfig {
    title?:string
    description?: string
    component?: string
    additionProps?:{
        [key:string]:any
    }
    withFormItem?: boolen
    widget?:'checkbox'|'textarea'|'select'|'radio'|'range'|string
    items:UISchema | UISchema[]
}
export interface UISchema extends VueJsonSchemaConfig {
    properties?:{
        [property:string]:UISchema
    }
}
```

## 基本demo

- 移除之前写的一些代码

- 安装`monaco-editor `   

  ```bash
  npm i -D monaco-editor@0.20 
  ```

- `vue-jss` css in js的库

  ```bash
  npm i vue-jss@0.0.4 jss@10.4.0 jss-preset-default@10.4.0
  ```

- 就可以新增一个`MonacoEditor.tsc`组件

  ```tsx
  import * as Monaco from 'monaco-editor'
  import {
    defineComponent,
    onBeforeMount,
    onMounted,
    PropType,
    ref,
    shallowRef,
    watch,
  } from 'vue'
  import { createUseStyles } from 'vue-jss'
  
  const useStyles = createUseStyles({
    container: {
      border: '1px solid #eee',
      display: 'flex',
      flexDirection: 'column',
      borderRadius: 5,
      // height: 500,
    },
    title: {
      backgroundColor: '#eee',
    },
    code: {
      flexGrow: 1,
    },
  })
  
  export default defineComponent({
    props: {
      code: {
        type: String as PropType<string>,
        required: true,
      },
      onChange: {
        type: Function as PropType<
          (value: string, event: Monaco.editor.IModelContentChangedEvent) => any
        >,
        required: true,
      },
      title: {
        type: String as PropType<string>,
        required: true,
      },
    },
    setup(props) {
      const editorRef = shallowRef()
      const containerRef = ref()
  
      let _subscription: Monaco.IDisposable | undefined,
        __prevent_tigger_change_event = false
  
      onMounted(() => {
        const editor = (editorRef.value = Monaco.editor.create(
          containerRef.value,
          {
            value: props.code,
            language: 'json',
            formatOnPaste: true,
            tabSize: 2,
            minimap: {
              enabled: false,
            },
          },
        ))
        _subscription = editor.onDidChangeModelContent((event) => {
          console.log('>>>>>', __prevent_tigger_change_event)
          if (!__prevent_tigger_change_event) {
            props.onChange(editor.getValue(), event)
          }
        })
      })
  
      onBeforeMount(() => {
        if (_subscription) {
          _subscription.dispose()
        }
      })
      watch(
        () => props.code,
        (v) => {
          const editor = editorRef.value
          const model = editor.getModel()
          if (v !== model.getValue()) {
            editor.pushUndoStop()
            __prevent_tigger_change_event = true
            model.pushEditOperations(
              [],
              [
                {
                  range: model.getFullModeRange(),
                  text: v,
                },
              ],
            )
            editor.pushUndoStop()
            __prevent_tigger_change_event = false
            //   if (v !== editorRef.value.getValue()) {
            //     editorRef.value.setValue()
            //   }
          }
        },
      )
  
      const classesRef = useStyles()
  
      return () => {
        const classes = classesRef.value
  
        return (
          <div class={classes.container}>
            <div class={classes.title}>
              <span>{props.title}</span>
            </div>
            <div class={classes.code} ref={containerRef}></div>
          </div>
        )
      }
    },
  })
  
  ```

- 在`app.tsx`中使用下

  ```tsx
  import { defineComponent, ref } from 'vue'
  import MonacoEditor from './components/MonacoEditor'
  import { createUseStyles } from 'vue-jss'
  
  function toJson(data: any) {
    return JSON.stringify(data, null, 2)
  }
  
  const schema = {
    type: 'string',
  }
  
  const useStyles = createUseStyles({
    editor: {
      minHeight: 400,
    },
  })
  
  export default defineComponent({
    setup() {
      const schemaRef = ref<any>(schema)
  
      const handleCodeChange = (code: string) => {
        let schema: any
        try {
          schema = JSON.parse(code)
        } catch (err) {
          console.log(err)
        }
        schemaRef.value = schema
      }
  
      const classesRef = useStyles()
  
      return () => {
        const code = toJson(schemaRef.value)
        const classes = classesRef.value
  
        return (
          <div>
            <MonacoEditor
              code={code}
              onChange={handleCodeChange}
              title="Schema"
              class={classes.editor}
            />
          </div>
        )
      }
    },
  })
  
  ```




## 简单展示APP

![image-20230304000755000](images/image-20230304000755000.png)

核心是`lib`的实现，待到下节实现。

## lib实现之SchemaForm

核心 就是根据用户设置的schema来渲染对应的formItem

// SchemaForm.tsx

```tsx
import { defineComponent, PropType } from 'vue'
import { Schema, SchemaTypes } from './types'

export default defineComponent({
  props: {
    schema: {
      type: Object as PropType<Schema>,
      required: true,
    },
    value: {
      //   type: Object, 值的类型是不确定的，可能是object也可能是string
      required: true,
    },
    onChange: {
      type: Function as PropType<(v: any) => void>,
      required: true,
    },
  },
  name: 'SchemaForm',
  setup(props, { slots, emit, attrs }) {
    return () => {
      // 解析schema
      const schema = props.schema
      const type = schema?.type
      switch (type) {
        //根据不同的类型渲染不同的表单项，对应string和number这种单一的还好，但是object这种可能自身就是一个完整的表单，所以不太应该把他们的代码放一起，不太合适，可以设计一个中间状态器，通过它来分发。
        case SchemaTypes.STRING: {
          return <input type="text" />
        }
      }
      return <div>This is 123</div>
    }
  },
})

```

// types.ts

```ts
export enum SchemaTypes {
  'NUMBER' = 'number',
  'INTEGER' = 'integer',
  'STRING' = 'string',
  'OBJECT' = 'object',
  'ARRAY' = 'array',
  'BOOLEAN' = 'boolean',
}

type SchemaRef = { $ref: string }
export interface Schema {
  type: SchemaTypes | string
  const?: any
  format?: string
  default?: any
  properties?: {
    [key: string]: Schema | { $ref: string }
  }
  items?: Schema | Schema[] | SchemaRef
  dependencies?: {
    [key: string]: string[] | Schema | SchemaRef
  }
  oneOf?: Schema[]
  //vjsf?: VueJsonSchemaConfig
  required?: string[]
  enum?: any[]
  enumKeyValue?: any[]
  additionalProperties?: any
  additionalItem?: Schema
}
```



## lib实现之SchemaItem

定义一个中间状态器，用于分发不同的表单项。根据不同的类型来把渲染组件的工作交给对应的组件实现。

创建一个fields文件夹存放对应的实现，先实现下Number与String。

```tsx
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'StringField',
  setup() {
    return () => <div>String field</div>
  },
})
```

```tsx
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'NumberField',
  setup() {
    return () => <div>Number field</div>
  },
})
```

在`SchemaItem.tsx`中使用

```tsx
import { defineComponent, PropType } from 'vue'
import NumberField from './fields/NumberField'
import StringField from './fields/StringField'
import { Schema, SchemaTypes } from './types'

export default defineComponent({
  name: 'SchemaItem',
  props: {
    schema: {
      type: Object as PropType<Schema>,
      required: true,
    },
    value: {
      required: true,
    },
    onChange: {
      type: Function as PropType<(v: any) => void>,
      required: true,
    },
  },
  setup(props) {
    return () => {
      const { schema } = props
      // TODO: 如果type没有指定，我们需要猜测这个type
      const type = schema.type
      let Component: any
      switch (type) {
        case SchemaTypes.STRING: {
          Component = StringField
          break
        }
        case SchemaTypes.NUMBER: {
          Component = NumberField
          break
        }
        default: {
          console.warn(`${type} is not supported`)
        }
      }
      return <Component {...props} />
    }
  },
})

```

再修改下`SchemaForm.tsx`文件，借助`SchemaItem.tsx`来渲染

```tsx
//...
  setup(props, { slots, emit, attrs }) {
    // 在这里定义一个处理事件 不直接把props.onChange传递进去，方便后续添加处理逻辑
    const handleChange = (v: any) => {
      props.onChange(v)
    }
    return () => {
      const { schema, value } = props
      // 使用SchemaItem中间处理器
      return (
        <SchemaItem schema={schema} value={value} onChange={handleChange} />
      )
    }
  },
 //...
```

最后测试一下，在demos文件夹下新增一个demo.ts文件测试下

```ts
export default {
  name: 'Demo',
  schema: {
    type: 'number',
  },
  uiSchema: '',
  default: '',
}
```



## 使用SFC方式继续实现Field

// StringField.vue

```vue
<template>
  <input type="text" :value="value" @input="handleChange" />
</template>
<!-- <script setup="props" lang="ts">
import { FiledPropDefine } from '../types'
// 2020/06月份刚sfc时候的setup写法
export default {
  props:FiledPropDefine
}

export const handleChange = (e: any) => {
  console.log(e.target.value)
  props.onChange(e.target.value)
}
</script> -->
<script setup lang="ts">
import { FiledPropDefine } from '../types'

const props = defineProps(FiledPropDefine)

const handleChange = (e: any) => {
  console.log(e.target.value)
  props.onChange(e.target.value)
}
</script>
```

使用SFC方式最大的问题就是在vscode中判断不出.vue文件的类型，因为.vue文件类型都是通过`shims-vue.d.ts`来实现的，ts并不知道props的定义。

## 解决下`monaco-editor`的报错问题

因为我使用的是cli5创建的项目，里面webpack版本是5，本来想依赖都按照教程中的版本指定安装，所以指定安装`monaco-editor-webpack-plugin@1.9.1`,但是安装的时候提示`peer webpack@"^4.5.0" from monaco-editor-webpack-plugin@1.9.1`，没得法只有安装最新的`monaco-editor-webpack-plugin`但是安装他又提示了`peer monaco-editor@">= 0.31.0" from monaco-editor-webpack-plugin@7.0.1`，所以只有二个插件都按最新的咯，尝试发现没得问题。

安装

```bash
npm i monaco-editor
npm i -D monaco-editor-webpack-plugin
```

在vue.config.js中使用

```js
const { defineConfig } = require('@vue/cli-service')
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')

module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack(config) {
    config.plugin('monaco').use(new MonacoWebpackPlugin())
  },
})
```

## 复杂节点的渲染

- 对象
- 数组

> 核心就是解析schema,编写一个utlis方法库来处理

- 安装依赖

  ```bash
  npm i jsonpointer lodash.union json-schema-merge-allof
  npm i @types/jsonpointer @types/lodash.union @types/json-schema-merge-allof -D
  ```

- 文件内容

  ```ts
  // 详情见项目
  import { Schema, SchemaTypes, VueJsonSchemaConfig } from './types'
  
  import jsonpointer from 'jsonpointer'
  import union from 'lodash.union'
  import mergeAllOf from 'json-schema-merge-allof'
  
  export function isObject(thing: any) {
    return typeof thing === 'object' && thing !== null && !Array.isArray(thing)
  }
  export function hasOwnProperty(obj: any, key: string) {
    /**
     * 直接调用`obj.hasOwnProperty`有可能会因为
     * obj 覆盖了 prototype 上的 hasOwnProperty 而产生错误
     */
    return Object.prototype.hasOwnProperty.call(obj, key)
  }
  export function retrieveSchema(
    schema: any,
    rootSchema = {},
    formData: any = {},
  ): Schema {
    if (!isObject(schema)) {
      return {} as Schema
    }
    let resolvedSchema = resolveSchema(schema, rootSchema, formData)
  
    // TODO: allOf and additionalProperties not implemented
    if ('allOf' in schema) {
      try {
        resolvedSchema = mergeAllOf({
          // TODO: Schema type not suitable
          ...resolvedSchema,
          allOf: resolvedSchema.allOf,
        } as any) as Schema
      } catch (e) {
        console.warn('could not merge subschemas in allOf:\n' + e)
        const { allOf, ...resolvedSchemaWithoutAllOf } = resolvedSchema
        return resolvedSchemaWithoutAllOf
      }
    }
    const hasAdditionalProperties =
      resolvedSchema.hasOwnProperty('additionalProperties') &&
      resolvedSchema.additionalProperties !== false
    if (hasAdditionalProperties) {
      // put formData existing additional properties into schema
      return stubExistingAdditionalProperties(
        resolvedSchema,
        rootSchema,
        formData,
      )
    }
    return resolvedSchema
  }
  ```

- 同时还需要扩展下`types.ts`中的`Schema`的定义

### 编写ObjectField渲染

- 修改下`SchemaForm.tsx`文件，组件添加`rootSchem`属性

  ```tsx
      return () => {
        const { schema, value } = props
        // 使用SchemaItem中间处理器
        return (
          <SchemaItem
            schema={schema}
            rootSchema={schema}
            value={value}
            onChange={handleChange}
          />
        )
      }
  ```

- 同时需要修改下`types.ts`中的`FiledPropDefine`类型，添加`rootSchema`

  ```ts
  export const FiledPropDefine = {
    schema: {
      type: Object as PropType<Schema>,
      required: true,
    },
    value: {
      required: true,
    },
    onChange: {
      type: Function as PropType<(v: any) => void>,
      required: true,
    },
    rootSchema: {
      type: Object as PropType<Schema>,
      required: true,
    },
  } as const
  ```

- 创建`ObjectField.tsx`文件

  ```tsx
  import { defineComponent, inject } from 'vue'
  import { FiledPropDefine } from '../types'
  import SchemaItem from '../SchemaItem'
  console.log(SchemaItem, '触发下循环引用')
  
  export default defineComponent({
    name: 'ObjectField',
    props: FiledPropDefine,
    setup() {
      return () => {
        return <div>Object</div>
      }
    },
  })
  ```

- 修改下`SchemaItem.tsx`文件，加入对Object类型的渲染

  核心方法是递归解析`retrieveSchema`方法，得到需要的schema

  ```tsx
  import { computed, defineComponent } from 'vue'
  // ...
  import ObjectField from './fields/ObjectField'
  import { FiledPropDefine, SchemaTypes } from './types'
  import { retrieveSchema } from './utils'
  
  export default defineComponent({
    name: 'SchemaItem',
    props: FiledPropDefine,
    setup(props) {
      const retrievedSchemaRef = computed(() => {
        const { schema, rootSchema, value } = props
        return retrieveSchema(schema, rootSchema, value)
      })
      return () => {
        const { schema } = props
        const retrievedSchema = retrievedSchemaRef.value
        // ...
        let Component: any
        switch (type) {
  		// ...
          case SchemaTypes.OBJECT: {
            Component = ObjectField
            break
          }
          default: {
            console.warn(`${type} is not supported`)
          }
        }
        return <Component {...props} schema={retrievedSchema} />
      }
    },
  })
  ```

  

#### 处理循环依赖

- 安装`circular-dependency-plugin`检测循环引用

  ```bash
  npm i circular-dependency-plugin -D
  ```

  5.2.0

- 在vue.config.js中使用

  ```js
  const CircularDependencyPlugin = require('circular-dependency-plugin')
  
  module.exports = defineConfig({
    chainWebpack(config) {
      config.plugin('circular').use(new CircularDependencyPlugin())
    },
  })
  ```

  ![image-20230308215410232](images/image-20230308215410232.png)

- 通过`provide`来解决ObjectField的循环依赖问题

  - 在`SchemaForm.tsx`中注册`provide`

    ```tsx
      setup(props, { slots, emit, attrs }) {
        //...
        // 将SchemaItem组件通过provide提供给子组件
        const context = {
          SchemaItem,
        }
        provide(ShcemaFormContextKey, context)
    
        return () => {
          const { schema, value } = props
          // 使用SchemaItem中间处理器
          return (
            <SchemaItem
              schema={schema}
              rootSchema={schema}
              value={value}
              onChange={handleChange}
            />
          )
        }
      },
    ```

  - 然后就可以在`ObjectField.tsx`中使用`inject`拿到

    ```tsx
    import { defineComponent, inject } from 'vue'
    import { FiledPropDefine } from '../types'
    import { ShcemaFormContextKey } from '../context'
    
    export default defineComponent({
      name: 'ObjectField',
      props: FiledPropDefine,
      setup() {
        const context = inject(ShcemaFormContextKey)
        console.log(context)
    
        return () => {
          return <div>Object</div>
        }
      },
    })
    
    ```

  > 注意 : provide的数据 如果需要监听变化，那么请声明一个响应式的变量来传递

#### provide的简单解析

一句话 父组件提供给子组件{key:value}，如果中间的子组件又有一个provide，那么最后的子组件中就含有爷父组件的provide，也就是一层层的传递，不会影响兄弟组件。

#### 完善ObjectField渲染

- 添加inject的类型定义 方便检验其中的`SchemaItem`属性，借助`DefineComponent`类型

- 给`<SchemaItem />`添加需要的属性，需要注意`value`需要判断是否为对象，是对象才取key

  ```tsx
  import { defineComponent, inject, DefineComponent, ExtractPropTypes } from 'vue'
  // ..
  import { ShcemaFormContextKey } from '../context'
  import { isObject } from '../utils'
  
  type SchemaItemDefine = DefineComponent<typeof FiledPropDefine>
  export default defineComponent({
   // ...
    setup(props) {
      const context: { SchemaItem: SchemaItemDefine } | undefined =
        inject(ShcemaFormContextKey)
  
      if (!context) {
        throw Error('SchemaForm shoud be used')
      }
  
      const handleObjectFieldChange = (key: string, v: any) => {
        const value: any = isObject(props.value) ? props.value : {}
        if (v === undefined) {
          delete value[key]
        } else {
          value[key] = v
        }
        // 向上传递
        props.onChange(value)
      }
  
      return () => {
        const { schema, rootSchema, value } = props
  
        const { SchemaItem } = context
  
        const properties = schema.properties || {}
  
        const currentValue: any = isObject(value) ? value : {}
  
        return Object.keys(properties).map((key: string, index: number) => (
          <SchemaItem
            schema={properties[key]}
            rootSchema={rootSchema}
            value={currentValue[key]}
            key={index}
            onChange={(v: any) => handleObjectFieldChange(key, v)}
          />
        ))
      }
    },
  })                            
  ```

  



### 编写ArrayField渲染

> 首先优化下前面的ObjectField代码，提取类型定义 提取获取context
>
> // ObjectField.tsx
>
> ```ts
> const context = useVJSFContext()
> ```
>
> // context.ts
>
> ```ts
> export function useVJSFContext() {
>   const context: { SchemaItem: ComonFieldType } | undefined =
>     inject(ShcemaFormContextKey)
> 
>   if (!context) {
>     throw Error('SchemaForm shoud be used')
>   }
>   return context
> }
> ```



```txt
/**
 * 3种情况
 * {
 *  items:{ type:string}
 * }
 *
 * {
 *  items:[
 *      {  type: string },
 *      {  type: number },
 *  ]
 * }
 *
 * {
 *  items:{ type: string,   enum:['1','2']}
 * }
 */
```

#### 固定长度数组的渲染

- 创建`ArrayField.tsx`文件

```tsx
import { defineComponent } from 'vue'
import { FiledPropDefine, Schema } from '../types'
import { useVJSFContext } from '../context'

export default defineComponent({
  name: 'ArrayField',
  props: FiledPropDefine,
  setup(props) {
    const context = useVJSFContext()
    const handleMultiTypeChange = (v: any, index: number) => {
      const { value } = props
      const arr = Array.isArray(value) ? value : []
      arr[index] = v
      props.onChange(arr)
    }

    return () => {
      const { schema, rootSchema, value } = props
      const SchemaItem = context.SchemaItem
      const isMultiType = Array.isArray(schema.items)

      if (isMultiType) {
        const items: Schema[] = schema.items as any
        const arr = Array.isArray(value) ? value : []
        return items.map((s: Schema, index: number) => (
          <SchemaItem
            schema={s}
            key={index}
            rootSchema={rootSchema}
            value={arr[index]}
            onChange={(v: any) => handleMultiTypeChange(v, index)}
          />
        ))
      }
      return null
    }
  },
})
```

- 修改`SchemaItem.tsx`文件，添加array类型渲染

```tsx
// ....
import ArrayField from './fields/ArrayField'
// ...

export default defineComponent({
  name: 'SchemaItem',
  props: FiledPropDefine,
  setup(props) {
   // ...
    return () => {
     // ...
      let Component: any
      switch (type) {
		// ....
        case SchemaTypes.ARRAY: {
          Component = ArrayField
          break
        }
        default: {
          console.warn(`${type} is not supported`)
        }
      }
      return <Component {...props} schema={retrievedSchema} />
    }
  },
})
```

- 测试一下，在`demo/simple.ts`下

  ```ts
  schema: {
      description: 'A simple form example.',
      type: 'object',
      required: ['firstName', 'lastName'],
      properties: {
       // ...
        staticArray: {
          type: 'array',
          items: [
            {
              type: 'string',
            },
            {
              type: 'number',
            },
          ],
        },
      },
    },
  ```

  

#### 单类型数组的渲染

- 修改`ArrayField.tsx`文件，添加关于单类型数组的解析，添加排序等外层容器

  ```tsx
  import { defineComponent } from 'vue'
  import { FiledPropDefine, Schema } from '../types'
  import { useVJSFContext } from '../context'
  
  const ArrayItemWrapr = defineComponent({
    name: 'ArrayItemWrapr',
    //   props: {},
    setup(props, { slots }) {
      return () => {
        return (
          <div>
            <div>
              <button>新增</button>
              <button>删除</button>
              <button>上移</button>
              <button>下移</button>
            </div>
            <div>{slots.default && slots.default()}</div>
          </div>
        )
      }
    },
  })
  
  export default defineComponent({
    name: 'ArrayField',
    props: FiledPropDefine,
    setup(props) {
      const context = useVJSFContext()
      const handleArryItemChange = (v: any, index: number) => {
        const { value } = props
        const arr = Array.isArray(value) ? value : []
        arr[index] = v
        props.onChange(arr)
      }
  
      return () => {
        const { schema, rootSchema, value } = props
        const SchemaItem = context.SchemaItem
  
        const isMultiType = Array.isArray(schema.items)
        const isSelect = schema.items && (schema.items as any).enum
  
        if (isMultiType) {
        // ....
        } else if (!isSelect) {
          const arr = Array.isArray(value) ? value : []
          return arr.map((v: any, index: number) => {
            return (
              <ArrayItemWrapr>
                <SchemaItem
                  schema={schema.items as Schema}
                  value={v}
                  key={index}
                  rootSchema={rootSchema}
                  onChange={(v: any) => handleArryItemChange(v, index)}
                />
              </ArrayItemWrapr>
            )
          })
        }
        return null
      }
    },
  })
  ```

- 测试一下，修改下`demo/simple.ts`，添加单类型数组定义，注意需要添加一下data不然没有输入框显示出现

  ```ts
  export default {
    name: 'Simple',
    schema: {
      description: 'A simple form example.',
      type: 'object',
      required: ['firstName', 'lastName'],
      properties: {
       // ...
        singleTypeArray: {
          type: 'array',
          items: {
            type: 'string',
          },
        },
      },
    },
    uiSchema: {
  	// ...
    },
    default: {
      firstName: 'Chuck',
      lastName: 'Norris',
      age: 75,
      bio: 'Roundhouse kicking asses since 1940',
      password: 'noneed',
      singleTypeArray: ['zyq'],
    },
  }
  ```

- 继续完善，简单添加样式，借助`vue-jss`

- 添加新增 删除 上移 下移逻辑

  ```tsx
  const ArrayItemWrapr = defineComponent({
    name: 'ArrayItemWrapr',
    props: {
      onAdd: {
        type: Function as PropType<(index: number) => void>,
        required: true,
      },
      onDelete: {
        type: Function as PropType<(index: number) => void>,
        required: true,
      },
      onUp: {
        type: Function as PropType<(index: number) => void>,
        required: true,
      },
      onDown: {
        type: Function as PropType<(index: number) => void>,
        required: true,
      },
      index: {
        type: Number,
        required: true,
      },
    },
    setup(props, { slots }) {
      const classesRef = useStyles()
      const handleAdd = () => props.onAdd(props.index)
      const handleDelete = () => props.onDelete(props.index)
      const handleUp = () => props.onUp(props.index)
      const handleDown = () => props.onDown(props.index)
      return () => {
        const classes = classesRef.value
        return (
          <div class={classes.container}>
            <div class={classes.actions}>
              <button class={classes.action} onClick={handleAdd}>
                新增
              </button>
              <button class={classes.action} onClick={handleDelete}>
                删除
              </button>
              <button class={classes.action} onClick={handleUp}>
                上移
              </button>
              <button class={classes.action} onClick={handleDown}>
                下移
              </button>
            </div>
            <div class={classes.content}>{slots.default && slots.default()}</div>
          </div>
        )
      }
    },
  })
  
  export default defineComponent({
    name: 'ArrayField',
    props: FiledPropDefine,
    setup(props) {
  	// ...
      const handleAdd = (index: number) => {
        const { value } = props
        const arr = Array.isArray(value) ? value : []
        arr.splice(index + 1, 0, undefined)
  
        props.onChange(arr)
      }
      const handleDelete = (index: number) => {
        const { value } = props
        const arr = Array.isArray(value) ? value : []
        arr.splice(index, 1)
  
        props.onChange(arr)
      }
      const handleUp = (index: number) => {
        if (index === 0) {
          return
        }
        const { value } = props
        const arr = Array.isArray(value) ? value : []
        const item = arr.splice(index, 1)
        arr.splice(index - 1, 0, item[0])
  
        props.onChange(arr)
      }
      const handleDown = (index: number) => {
        const { value } = props
        const arr = Array.isArray(value) ? value : []
        if (index === arr.length - 1) {
          return
        }
        const item = arr.splice(index, 1)
        arr.splice(index + 1, 0, item[0])
  
        props.onChange(arr)
      }
  
      return () => {
      //...
        if (isMultiType) {
          // ...
        } else if (!isSelect) {
          const arr = Array.isArray(value) ? value : []
          return arr.map((v: any, index: number) => {
            return (
              <ArrayItemWrapr
                index={index}
                onAdd={handleAdd}
                onDelete={handleDelete}
                onUp={handleUp}
                onDown={handleDown}
              >
                <SchemaItem
                  schema={schema.items as Schema}
                  value={v}
                  key={index}
                  rootSchema={rootSchema}
                  onChange={(v: any) => handleArryItemChange(v, index)}
                />
              </ArrayItemWrapr>
            )
          })
        }
        return null
      }
    },
  })
  ```




#### 多选数组的渲染

- 在`lib`下新建一个`widgets`文件夹，并创建一个`Selection.tsx`组件，用于实现多选

  ```tsx
  import { defineComponent, PropType, ref, watch } from 'vue'
  
  export default defineComponent({
    name: 'SelectionWidget',
    props: {
      value: {},
      onChange: {
        type: Function as PropType<(v: any) => void>,
        required: true,
      },
      options: {
        type: Array as PropType<
          {
            key: string
            value: any
          }[]
        >,
        required: true,
      },
    },
    setup(props) {
      const currentValueRef = ref(props.value)
  
      watch(currentValueRef, (newVal) => {
        // currentValueRef被修改时，触发onchange
        if (newVal !== props.value) {
          props.onChange(newVal)
        }
      })
  
      watch(
        () => props.value,
        (v) => {
          // 父组件传递进来的字 赋值给currentValueRef 实现了 value的双向
          if (v !== currentValueRef.value) {
            currentValueRef.value = v
          }
        },
      )
  
      return () => {
        const { options } = props
  
        return (
          <select multiple v-model={currentValueRef.value}>
            {options.map((op) => (
              <options value={op.value}>{op.key}</options>
            ))}
          </select>
        )
      }
    },
  })
  
  ```

  > 注意： v-model={currentValueRef.value} 这里的.vlaue不能省略，在SFC文件中的模板可以省略是因为vue对模板解析的时候做了处理

- 修改下ArrayField.tsx文件，引入上面的组件

  ```tsx
  // ...
  import SelectionWidget from '../widgets/Selection'
  // ...
  export default defineComponent({
    name: 'ArrayField',
    props: FiledPropDefine,
    setup(props) {
      const context = useVJSFContext()
  
    // ...
  
      return () => {
       //...
  
        if (isMultiType) {
        // ...
        } else if (!isSelect) {
         //...
        } else {
          const enumOptions = (schema as any).items.enum
          const options = enumOptions.map((e: any) => {
            return {
              key: e,
              value: e,
            }
          })
          return (
            <SelectionWidget
              onChange={props.onChange}
              value={props.value}
              options={options}
            />
          )
        }
      }
  ```

- 测试一下，修改下`demo/simple.tx`文件，添加多选数组schema定义

  ```ts
        multiSelectArray: {
          type: 'array',
          items: {
            type: 'string',
            enum: ['123', '456', '789'],
          },
        },
  ```

# 单元测试

[Jest](https://jestjs.io/zh-Hans)   测试框架

[vue-test-utils](https://test-utils.vuejs.org/) vue官方提供的关于vue的测试方法 

为啥要单测

- 检测bug
- 提升回归效率 方便改代码后检测是否有问题
- 保证代码质量

测试覆盖率

## jest单元测试配置

使用vuecli创建项目时，勾选了jest，已经帮我们初始化好了。

查看下jest的配置，项目中的`jest.config.js`，发现它通过prest预设了好多。

可以从vue-cli的地址上看到

// [cli-plugin-unit-jest](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest)/[presets](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets)/[typescript-and-babel](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets/typescript-and-babel)/**jest-preset.js** 

```
const deepmerge = require('deepmerge')
const defaultTsPreset = require('../typescript/jest-preset')

module.exports = deepmerge(
  defaultTsPreset,
  {
    globals: {
      'ts-jest': {
        babelConfig: true
      }
    }
  }
)
```

// [cli-plugin-unit-jest](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest)/[presets](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets)/[typescript](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets/typescript)/**jest-preset.js** 

```
const deepmerge = require('deepmerge')
const defaultPreset = require('../default/jest-preset')

let tsJest = null
try {
  tsJest = require.resolve('ts-jest')
} catch (e) {
  throw new Error('Cannot resolve "ts-jest" module. Typescript preset requires "ts-jest" to be installed.')
}

module.exports = deepmerge(
  defaultPreset,
  {
    moduleFileExtensions: ['ts', 'tsx'],
    transform: {
      '^.+\\.tsx?$': tsJest
    }
  }
)
```

// [cli-plugin-unit-jest](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest)/[presets](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets)/[default](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest/presets/default)/**jest-preset.js**

```js
// eslint-disable-next-line node/no-extraneous-require
const semver = require('semver')

let vueVersion = 2
try {
  // eslint-disable-next-line node/no-extraneous-require
  const Vue = require('vue/package.json')
  vueVersion = semver.major(Vue.version)
} catch (e) {}

let vueJest = null
try {
  vueJest = require.resolve(`@vue/vue${vueVersion}-jest`)
} catch (e) {
  throw new Error(`Cannot resolve "@vue/vue${vueVersion}-jest" module. Please make sure you have installed "@vue/vue${vueVersion}-jest" as a dev dependency.`)
}

module.exports = {
  testEnvironment: 'jsdom',
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ],
  transform: {
    // process *.vue files with vue-jest
    '^.+\\.vue$': vueJest,
    '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$':
    require.resolve('jest-transform-stub'),
    '^.+\\.jsx?$': require.resolve('babel-jest')
  },
  transformIgnorePatterns: ['/node_modules/'],
  // support the same @ -> src alias mapping in source code
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  // serializer for snapshots
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  testMatch: [
    '**/tests/unit/**/*.spec.[jt]s?(x)',
    '**/__tests__/*.[jt]s?(x)'
  ],
  // https://github.com/facebook/jest/issues/6766
  testURL: 'http://localhost/',
  watchPlugins: [
    require.resolve('jest-watch-typeahead/filename'),
    require.resolve('jest-watch-typeahead/testname')
  ]
}
```

核心知道底层的配置文件是那些，有哪些参数，方便后续自己修改。

## 使用jest写测试用例

- describe
- it/test

### expect断言

更多用法看官网

https://jestjs.io/zh-Hans/docs/expect

### 预设与清理

- `beforeEach` / `afterEach `每个测试之前/之后会执行
- `beforeAll` / `afterAll `所有测试之前/之后执行
- 有作用域
- 在before中设置值，在after中清除

### 异步测试

```ts
describe('HelloWorld.vue', () => {
  // 实践是错误滴，但是jest是不知道的
  it('should work', () => {
    setTimeout(() => {
      expect(1 + 1).toBe(3)
    }, 1000)
  })
})
```

解决办法1: 使用回调中的done方法

```ts
it('should work', (done) => {
    setTimeout(() => {
      expect(1 + 1).toBe(3)
      done() // 这里
    }, 1000)
  })
```

办法2： Promise

```ts
  it('should work', () => {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        expect(1 + 1).toBe(3)
        resolve()
      }, 1000)
    })
  })
```

办法3： async

```ts
it('should work', async () => {
    const x = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(1 + 1)
      }, 1000)
    })
    expect(x).toBe(1)
  })

// 或者测试这个
  it('renders props.msg when passed', async () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    })
    await wrapper.setProps({
      msg: '123',
    })
    expect(wrapper.text()).toMatch('123')
  })
```



## 使用vue-test-utils测试vue组件

> 根据jest的配置文件，我们得到其省略文件名后缀进行文件导入解析时的默认加载顺序如下
>
> ```
>  moduleFileExtensions: [
>     'js',
>     'jsx',
>     'json',
>     // tell Jest to handle *.vue files
>     'vue',
>     'ts', 'tsx'
>   ],
> ```
>
> 所以在省略后缀引入时，`import NumberField from './fields/NumberField'`实际导入的是.vue的文件，如果你的想和项目运行时的导入逻辑保持一致可能需要调整一下这个字段顺序，或者就不要有同名的vue文件了，保留一个tsx文件就阔以了。

- 用就完事了

  ```ts
  import { mount } from '@vue/test-utils'
  
  import JsonSchemaForm, { NumberField } from '../../lib'
  
  describe('JsonSchemaForm', () => {
    it('should render correct number field', async () => {
      let value = ''
      const wrapper = mount(JsonSchemaForm, {
        props: {
          schema: {
            type: 'number',
          },
          value: value,
          onChange: (v: any) => {
            value = v
          },
        },
      })
  
      const numberFiled = wrapper.findComponent(NumberField)
      expect(numberFiled.exists()).toBeTruthy()
  
      // await numberFiled.props('onChange')('123')
      const input = numberFiled.find('input')
      input.element.value = '123'
      input.trigger('input')
      expect(value).toBe(123)
    })
  })
  
  ```

  

## 代码覆盖率

测试代码覆盖率

```bash
npm run test:unit -- --coverage
```

等价于

```json
script:{
 "test:unit": "vue-cli-service test:unit --coverage",
}
```

控制台输出，以及会得到一个`coverage`文件夹，里面有各种文件

```
Stmts语句覆盖率 Branch分支覆盖率 Funcs函数覆盖率 Lines行覆盖率
------------------|---------|----------|---------|---------|---------------------------------------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                                 
------------------|---------|----------|---------|---------|---------------------------------------------------
All files         |    33.6 |     8.08 |   17.28 |   34.26 |                                                   
 lib              |   34.09 |     9.58 |      25 |   34.25 |                                                   
  SchemaForm.tsx  |     100 |      100 |     100 |     100 |                                                   
  SchemaItem.tsx  |   74.07 |      100 |     100 |   74.07 | 29-30,37-45                                       
  context.ts      |      50 |      100 |       0 |      50 | 8-13                                              
  index.ts        |     100 |      100 |     100 |     100 |                                                   
  types.ts        |     100 |      100 |     100 |     100 |                                                   
  utils.ts        |   17.68 |     7.04 |   11.11 |    17.5 | ...42,446,453-460,465-467,476-527,533-551,562-584 
 lib/fields       |   34.84 |     3.84 |      10 |    36.5 |                                                   
  ArrayField.tsx  |   11.59 |        0 |       0 |    12.5 | 69-76,103-196                                     
  NumberField.vue |   95.83 |       50 |     100 |   95.83 | 20                                                
  ObjectField.tsx |   26.31 |        0 |       0 |   27.77 | 12-40                                             
  StringField.vue |      50 |      100 |       0 |      50 | 12-27,36                                          
 lib/widgets      |   14.28 |      100 |       0 |   14.28 |                                                   
  Selection.tsx   |   14.28 |      100 |       0 |   14.28 | 22-47                                             
------------------|---------|----------|---------|---------|---------------------------------------------------
```

## ObjectField的单元测试

> 使用共用变量时，推荐在beforeEach中进行赋值，可以避免在某个测试中改变了这个变量

```ts
import { mount } from '@vue/test-utils'

import JsonSchemaForm, { NumberField, StringField } from '../../lib'

describe('ObjectField', () => {
  let schema: any
  beforeEach(() => {
    schema = {
      type: 'object',
      properties: {
        name: {
          type: 'string',
        },
        age: {
          type: 'number',
        },
      },
    }
  })
  it('should render properties to correct fileds', async () => {
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema,
        value: {},
        onChange: () => {},
      },
    })

    const strField = wrapper.findComponent(StringField)
    const numField = wrapper.findComponent(NumberField)
    expect(strField.exists()).toBeTruthy()
    expect(numField.exists()).toBeTruthy()
  })

  it('should change value when sub fields trigger onChange', async () => {
    let value: any = {}
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema,
        value: value,
        onChange: (v: any) => {
          value = v
        },
      },
    })

    const strField = wrapper.findComponent(StringField)
    const numField = wrapper.findComponent(NumberField)

    await strField.props('onChange')('1')
    expect(value.name).toBe('1')
    await numField.props('onChange')(1)
    expect(value.age).toBe(1)
  })

  it('should handleObjectFieldChange else', async () => {
    let value: any = {
      name: '123',
    }
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema,
        value: value,
        onChange: (v: any) => {
          value = v
        },
      },
    })

    const strField = wrapper.findComponent(StringField)

    await strField.props('onChange')(undefined)
    expect(value.name).toBe(undefined)
  })
})

```

## ArrayField的单元测试

> 只运行特定的测试用例，通过`-t`的方式来指定运行，会匹配符合it中的字符串的用例
>
> ```bash
> npm run test:unit -- -t multi type
> ```

```ts
import { mount } from '@vue/test-utils'

import JsonSchemaForm, {
  ArrayField,
  NumberField,
  SelectionWidget,
  StringField,
} from '../../lib'

describe('ArrayField', () => {
  it('should render multi type', async () => {
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema: {
          type: 'array',
          items: [
            {
              type: 'string',
            },
            {
              type: 'number',
            },
          ],
        },
        value: [],
        onChange: () => {},
      },
    })

    // 先从wrapper中找到array
    const arrayField = wrapper.findComponent(ArrayField)
    // 再从array中找number
    const numberFiled = arrayField.findComponent(NumberField)
    // 再从array中找string
    const stringField = arrayField.findComponent(StringField)

    expect(numberFiled.exists()).toBeTruthy()
    expect(stringField.exists()).toBeTruthy()
  })

  it('should render single type', async () => {
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema: {
          type: 'array',
          items: {
            type: 'string',
          },
        },
        value: ['1', '2'],
        onChange: () => {},
      },
    })

    // 先从wrapper中找到array
    const arrayField = wrapper.findComponent(ArrayField)
    // 再从array中找所有的string
    const stringFields = arrayField.findAllComponents(StringField)

    expect(stringFields.length).toBe(2)
    expect(stringFields[0].props('value')).toBe('1')
  })

  it('should render select type', async () => {
    const wrapper = mount(JsonSchemaForm, {
      props: {
        schema: {
          type: 'array',
          items: {
            type: 'string',
            enum: ['1', '2', '3'],
          },
        },
        value: [],
        onChange: () => {},
      },
    })

    // 先从wrapper中找到array
    const arrayField = wrapper.findComponent(ArrayField)
    const selectionWidget = arrayField.findComponent(SelectionWidget)

    expect(selectionWidget.exists()).toBeTruthy()
  })
})
```



# 主题系统

- 满足样式和交互的多样性
- 核心逻辑是不变的 lib，部件 widget
- 不同于样式主题系统
  - 交互可以变化
  - 组件的产出可以完全不同
  - 统一接口后所有内容都可以自定义
- 可以基于不同的组件库来实现

## 拆分主题系统的打包

>  减少强依赖

- vue-cli打包配置https://cli.vuejs.org/zh/guide/build-targets.html#%E5%BA%93

- 在`lib`文件夹下新建`theme-default`文件夹

- 修改package.json文件

  ```json
    "scripts": {
      //...
      "build:core": "vue-cli-service build --target lib lib/index.ts",
      "build:theme": "vue-cli-service build --target lib lib/theme-default/index.tsx",
       //...
    },
  ```

- 运行`npm run build:theme`会发现打出来的文件有点多，是因为在`vue.config.js`中未区分环境把`monaco-editor`的内容也打包进来了。

  ![image-20230313224512883](images/image-20230313224512883.png)
  - 解决办法，通过.env文件或在运行时添加一个环境变量区分，这里使用简单下环境变量区分就行了

    > window下需要通过set TYPE=xx
    >
    > mac下直接TYPE=xxx

    ```
     "scripts": {
        "build:core": "set TYPE=lib && vue-cli-service build --target lib lib/index.ts",
        "build:theme": "set TYPE=lib && vue-cli-service build --target lib lib/theme-default/index.tsx",
      },
    ```

    TODO

    但是测试发现一个神奇的问题,通过上面的set方式，在vue.config.js中打印时值是lib，但是比对却是不相等。。。。

    ```js
    const isLib = process.env.TYPE === 'lib'
    
    console.log(process.env.TYPE)
    console.log(process.env.TYPE === 'lib')
    console.log(isLib)
    ```

    ![image-20230313230533940](images/image-20230313230533940.png)

  - 没得法使用`cross-env`吧，正好也可以解决不同系统下的区别问题

    ```
    npm install --save-dev cross-env
    ```

    ```
      "scripts": {
        "build:core": "cross-env TYPE=lib vue-cli-service build --target lib lib/index.ts",
        "build:theme": "cross-env TYPE=lib vue-cli-service build --target lib lib/theme-default/index.tsx",
      },
    ```

    成功后

    ![image-20230313230845399](images/image-20230313230845399.png)

- 修改生成的文件名

  通过`--name`修改文件名，并把core和theme放在不同的文件夹下

  ```json
    "scripts": {
      "build:core": "set TYPE=lib && vue-cli-service build --target  lib --name index lib/index.ts",
      "build:theme": "cross-env TYPE=lib vue-cli-service build --target lib --name theme-default/index lib/theme-default/index.tsx",
    },
  ```

- build时会自动清除之前的文件，会导致无法同时存在core包与theme包，所以需要设置下打包时不清除之前的文件。加上`--no-clean`，但是这样，可能会存在遗留之前的文件。所有可以通过`rimraf`这个包来选择时机清除。

  ```
  npm i -D rimraf
  ```

  ```json
    "scripts": {
      "build:core": "set TYPE=lib && vue-cli-service build --target  lib --name index --no-clean lib/index.ts",
      "build:theme": "cross-env TYPE=lib vue-cli-service build --target lib --name theme-default/index --no-clean lib/theme-default/index.tsx",
      "build": "rimraf dist && npm run build:core && npm run build:theme",
    },
  ```

  



## 拆分主题并进行定义

- 提取widget的类型定义，修改下`type.ts`

  ```ts
  export const CommonWidgetPropsDefine = {
    value: {},
    onChange: {
      type: Function as PropType<(v: any) => void>,
      required: true,
    },
  } as const
  
  export type CommonWidgetDefine = DefineComponent<typeof CommonWidgetPropsDefine>
  
  export const SelectionWidgetPropsDefine = {
    ...CommonWidgetPropsDefine,
    options: {
      type: Array as PropType<
        {
          key: string
          value: any
        }[]
      >,
      required: true,
    },
  } as const
  
  export type CommonWidgetDefine = DefineComponent<typeof CommonWidgetPropsDefine>
  export type SelectionWidgetDefine = DefineComponent<
    typeof SelectionWidgetPropsDefine
  >
  
  export interface Theme {
    widgets: {
      SelectionWidget: SelectionWidgetDefine
      TextWidget: CommonWidgetDefine
      NumberWidget: CommonWidgetDefine
    }
  }
  ```

- 修改`SchemaForm.tsx`文件，添加`theme`属性，并新增一个provide属性`theme`，就可以直接在`ArrayField.tsx`中通过`inject`拿到theme下的widget了

  ```tsx
  export default defineComponent({
    props: {
    
      theme: {
        type: Object as PropType<Theme>,
        required: true,
      },
    },
    name: 'SchemaForm',
    setup(props, { slots, emit, attrs }) {
     // ...
      const context = {
        SchemaItem,
        theme: props.theme,
      }
      provide(ShcemaFormContextKey, context)
      // ...
  
  })
  ```

- 紧接着就可以修改下`context.ts`中的theme的定义，支持`theme`，在`ArrayField.tsx`中就不需要引入`SelectionWidget.tsx`了

  // context.tx

  ```ts
  import { ComonFieldType, Theme } from './types'
  
  export function useVJSFContext() {
    const context: { theme: Theme; SchemaItem: ComonFieldType } | undefined =
      inject(ShcemaFormContextKey)
   // ...
  }
  
  ```

  // ArrayField.tsx

  ```tsx
  const SchemaItem = context.SchemaItem
  const SelectionWidget = context.theme.widgets.SelectionWidget
  ```



## 使用ThemeProvider解耦

> 之前的代码中我们在`schemaForm`中定义了theme属性，并通过provide提供给子组件，但是提供的是一个非响应式对象，与我们实际的需求是不符合的，所以就需要提供一个响应式的对象来实现响应式的更新。

- 在`lib`下新建一个`theme.tsx`文件，用这个组件来提供一个全局的响应式的theme

  ```ts
  import {
    computed,
    ComputedRef,
    defineComponent,
    inject,
    PropType,
    provide,
  } from 'vue'
  import { Theme } from './types'
  
  const THEME_PROVIDER_KEY = Symbol()
  const ThemeProvider = defineComponent({
    name: 'VJSFThemeProvider',
    props: {
      theme: {
        type: Object as PropType<Theme>,
        required: true,
      },
    },
    setup(props, { slots }) {
      const context = computed(() => props.theme)
      provide(THEME_PROVIDER_KEY, context)
      return () => slots.default && slots.default()
    },
  })
  
  // 提供给子组件使用的方法
  export function getWidget(name: string) {
    const context: ComputedRef<Theme> | undefined =
      inject<ComputedRef<Theme>>(THEME_PROVIDER_KEY)
    if (!context) {
      throw new Error('vjsf theme required')
    }
    // 这样写 就成非响应式的了，后续的widget改变不会触发了。
    // const widgetRef = context.value.widgets[name]
    // 这里需要一个响应式的数据
    const widgetRef = computed(() => {
      return (context.value.widgets as any)[name]
    })
    return widgetRef
  }
  export default ThemeProvider
  ```

- 修改下`ArrayField.tsx`中从`context`中获取到的SelectionWidget组件,同时也可以去除`contenxt.ts`中添加的theme类型定义了。

  ```tsx
  import { getWidget } from '../theme'
  
  setup(){
  // ...
  const SelectionWidgetRef = getWidget('SelectionWidget') // 注意名字对应
  return ()=>{
  	// const SelectionWidget = context.theme.widgets.SelectionWidget
        const SelectionWidget = SelectionWidgetRef.value
  }
  }
  ```

- 去除`SchemeForm.tsx`中定义的`theme`属性，在`lib/index.ts`中导出`ThemeProvider`

- 在`App.tsx`中使用`ThemeProvider`

  ```tsx
  <div class={classes.form}>
  <ThemeProvider theme={themeDefault as any}>
    <SchemaForm
      schema={demo.schema}
      value={demo.data}
      onChange={handleChange}
    />
  </ThemeProvider>
  </div>
  ```

> 通过定义一个组件的方式来提供theme，与之前通过`SchemaForm.tsx`通过props+provide的方式提供，实现了与`SchemaForm`解耦，好处就是，可以在其他地方导入导出，纯组件化的设计理念，通过组件的拆分组合来增加功能，而不是把功能耦合在一个组件中，但是也不是万能的只是提供了一种方式。

## 修复TS的类型问题

- `App.tsx`中`<ThemeProvider theme={themeDefault as any}>`

  - 修改下`theme-default/index.tsx`中的内容

    ```tsx
    import SelectionWidget from './Selection'
    import { CommonWidgetPropsDefine, CommonWidgetDefine } from '../types'
    import { defineComponent } from 'vue'
    
    const CommonWidget = defineComponent({
      props: CommonWidgetPropsDefine,
      setup() {
        return () => null
      },
    }) as CommonWidgetDefine // 核心是这里 指定下类型 
    
    export default {
      widgets: {
        SelectionWidget,
        TextWidget: CommonWidget,
        NumberWidget: CommonWidget,
      },
    }
    
    ```

  - `theme-default/Selection.tsx`修改

    ```tsx
    import { defineComponent, PropType, ref, watch } from 'vue'
    import { SelectionWidgetDefine, SelectionWidgetPropsDefine } from '../types'
    
    export default defineComponent({
      name: 'SelectionWidget',
      props: SelectionWidgetPropsDefine,
      setup(props) {
       //xxxx
      }
    }) as SelectionWidgetDefine
    ```

  > 主要是defineComponent的类型定义太复杂了

- `lib/theme.tsx`中的`getWidget`方法里面`(context.value.widgets as any)[name]`

  - 在`types.ts`下添加 定义下name

  ```ts
  export enum SelectionWidgetNames {
    SelectionWidget = 'SelectionWidget',
  }
  export enum CommonWidgetNames {
    TextWidget =