抖音"哲玄前端"《大前端全栈实践》学习记录
项目:Elpis
文章只是描述了大概流程和思路,离最终实现还有一段距离,也无法展示太多代码细节。会通过Elpis
配置一个名为 dashboard
的精简版页面作为示例。
概述
业务解析语言是一种面向管理业务的领域特定语言(DSL),其核心目标是通过标准化的业务描述定义,简化企业管理应用(如ERP、CRM等)的开发流程。该语言通过抽象化业务规则与实现细节,提供高度可配置的解决方案。
该语言的设计理念源自企业管理软件开发的实践需求,通过分离业务描述与技术实现,使非技术人员能够通过配置而非编码构建系统。其标化定义推动了"零代码"管理平台的实现,为业务人员直接参与信息化建设提供了工具基础。(来源:百度百科)
核心在于分离业务描述与技术实现。起因是我这个星期的工作是写几个列表页和表单页,然后我下个星期和下下星期的工作内容都差不多。于是为了提高效率对一些通用的UI进行了组件封装,然后在长期的工作逐渐统一了一些规范,于是进行一些优化,通过配置化来生成模块(类似表单生成器,把一个JSON数据传给一个组件,然后渲染出一个表单)。
然后下个月又有一个新项目,新项目跟之前的项目也长差不多样子,虽然100个产品就有80个不同的要求和样式,但来来回回都是那些东西。如何让项目能够 配置化(整个项目配置化,而不是某个页面或者某个模块配置化),并且可以快速移植和复用,甚至一套代码就可以配置多个项目。DSL 就是实现这个目的的一种方式。使用者不需要太关注我是用react还是vue开发,只需要按照要求就可以生成整个项目,满足大部分日常需求,同时也暴露了口子可以进行一些定制化页面的开发。
纯前端显然不太可能实现,因为涉及到文件的操作,接口开发等,因此这是一个全栈的项目。使用了 Node.js
作为后端语言,主要使用的框架和工具包括:
- Koa
- webpack
- Vue3 + element-plus
- ...
架构示意图

精简流程图

1. 基本流程
模板配置 model
文件夹目录如下:
markdown
└── model
├── business
│ ├── model.js
│ └── project
│ ├── pdd.js
│ └── jd.js
└── index.js
核心数据
:每个文件夹就是同一类配置,model/**/model.js 是每一类配置的基类。
css
module.exports = {
model: "dashboard",
name: "电商系统",
menu: [
{
key: "product",
name: "商品管理",
menuType: "module",
moduleType: "schema",
schemaConfig: {
api: "/api/proj/product",
schema: {
type: "object",
properties: {
product_id: {
type: "string",
label: "商品ID",
tableOption: {
width: 300,
"show-overflow-tooltip": true,
},
},
product_name: {
type: "string",
label: "商品名称",
// tableOption: {}, // 没有tableOption,不展示
searchOption: {
comType: "dynamicSelect",
api: "/api/proj/product_enum/list",
},
},
inventory: {
type: "number",
label: "库存",
tableOption: {},
searchOption: {
comType: "input",
},
},
},
},
tableConfig: {
headerButtons: [
{
label: "新增",
eventKey: "showComponent",
type: "primary",
plain: true,
},
],
rowButtons: [
{
label: "编辑",
eventKey: "showComponent",
type: "warning",
plain: true,
},
],
},
},
},
],
};
当调用model/index.js时,会把 model/**/model.js(基类)和 model/**/project 下的每个文件进行合并,返回配置列表。
yaml
// modelList
[{
model: {
model: 'dashboard',
name: '电商系统',
menu: [Array],
key: 'business'
},
project: { jd: [Object], pdd: [Object] }
}]
返回的 modelList 会被 app/service/project.js
调用

app/service/project.js
则会被 app/controller/project.js
层调用。

接下来,controller 就会被 router 调用,生成几个接口。

另一方面 router 也可以渲染页面。

所以我们访问 /view/dashboard/schema?proj_key=jd&key=product
,就会通过 nunjucks
动态解析我们打包出来的模版文件,这个模板就是一个完整的SPA单页应用的结构。这个模板就跟我们平时用vue开发一个项目有点类似。我们设计了路由,页面,同时我们也知道接口地址(api/proj
、api/proj/list
等),并且接口是遵循RESTFul的。

阶段性回顾下,按照上面的分析可以通过文件的调用链路,了解整个过程。

2. 项目模版设计
定义一个boot函数,作为启动方法

调用这个boot方法,并传入pageComponent组件和路由,就能初始化项目。pageComponent相当于我们平时vue或者react项目的 App.vue 和 App.jsx 入口文件。那为什么 entry.dashboard.js 引入 boot进行调用就能起效果呢,那是因为 webpack 配置的时候找到了 entry.xxx.js 格式的文件作为入口文件进行解析打包。
ini
const glob = require("glob");
const path = require("path");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件 entry.[pageName].js
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.**.js");
const entryFiles = glob.sync(entryList);
entryFiles.forEach((file) => {
const entryName = path.basename(file, ".js");
// 生成webpack的入口entry配置
pageEntries[entryName] = file;
//
htmlWebpackPluginList.push(
new HtmlWebpackPlugin({
// 产物(最终模板)输出路径
filename: path.resolve(process.cwd(), "./app/public/dist/", `${entryName}.tpl`),
// 指定要使用的模板文件
template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
// 要注入的代码块
chunks: [entryName],
})
);
});
module.exports = {
// 入口
entry: pageEntries,
output: { /**/ },
resolve: { /**/ },
plugins:{ /**/ },
optimization: { /**/ },
};
entry.dashboard.js 里面定义了页面的结构,例如/view/dashboard/schema
是只有头部和主体内容的,/view/dashboard/sider
是主体部分有侧边栏,点击才显示内容。
javascript
import boot from "$pages/boot.js";
import PageComponent from "./dashboard.vue";
const routes = [];
// 头部菜单路由
routes.push({
path: "/view/dashboard/iframe",
component: () => import("./complex-view/iframe-view/iframe-view.vue"),
});
routes.push({
path: "/view/dashboard/schema",
component: () => import("./complex-view/schema-view/schema-view.vue"),
});
// custom 自定义路由
routes.push({
path: "/view/dashboard/todo",
component: () => import("./todo/todo.vue"),
});
// 侧边栏菜单路由(头部菜单的子路由)
routes.push({
path: "/view/dashboard/sider",
component: () => import("./complex-view/sider-view/sider-view.vue"),
children: [
{
path: "iframe",
component: () => import("./complex-view/iframe-view/iframe-view.vue"),
},
{
path: "schema",
component: () => import("./complex-view/schema-view/schema-view.vue"),
},
{
path: "todo",
component: () => import("./todo/todo.vue"),
},
],
});
// 侧边栏兜底策略
routes.push({
path: "/view/dashboard/sider/:chapters+",
component: () => import("./complex-view/sider-view/sider-view.vue"),
});
boot(PageComponent, { routes });
下面是对一些组件的简单说明:
dashboard.vue
,全局组件,布局是上面header组件,下面是内容,预留了插槽,可以通过参数透传填充内容。后续所有的组件都会被包在 dashboard.vue 中,都会有header头,一些通用数据,可以在这个组件进行请求,然后存储到全局的状态管理store中,其他组件通过store进行读写。schema-view.vue
通用列表页组件,上面是搜索栏,下面是表格。search-panel
和table-panel
是从store中拿到上面提到的 DSL 模版数据,经过处理变成页面比较方便使用的数据,然后进行渲染。
schema—view.vue
<template>
<el-row class="schema-view">
<search-panel
v-if="searchSchema?.properties && Object.keys(searchSchema.properties).length > 0"
@search="onSearch"
></search-panel>
<table-panel @operate="onTableOperate"></table-panel>
</el-row>
</template>
<script setup>
import { provide, ref } from "vue";
import SearchPanel from "./complex-view/search-panel/search-panel.vue";
import tablePanel from "./complex-view/table-panel/table-panel.vue";
import { useSchema } from "./hook/schema.js";
const { api, tableSchema, tableConfig, searchSchema, searchConfig } = useSchema();
const apiParams = ref({});
provide("schemaViewData", {
api,
apiParams,
tableSchema,
tableConfig,
searchSchema,
searchConfig,
});
const onSearch = (searchValObj) => {
apiParams.value = searchValObj;
};
const onTableOperate = (operateObj) => {
console.log(operateObj);
};
</script>
其实后面就是结合 model/**/model.js
数据和自己想要沉淀的组件结构,进行开发。以上文的核心数据
为例。
menu.schemaConfig.schema 是一个标准的JSON-schema 格式数据,properties 下面的每个字段的tableOption
、searchOption
表示在表格和搜索是否显示以及展示的具体配置,这是属性级别的。
menu.schemaConfig.tableConfig 则是配置整个表格的,例如这个表格头部有新建按钮,这个表格的每一行都有一个操作列,有编辑、删除等。
至于接口请求,使用的是RESTFul规范,menu.schemaConfig.api 配置了 /api/proj/product
,除了请求表格数据时,调的接口是 /api/proj/product/list
,其他操作通过method的不同进行区分,删除是 delete、新增 post、查询详情是get,以此类推。
篇幅有限,细节和具体代码无法一一呈现。通过这个项目学习可以从一个更加宏观的角度去思考问题,同时打通了数据结构、页面、接口等整个链路,沉淀出更多的通用能力。