如何从无到有搭建一套完整的低代码平台(三)通用组件库的配置
一. 介绍
上一节设计了一下client的基本配置。选用vite + ts + vue3.0搭建前端核心,有兴趣的同学可以移步去看一下上期的内容。接下来我们继续按部就班的实现一下接下来的功能。
二. client总览
1. 基本布局
简单看一下市面上的成熟低代码平台,他们的desgin页面的基本布局
不看其他的功能,主要就是左边为可选择组件(原子组件)、中间是一个design的画布、右侧是选中每个组件的属性配置面板。
我们可以得出结论, 对于一个低代码平台的页面design部分,主要是三个要素。
- 原子组件
- 页面布局
- 单组件属性配置面板
实现这三个部分是最重要的。我们先来实现一下原子组件。
三. 通用可配置组件库
1. UI库
根据风格喜好和使用习惯来说,我这边选择element的牛逼plus版本,支持vue3.0 官网地址
这里要注意的一点是,不要直接在client项目中安装element-plus,我这边考虑的是单独建一个UI包,因为之后不仅仅是单独使用现有的element-plus的组件,还会对其进行二次封装。而且也希望原子组件和业务组件分开。 ok,我们开始!
cd packages && cd commonUI
pnpm add element-plus
官方提供了完整引入和手动引入的形式。考虑到原子组件的组合问题,这边采用手动引入的方式
这里以Button组件为例:
新建 src/Button/index.ts
import { ElButton } from 'element-plus';
import 'element-plus/es/components/button/style/css';
export default ElButton;
单组件手动引入的时候一定要注意一下手动引入css样式
新建src/index.ts
import ElButton from './BUtton/index';
export {
ElButton
}
修改一下package.json文件:
"main": "./src/index.ts",
"type": "module"
这样我们完成了一个基本组件的配置和导出。
回到client中:在第一篇中我们对packages里面的包都做了互相依赖。所以这里可以直接引用
在client的app.vue文件中引入
<template>
<div class="box">
<h1>lowCode</h1>
<ElButton>Default</ElButton>
</div>
</template>
<script lang="ts" setup>
import { ElButton } from '@six-membered/ui';
</script>
<style lang="less" scoped>
@import url('./index.less');
</style>
打开页面:
可以看到button已经被引用进来
2. 自动组件生成
看一下Button/index.ts文件,发现,其实所有从element-plus导出的组件,都可以采用这种方式引入,而且引入的配置代码及其相似。那我们是不是可以写个脚本,批次生成这样的组件,不用在手动一个个写啦!(这里先不考虑组合以及二次组件扩展的问题
)
其实也很简单,就是通过node的fs模块,向文件目录中写入对应的文件。然后做一下异常处理即可。
file-save
市面上比较火的创建文件和写入文件的API。
file-save 有个好处就是可以接受链式调用。这样的话给我们提供了极大的便利!
简单试一下:
向commonUI根目录写入一个文件
新建builder/bin.js
const fileSave = require('file-save');
const template = `
import { ElButton } from 'element-plus';
export {
ElButton
}
`
fileSave('./test.ts')
.write(template, 'utf-8')
.end('\n')
.finish(() => {
console.log('写入文件完成!')
})
向根目录的test.ts文件写入模版内容(有test.ts直接写入,没有创建)
在终端执行一下:
在根目录上生成了一个新的test.ts文件
OK,接下来就好办了,我们维护一个json文件,通过读取这个json文件的配置自动生成对应的目录。 在commonUI的根目录下新建 components.json,这里保存着组件的名称和在目录下的文件路径。
{
"Button": "./src/Button/index.ts",
"Card": "./src/Card/index.ts",
"Dialog": "./src/Dialog/index.ts",
"ButtonGroup": "./src/ButtonGroup/index.ts"
...
}
在bin.js中读取一下:
const fileSave = require('file-save');
const componentsMap = require('../components.json');
思考一个问题,既然要批量生成,那肯定要有个模版,根据之前的Button/index.ts我们不难看出,这个模版作为生成每个组件里面的内容,需要接受两个参数
- 组件名称
- 组件路径
OK,那我们实现一下模版。
在builder目录下新建template.js:
function getTemplate(variable) {
const { name } = variable;
function setComponentTemplate() {
return `
import { El${name} } from 'element-plus';
import 'element-plus/es/components/${name.toLowerCase()}/style/css'
export default El${name};
`
}
}
return {
setComponentTemplate
}
}
module.exports = getTemplate;
这里要注意几个问题
template模版顶格写,避免生成出来的js代码有空行,会导致代码较乱。
name.toLowerCase()是有问题的,因为有些组件的样式引入的路径不是这样。
比如这个breadcrumb-item就不是采用这样的引入方式。 这里需要做一下文件名称的处理,放在最后实现。
回到bin.js里面,导入这个模版。
但是要注意一下的是,需要单独定义一个readTemplate,因为最后要单独生成一下index.ts。
// 需要累积组合import和export,执行一次,去替换之前的结果
import El${name} from './${name}/index';
export {
${name}
}
const fileSave = require('file-save');
const componentsMap = require('../components.json');
const fileTemplateFuntion = require('./template');
// 这里单独定义一个readTemplate,因为最后要单独生成一下index.ts
// import和export单独处理
let readTemplate = {
import: '',
export: ''
}
for (let key in componentsMap) {
// 遍历一下componentsMap, 累积生成readTemplate(先生成,不写入文件中)
// 在生成readTemplate的同时,循环生成单组件对应的文件。
const getComponentTemplate = fileTemplateFuntion({ name: key }).setComponentTemplate();
readTemplate.import += `import El${key} from './${key}/index';\n`;
readTemplate.export += ` El${key},\n`;
fileSave(componentsMap[key])
.write(getComponentTemplate, 'utf-8')
.end('\n')
}
// 重新定义一个render模版,拼接成需要渲染的模块,写入文件中。
const renderTemplate = `
${readTemplate.import}
export {
${readTemplate.export}
}
`
// 文件写入
fileSave('./src/index.ts')
.write(renderTemplate, 'utf-8')
.end('\n')
OK,这里一个模版的渲染就完成了。我们在package.json里面修改一下启动命令:
"scripts": {
"ui:create": "node builder/bin.js"
}
然后 pnpm run ui:create
看一下现在的文件目录:
根据components.json生成对应的文件。还是比较节省时间和金钱的有么有!!
3. 修改配置组件名称
之前我们提到了这个问题,不是所有组件的样式都是按照button、card这样的方式引入的,所以我们这里要做一个补丁去修复一下这个问题。
去到element-plus源码里面看一下目录结构:
可以看出来,文件的名称就是style样式引入的名称。转化一下就是对应的组件名称。 转换有两种规则:
- 带'-'连接的,比如breadcrumb-item文件名被转换为ElBreadcrumbItem
- 不带'-'的。比如button、dialog 文件名被转换为ElButton、ElDialog
OK,那就以element-plus/es/components/** 里面的文件名
称作为components.json里面的配置文件写法
修改一下components.json:
{
"button": "./src/Button/index.ts",
"card": "./src/Card/index.ts",
"dialog": "./src/Dialog/index.ts",
"button-group": "./src/ButtonGroup/index.ts"
}
新建一个uppercaseAndLowercase.js:
function upperCase(char) {
if (!char.length) return;
// 通过replace把首字母替换为大写
return char.replace(char.charAt(0), char.charAt(0).toUpperCase());
}
function upperCaseAndCharacters(char) {
if (char.includes('-')) {
// 把`-`分割,然后对生成的数组每一项单独进行首字母大写替换
const _char = char.split('-');
return _char.reduce((prev, current) => {
return prev += upperCase(current)
}, "")
}
else {
return upperCase(char);
}
}
module.exports = {
upperCase,
upperCaseAndCharacters
}
可以简单做个测试:
目前看起来没有什么问题。
下一步,我们对模版template.js和bin.js进行改造:
- template.js
function getTemplate(variable) {
// 在加一个处理好的参数,还是需要保留components.json里面的name,这个是引入style的路径
const { name, transformName } = variable;
function setComponentTemplate() {
return `
import { El${transformName} } from 'element-plus';
import 'element-plus/es/components/${name}/style/css'
export default El${transformName};
`
}
return {
setComponentTemplate
}
}
module.exports = getTemplate;
最终写入的模版有两种:
- 第一种:
import { ElButton } from 'element-plus';
import 'element-plus/es/components/Button/style/css'
export default ElButton;
- 第二种:
import { ElButtonGroup } from 'element-plus';
import 'element-plus/es/components/button-group/style/css'
export default ElButtonGroup;
-
bin.js
const fileSave = require('file-save');
const componentsMap = require('../components.json');
const fileTemplateFuntion = require('./template');
const { upperCaseAndCharacters } = require('./uppercaseAndLowercase');
let readTemplate = {
import: '',
export: ''
}
for (let key in componentsMap) {
// 对参数进行处理一下,转成我们想要的格式
const _key = `${upperCaseAndCharacters(key)}`;
const getComponentTemplate = fileTemplateFuntion({ name: key, transformName: _key}).setComponentTemplate();
readTemplate.import += `import El${_key} from './${_key}/index';\n`;
readTemplate.export += ` El${_key},\n`;
fileSave(componentsMap[key])
.write(getComponentTemplate, 'utf-8')
.end('\n')
}
const renderTemplate = `
${readTemplate.import}
export {
${readTemplate.export}
}
`
fileSave('./src/index.ts')
.write(renderTemplate, 'utf-8')
.end('\n')
ok,现在在components.json中增加几项来试试。
- components.json
{
"button": "./src/Button/index.ts",
"card": "./src/Card/index.ts",
"dialog": "./src/Dialog/index.ts",
"button-group": "./src/ButtonGroup/index.ts",
"dropdown": "./src/Dropdown/index.ts",
"dropdown-item": "./src/DropdownItem/index.ts",
"dropdown-menu": "./src/DropdownMenu/index.ts"
}
pnpm run ui:create
现在看一下目录结构
正好就是我想要的效果!那么我们在client里面使用一下试试
回到client目录: app.vue,从官网copy一下下拉菜单的案例,稍微修改一下。
<template>
<div class="box">
<h1>lowCode</h1>
<ElButton>Default</ElButton>
<ElDropdown>
<span class="el-dropdown-link">
Dropdown List
</span>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>Action 1</ElDropdownItem>
<ElDropdownItem>Action 2</ElDropdownItem>
<ElDropdownItem>Action 3</ElDropdownItem>
<ElDropdownItem disabled>Action 4</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
<script lang="ts" setup>
import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu } from '@six-membered/ui';
</script>
<style lang="less" scoped>
@import url('./index.less');
</style>
项目启动一下:
可以看到dropdown已经被加载进来了。
真nice!!
三. 结尾
基本组件库配置什么的就算完成了,接下来继续实现别的。有兴趣的同学可以点一波关注哦!!!感谢🙏
参考文章:
往期文章:
转载自:https://juejin.cn/post/7270916734138990592