Skip to main content

实现一个主题系统

主题系统的应用

  • 系统的核心逻辑是不变的,但是交互和样式是可以抽离出来。

不同于样式主题

  • 交互可以变化
  • 组件的产出可以完全不同
  • 统一接口之后所有内容皆可自定义
  • 可以基于不同组件库来实现

拆分主题代码的打包

  • 减少强依赖
  • 通过以下指令拆分打包
{
"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}
/>
)
}
},
})

第二种方式

  • 第一种方式,themeSchemaForm存在一定的绑定关系,存在着强耦合。

  • 可以通过提供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>
)
},
})