实现一个主题系统
主题系统的应用
- 系统的核心逻辑是不变的,但是交互和样式是可以抽离出来。
 
不同于样式主题
- 交互可以变化
 - 组件的产出可以完全不同
 - 统一接口之后所有内容皆可自定义
 - 可以基于不同组件库来实现
 
拆分主题代码的打包
- 减少强依赖
 - 通过以下指令拆分打包
 
{
    "scripts":
    {
    "build:core": "TYPE=lib vue-cli-service build --target lib --name index --no-clean lib/index.ts",
    "build:theme": "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",
    }
}
- 通过
TYPE=lib,可以区分环境变量来打包 
const isLib = process.env.TYPE === 'lib'
if (!isLib) {
      config.plugin('monaco').use(new MonacoWebpackPlugin())
    }
通过provide实现主题系统代码和核心逻辑代码拆分
- 如果在核心逻辑代码直接引入主题系统代码,会形成强依赖,代码也会打包在一起
 
第一种方式
使用provide可以实现拆分,再通过props传入theme的代码
在
SchemaForm.tsx声明themeprops,并通过provide注入到子组件中
//SchemaForm.tsx
export default defineComponent({
  name: 'SchemaForm',
  props: {
    theme: {
      type: Object as PropType<Theme>,
      required: true,
    },
  },
  setup(props, { slots, emit, attrs }) {
    const context: any = {
      theme: props.theme,
    }
    provide(SchemaFormContextKey, context)
   
  },
  },
})
- 在需要用到主题代码的子组件中,通过
inject获取对应样式组件。 
//ArrayFiled.tsx
export default defineComponent({
  name: 'ArrayFiled',
 
  setup(props, { slots, emit, attrs }) {
    let context =  inject(SchemaFormContextKey)
    let SelectionWidget = context.theme.widgets.SelectionWidget
  },
})
   
- 在使用
SchemaForm组件的地方,引入主题代码,传入theme 
import Theme from '../lib/theme-default'
export default defineComponent({
  name: 'App',
  setup() {
    return () => {
      return (
          <SchemaForm
            theme={Theme as any}
            schema={demo.schema}
            onChange={handleChange}
            value={demo.data}
          />
      )
    }
  },
})
第二种方式
第一种方式,
theme和SchemaForm存在一定的绑定关系,存在着强耦合。可以通过提供provider组件的方式,将这种强耦合解决掉
这样做的好处是,通过组件的拆分组合来完成一个组件,而不是把所有的东西都放在一个组件里去
写一个
ThemeProvider组件,提供注入对象,然后再写一个getWidget获取注入对象
//ThemeProvider.tsx
const THEME_PROVIDER_KEY = Symbol()
export default 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<T extends SelectionWidgetNames | CommonWidgetNames>(
  name: T,
) {
  const context: ComputedRef<Theme> | undefined = inject<ComputedRef<Theme>>(
    THEME_PROVIDER_KEY,
  )
  if (!context) {
    throw new Error('vjsf theme required')
  }
  const widgetRef = computed(() => {
    return context.value.widgets[name]
  })
  return widgetRef
}
- 在子组件中,通过调用
getWidget获取注入对象 
import {
  SelectionWidgetNames,
} from '../types'
import { getWidget } from '../theme'
export default defineComponent({
  name: 'ArrayFiled',
  props: FiledPropsDefine,
  setup(props) {
    let SelectionWidgetRef = getWidget(SelectionWidgetNames.SelectionWidget)
    return () => {
        let SelectionWidget = SelectionWidgetRef.value
        return (
          <SelectionWidget
            onChange={props.onChange}
            value={props.value}
            options={options}
          />
        )
      }
    }
  },
})
- 在使用
SchemaForm组件的地方,再包装一个ThemeProvider组件 
import SchemaForm, { ThemeProvider } from '../lib'
export default defineComponent({
  name: 'App',
  setup() {
    return () => {
      return (
        <ThemeProvider theme={Theme as any}>
          <SchemaForm
            schema={demo.schema}
            onChange={handleChange}
            value={demo.data}
          />
        </ThemeProvider>
      )
    }
  },
})
- 这样拆分还有一个好处,就是主题代码和核心逻辑代码拆分成两个包时,主题代码可以引用核心逻辑代码的
ThemeProvide, 再包装一层,提供自己的主题 
// vjsf-theme-default // import {ThemeProvider} from 'vue3-jsonschema-form'
// vue3-jsonschema-form
export const ThemeDefaultProvider = defineComponent({
  setup(p, { slots }) {
    return () => (
      <ThemeProvider theme={defaultTheme}>
        {slots.default && slots.default()}
      </ThemeProvider>
    )
  },
})