《Fantastic-admin 技术揭秘》系列将带你了解 Fantastic-admin 这款框架各种功能的设计与实现。通过了解这些技术细节,你不光可以更轻松地使用 Fantastic-admin 这款框架,也可以在其他项目中使用这些技术。
你可以点击 这里 查看本系列的所有文章,也欢迎你在评论区留言告诉我你感兴趣的内容,或许下一篇文章就会带你揭秘其中的奥秘。
前言
什么是“应用配置化”?举个最常见的例子,我们使用 VSCode 就提供了一个丰富的应用设置,它允许开发者通过配置文件来配置应用的各项功能,从而实现应用的灵活性。
在 Fantastic-admin 中,应用设置是一个非常重要的功能,它允许用户配置主题、布局、菜单、标签栏等各项功能。
分析需求
首先需要有一套默认的配置,也就是当用户不进行任何配置时,应用程序会执行的默认行为。
其次,所有配置项都是可选的,用户如果配置了其中某一项,则应用程序会使用用户配置的值,其他未配置的项则使用默认配置。
最后,为了确保配置的正确性,需要对配置进行校验,通常使用 ts 的类型检查即可。
初步实现
假设现在有 3 个配置项,分别是 title
、enableLogo
和 logo
。
interface Config {
// 标题
title?: string;
// 是否启用 logo
enableLogo?: boolean;
// logo 地址
logo?: string;
}
// 默认配置
const defaultConfig: Config = {
title: 'Fantastic-admin',
enableLogo: true,
logo: 'https://fantastic-admin.hurui.me/logo.svg',
};
这时候,我们就可以通过 Object.assign
来实现配置的合并:
const config = Object.assign(defaultConfig, {
enableLogo: false,
} as Config);
// config = { title: 'Fantastic-admin', enableLogo: false, logo: 'https://fantastic-admin.hurui.me/logo.svg' }
复杂配置
当配置项越来越多,你可能会开始考虑将配置项进行分组归类,比如:
interface Config {
app?: {
// 标题
title?: string;
}
logo?: {
// 是否启用
enable?: boolean;
// 地址
url?: string;
}
}
// 默认配置
const defaultConfig: Config = {
app: {
title: 'Fantastic-admin',
},
logo: {
enable: true,
url: 'https://fantastic-admin.hurui.me/logo.svg',
},
};
这时候如果继续使用 Object.assign
来实现配置的合并,就会发现无法满足需求了,比如:
const config = Object.assign(defaultConfig, {
logo: {
enable: false,
},
});
// config = { app: { title: 'Fantastic-admin' }, logo: { enable: false } }
合并后 logo
对象被覆盖了,导致 url
属性丢失了。其原因在于 Object.assign
并不关心值的类型,它只是简单地将值赋值给目标属性。
引入 defu
要解决这个问题,可以自己写一个递归合并函数,但是这里推荐使用 defu 这个库,它可以帮助我们更方便地实现配置的合并。
import { defu } from 'defu'
const config = defu({
logo: {
enable: false,
},
} as Config, defaultConfig);
// config = { app: { title: 'Fantastic-admin' }, logo: { enable: false, url: 'https://fantastic-admin.hurui.me/logo.svg' } }
自定义合并规则
通常情况下,配置项类型都是 boolean
、string
、number
等基础类型,但是有时候也会出现 array
类型,比如一些用于排序的配置项:
interface Config {
sort: string[];
}
const defaultConfig: Config = {
sort: ['name', 'age'],
};
const config = defu({
sort: ['age', 'name'],
} as Config, defaultConfig);
// config = { sort: ['age', 'name', 'name', 'age'] }
这显然不符合预期,这时候就需要自定义合并规则:
import { createDefu } from 'defu'
const merge = createDefu((obj, key, value) => {
// 如果源属性和目标属性的值都是数组,则覆盖
if (Array.isArray(obj[key]) && Array.isArray(value)) {
obj[key] = value
return true
}
})
interface Config {
sort: string[];
}
const defaultConfig: Config = {
sort: ['name', 'age'],
};
const config = merge({
sort: ['age', 'name'],
} as Config, defaultConfig);
// config = { sort: ['age', 'name'] }
到此为止,我们已经实现了配置的合并,也基本能满足应用配置化的需求了。
移除不存在的属性
在一些特殊场景下,配置项是异步加载的,比如用户的偏好设置,这些配置是存放在后端,在应用运行时通过接口获取并动态合并到配置中。
这就会出现一个问题,就是异步返回的配置项可能有一些不存在的属性,虽然和默认配置项合并后不会有太大影响,但总归是不符合预期的。
这时候就需要移除不存在的属性,方法也很简单,只需要稍微修改下自定义合并的规则:
import { createDefu } from 'defu'
const merge = createDefu((obj, key, value) => {
// 如果属性不存在,则移除
if (obj[key] === undefined) {
delete obj[key]
return true
}
// 如果属性是数组,则覆盖
if (Array.isArray(obj[key]) && Array.isArray(value)) {
obj[key] = value
return true
}
})
interface Config {
app?: {
// 标题
title?: string;
}
logo?: {
// 是否启用
enable?: boolean;
// 地址
url?: string;
}
}
// 默认配置
const defaultConfig: Config = {
app: {
title: 'Fantastic-admin',
},
logo: {
enable: true,
url: 'https://fantastic-admin.hurui.me/logo.svg',
},
};
const config = merge({
app: {
subTitle: '次标题',
},
logo: {
enable: false,
},
} as Config, defaultConfig);
// config = { app: { title: 'Fantastic-admin' }, logo: { enable: false, url: 'https://fantastic-admin.hurui.me/logo.svg' } }
扩展
在 Fantastic-admin 中,我将这些配置项提供了一个在线配置的功能,可以动态修改配置并预览,并且还提供了一个复制按钮,方便一键复制当前配置。
复制按钮会复制出一份完整配置,其中包含了自定义的配置和默认的配置,但其实用户只想要复制其中自定义配置的部分。
这时候需要写一个函数,将完整配置和默认配置比较,提取出其中不同的部分,也就是用户自定义的配置。
function isObject(value: any) {
return typeof value === 'object' && !Array.isArray(value)
}
// 比较两个对象,提取出不同的部分
function diffTwoObj(originalObj: Record<string, any>, diffObj: Record<string, any>) {
if (!isObject(originalObj) || !isObject(diffObj)) {
return diffObj
}
const diff: Record<string, any> = {}
for (const key in diffObj) {
const originalValue = originalObj[key]
const diffValue = diffObj[key]
if (JSON.stringify(originalValue) !== JSON.stringify(diffValue)) {
if (isObject(originalValue) && isObject(diffValue)) {
const nestedDiff = diffTwoObj(originalValue, diffValue)
if (Object.keys(nestedDiff).length > 0) {
diff[key] = nestedDiff
}
}
else {
diff[key] = diffValue
}
}
}
return diff
}
const diff = diffTwoObj(
// 默认配置
{
app: {
title: 'Fantastic-admin',
},
logo: {
enable: true,
url: 'https://fantastic-admin.hurui.me/logo.svg',
},
},
// 完整配置
{
app: {
title: 'Fantastic-admin',
},
logo: {
enable: false,
url: 'https://fantastic-admin.hurui.me/logo.svg',
},
}
)
// diff = { logo: { enable: false } }