实现一个主题系统
主题系统的应用
- 系统的核心逻辑是不变的,但是交互和样式是可以抽离出来。
不同于样式主题
- 交互可以变化
- 组件的产出可以完全不同
- 统一接口之后所有内容皆可自定义
- 可以基于不同组件库来实现
拆分主题代码的打包
- 减少强依赖
- 通过以下指令拆分打包
{
"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
声明theme
props,并通过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>
)
},
})