背景
我们需要将登录页用完整的配置化的方案去解决不同项目的自定义配置。比如项目的logo,企业品牌词等
分析一下页面结构
我们将页面分为上中下三层结构,即header部分,页面body部分,页脚footer部分,这也是大部分需要给业务方配置的地方。
数据结构
开始初步的设计我们自己的数据结构
解析这种数据结构
引发问题
- 虽然将页面分成了三个部分,但每个部分的框架层的参数没有做一个统一,显的比较乱。
- 如果项目之间公共部分有差异,需要做大量判断(判断哪个项目),不利于维护。
- 某个项目公共部分需求变更,可能会导致其他项目出问题,扩展性差。
- 项目需求增多,会导致配置的参数无限扩展,导致不可控。
- ……
继续调研分析
我们都知道html渲染的东西很复杂,有一颗dom树,如下展示
由此设计一种数据结构
现在又会出现一个问题
- css差异化处理
- js差异,同样的点击事件处理的效果不同
- 不同模块组件的数据共享问题
css 差异化处理
js 差异
同样的点击事件处理效果不同
方案对比:
方案1:
优点: 知道这个函数做了哪些事情,调用者可以自由掌控。
缺点: 如果里面需要做条件判断等等,这个schema就写的很繁琐。
方案2:
优点: 简洁明了,直接表明我能做什么。
缺点: 缺乏自由度,如果不满足要求,很可能要去组件里面修改。
不同模块组件的数据共享问题
登录入口的代码实现:
export default function Login (props: any) {
const dispatch = useDispatch()
const children = useSelector((state: RootStateProps) => state.page?.children) || []
const prefix = useClassnamePrefix()
useEffect(() => {
(async () => {
let loginSchema = await getDymaicFileByName('login')
// 使用的是哪个项目的schema projectA projectB?
dispatch({type: SET_LOGIN_CONFIG, config: loginSchema})
// 当前是哪个页面或者是哪个模块
dispatch({type: SET_PROJECT, project: {pageName: 'login'}})
})()
}, [])
return <div className={login-wrap ${prefix}-login-wrap}>
// children 这个可以实现无限子component的渲染
{children?.map((item, index) => <item.component key={index} {...item} />)}
}
最终数据结构
webapck编译
从执行编译的时候我们可以看到是按项目编译的
webapck自定义文件如下:
const fs = require('fs-extra');
const path = require('path');
const { camelCase } = require('lodash');
const prettier = require('prettier');
const chokidar = require('chokidar');
// 根据project参数确定 projectA projectB projectC
const project = camelCase(process.env.PROJECT);
const currentDirPath = process.cwd();
function ProjectConfigPlugin() {
}
ProjectConfigPlugin.prototype.apply = function (compiler) {
compiler.plugin('environment', function () {
outPutPageSchema();
});
// 本来应该通过webpack来监听的,但是配置半天没有反应,开发环境hack一下
// todo
chokidar.watch(path.join(process.cwd(), '/projectConfig')).on('all', () => {
outPutPageSchema();
});
};
// 生成每个页面的schema文件
function outPutPageSchema() {
const dirPath = path.join(currentDirPath, 'projectConfig', project);
const dir = fs.readdirSync(dirPath);
dir.forEach(async fileTxt => {
const fileName = fileTxt.replace('.json', '');
const json = await fs.readJson(path.join(dirPath, fileTxt));
componentToDymaic(json);
const tpl = `
import loadable from '@loadable/component'
import { nodeProps } from "@/schema";
const schema: nodeProps = ${JSON.stringify(json, null, 2)}
export default schema
`;
const outPath = path.join(currentDirPath, src/pages/${fileName}, ${fileName}.schema.ts);
fs.outputFileSync(outPath, prettier.format(tpl).replace(/"$|$"/g, ''));
});
}
// 动态路径替换
function componentToDymaic(object) {
if (object.component) {
object.component = $loadable(()=> import('@/components/${object.component}'))$;
}
if (object.children) {
object.children.map(item => componentToDymaic(item));
}
}
module.exports = ProjectConfiguration;
// webapck plugins中引入
webapck.plugins.push([
new ProjectConfigPlugin()
])
schema目录如下:
login文件如下:
{
"nodeType": "page",
"children": [
{
"nodeType": "header",
"component": "block/header",
"props": {
"href": "https://cloud.baidu.com/",
"img": "/logo.svg",
"txt": "HCM系统"
}
},
{
"nodeType": "mainbody",
"component": "block/mainBody",
"props": {
"showChildrenIndex": 0
},
"children": [
{
"nodeType": "login",
"component": "block/login",
"props": {
"admin": {
"title": "登录",
"links": [
{
"label": "立即注册",
"href": "/login/register"
},
{
"label": "找回密码",
"href": "/login/reset"
}
],
"loginSuccess": "xxxxx",
"openSecret": false
},
"nomal": {
"title": "登录",
"loginSuccess": "/",
"openSecret": false
}
}
}
]
},
{
"nodeType": "copyright",
"component": "block/copyRight",
"props": {
"textArr": ['xxxxx']
}
}
]
}
最终完整架构图:
结语
webpack在执行命令的时候将json输出为对应的文件,然后再由webpack进行打包生成对应的产物,输出展示,这样只需要改变json文件即可,更好的使用也可以用于得代码。