25 后台搭建功能:如何设计和实现Vue.js运营后台的搭建功能?
你好,我是杨文坚。
在课程项目“运营搭建平台”的功能分析中,我们把平台功能分成三大功能维度,“用户维度”“物料维度”和“页面维度”。
页面的数据结构,其实就是物料数据源的组成,上节课我们用JSON Schema来描述物料数据源,等于描述了页面的数据结构。物料维度相关的内容和技术实现就告一段落。从今天开始,我们进入页面功能维度的学习。
页面功能维度,是我们运营搭建平台项目最后一个功能维度,也是最重要、最复杂的功能维度。
页面功能维度可以分成五大功能模块,“页面搭建”“页面编译和运行”“页面发布流程”“页面版本管理”和“页面渲染方式”,按照业务逻辑的操作顺序,先有“页面搭建”,才能生产页面数据,从而带动后续的功能操作。所以,这节课我们就来学习运营搭建平台的“页面搭建”功能。
如何设计页面搭建的数据格式
之前我们说过,页面是由物料组成的,物料是由Vue.js组件和数据源组成的,而数据源构成页面的数据结构。所以,不管是页面还是物料,最底层的构成元素就是“数据”和“静态资源”,页面的搭建,也是围绕着“数据”和“静态资源”来实现的。
那么,我们要做的事情就是页面的数据格式设计。
不过设计搭建页面的数据格式之前,我们要先对页面布局结构做设计。毕竟,页面不是简单的物料叠加组合,而是根据有规范的排列规则,来排列物料。这个排列规律就是布局设计。当布局设计出来后,我们根据布局里的“坑位”来填充物料,就能形成一张完整的页面。
那么要如何做布局设计呢?
首先,我们要做“布局规范”的选择。在前端领域里,页面布局规范有很多种,比如网格布局、绝对定位布局、流式布局等等。在常见的搭建场景中,现在比较主流的布局规范是网格布局。
网格布局,顾名思义就是把页面网格化,页面由“行”和“列”两种模块元素组成。一个或多个“列布局”模块沿着水平方向,横向组成一个“行布局”模块,一个或多个“行布局”模块沿着垂直方向,纵向组成页面。
有网格布局的规范选择,接下来就是要做布局的数据设计了。我这里画了一张图辅助你理解。
网格布局,你可以直接理解成“行列布局”,那么布局数据的格式就是“页面数据”嵌套了“行数据”,“行数据”里嵌套了“列数据”。
我们可以基于TypeScript来设计页面布局数据格式。
// 列布局
interface LayoutColumn {
name?: string;
}
// 行布局
interface LayoutRow {
columns: LayoutColumn[];
}
// 布局
interface Layout {
rows: LayoutRow[];
}
设计了布局的基本数据格式后,还要考虑布局尺寸问题。我们都知道,用浏览器访问页面,常规浏览的视角是页面从上到下,也就是说页面的高度是动态变化的。
这就意味着,在网格布局中,页面的每一行高度,我们可以根据列布局中物料的高度弹性变化,但是页面的宽度就要做限制,每一行的宽度,受到顶级布局宽度的限制,带动着行里的每一列就要分割限制的宽度。这时候布局数据的TypeScript数据类型可以扩展添加宽度属性。
// 列布局
interface LayoutColumn {
name?: string;
width: string | number;
}
// 行布局
interface LayoutRow {
columns: LayoutColumn[];
}
// 布局
interface Layout {
width: number | string;
rows: LayoutRow[];
}
现在我们有了完整的布局数据格式,接下来就要把物料填充到网格布局中的每一列中。
怎么操作呢?在布局数据中直接添加“物料数据格式”到“列数据格式”?理论上可以这么做,但是在“列数据”中添加“物料数据”,会导致整个“页面布局数据”的数据层级太多,实际JSON的“数据深度”太大。
我们可以在每一个列数据中留下一个uuid,代表每一个物料模块,然后在布局数据格式的同个层级中,定义一个模块的Map对象数据,来包含所有物料模块数据格式。模块Map对象数据的键值Key就是每列的uuid,指向所引用的列。
模块数据可以这么设计,每个独立的物料模块中,包含了物料组件的名称,版本号和物料数据源。看具体的TypeScript代码。
// 物料模块
interface LayoutModule {
materialName: string;
materialVersion: string;
materialData: Record<string, any>; // 物料数据源
}
// 物料模块Map
interface LayoutModuleMap {
[uuid: string]: LayoutModule; // uuid指向每一列里的uuid
}
到了这里,我们有了网格布局数据格式,也有物料模块数据格式,把他们整合起来,就是完整的页面布局数据。
看完整的TypeScript数据类型代码。
// 列布局
interface LayoutColumn {
uuid: string; // 列唯一的uuid,指向物料模块的uuid
name?: string;
width: string | number;
}
// 行布局
interface LayoutRow {
uuid: string; // 行唯一的uuid
columns: LayoutColumn[];
}
// 布局
interface Layout {
width: number | string;
rows: LayoutRow[];
}
// 物料模块
interface LayoutModule {
materialName: string;
materialVersion: string;
materialData: Record<string, any>; // 物料数据源
}
// 物料模块Map
interface LayoutModuleMap {
[uuid: string]: LayoutModule;
}
// 完整的页面布局数据
interface PageLayoutData {
layout: Layout;
moduleMap: LayoutModuleMap;
}
基于这段完整的页面布局数据的TypeScript类型,我来实现一个页面搭建的JSON对象数据例子。
{
"layout": {
"rows": [
{
"uuid": "7a3dbfa7-29ca-4765-bd58-7a91abda7721",
"columns": [
{
"name": "其它广告位1",
"uuid": "2be63a53-8a16-43ba-8640-a1ab24bdbae3",
"width": 600
},
{
"name": "其它广告位2",
"uuid": "0184e8ed-7df4-4742-9bb4-b8dfa1a0aca7",
"width": 400
}
]
},
{
"uuid": "0fdbe663-51d8-4038-9d0d-bdeed0c6eb71",
"columns": [
{
"name": "促销商品模块",
"uuid": "26148a93-e66a-4448-b6a4-4bddbce9ae4f",
"width": 1000
}
]
}
],
"width": 1000
},
"moduleMap": {
"26148a93-e66a-4448-b6a4-4bddbce9ae4f": {
"materialData": {},
"materialName": "@my/material-product-list",
"materialVersion": "0.9.0"
},
"2be63a53-8a16-43ba-8640-a1ab24bdbae3": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
},
"0184e8ed-7df4-4742-9bb4-b8dfa1a0aca7": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
}
}
}
为了统一描述,这里的“布局数据”和“物料模块数据”整合后的数据,我们就统称为“页面布局数据”。
我们已经设计了用于页面搭建的“页面布局数据”,我也举例模拟搭建页面的JSON对象数据,接下来就可以实现页面搭建了。
页面搭建的实现原理,就是要基于“搭建页面数据”,再结合“物料组件资源”,也就是物料的JavaScript和CSS静态资源文件,来完成页面的搭建功能。按照我们之前的功能实现套路,首先要做功能逻辑链路设计,然后基于功能逻辑设计来做技术方案设计,最后落地成实际代码。那么,要如何设计页面搭建的功能链路呢?
如何设计页面搭建的功能链路
从页面布局数据的分析步骤可以看出,实现一个搭建的页面,需要先制定页面的布局,然后再向布局中填充物料。所以页面搭建功能链路,就是从布局到物料的操作过程。
我们能很清晰地把页面搭建功能链路分成五个步骤。
- 第一步:操作页面的行列布局
- 第二步:填充物料模块到列布局中
- 第三步:编辑物料的数据源
- 第四步:发布页面布局数据
- 第五步:支持重新编辑页面布局数据
我们分析一下功能链路上每一步的操作逻辑。
第一步,操作页面的行列布局。这一步,主要功能是支持添加“行布局”,在新增“行布局”时候,支持定义所在“行布局”中的“列布局”。这里的定义“列布局”包括了“列”的个数和每一列分割的宽度。具体操作界面就像这样。
这两张图就是课程的代码案例,实现了行布局的新增和列布局的定义。
从上面页面布局数据定义过程中,我们可以知道,“列”是整个页面布局中的最小单位,也就是说,最终物料是填充到“列布局”中的。
所以,第二步,填充物料模块到“列布局”中,我们先进行物料的查询,查询验证成功后就填充到“列布局”中。具体操作界面就像这样。
第三步,编辑物料的数据源。在这个步骤中,支持对每一列中的物料模块,进行数据源的编辑。根据已填充的物料组件,显示对应的数据源编辑表单,编辑对应的数据源。具体操作界面我也截图了。
截图展示了根据对应的物料,显示对应的数据源编辑表单。当提交了数据源给物料后,渲染出新的效果。
第四步,发布页面布局数据。这一步也就是提交完整的页面布局数据(布局和物料模块数据),存储到数据中,支持下次编辑。发布成功后,跳转到页面列表页面。
第五步,支持编辑页面布局数据。这一步中,支持对已发布的页面数据进行重新修改,基本复用了上面的所有功能点,包括编辑行列布局、填充物料组件、编辑物料数据源和发布修改数据。
到这里,我们就完成了页面搭建的功能逻辑链路的设计,是不是逻辑很清晰。
接下来,我们就要把这些功能逻辑设计进行技术层面的“翻译”,也就是技术方案的设计。
如何做页面搭建的技术方案设计
按照我们之前的套路,功能的技术方案设计,其实就是根据功能逻辑设计的步骤,对应找到合适的或者熟悉的技术实现方式。那我们就根据上面的功能逻辑设计的步骤,来定制技术方案。
第一步,操作页面的行列布局。这一步骤中,主要是实现布局的操控。首先,是“行布局”的操控,我们可以基于Vue.js的响应式数据来实现“行布局”的“增删改”操作。然后是“列布局”的定义,我们在行布局的新增时候,就可以用动态表单来实现对应“列布局”的宽度定义。
“列布局”还可以用我们之前课程中实现的“拖拽布局组件”来支持“列布局”的位置拖拽。说到这里,你可能有疑问,为什么“行布局”不用“拖拽布局组件”,而是直接操作响应式数据来控制呢?
这是因为行布局是垂直排列的,超出浏览器屏幕高度就会出现页面滚动,不方便拖拽,所以用数据来操控实现更合适。如果硬要“拖拽布局组件”来实现,就要改造“拖拽布局组件”的深层嵌套操作。这会增大工作量,而且在超长页面中,拖拽操作也对用户不友好,开发收益不大。
具体的列布局拖拽效果就像这样。
第二步,填充物料模块到“列”布局中。这里就用到我们之间课程中,组件库相关的“对话框组件”和“动态表单组件”。今天的代码案例中,我已经改造了“对话框组件”,让它支持渲染嵌套子组件。最终技术逻辑是弹出一个对话框,嵌套一个动态表单来进行填充物料。
在这个步骤中,我们还要基于选中的填充物料,把对应组件的静态资源,也就是JavaScript和CSS文件进行加载,并且渲染到对应“列布局中”。
在代码案例中,我选用了物料Vue.js组件的AMD模块,进行组件动态加载和动态渲染。主要是封装了一个动态渲染物料模块的JSX组件。
// packages/work-front/src/pages/manage/modules/page-editor.tsx
import * as Vue from 'vue';
// 动态初始化AMD 运行时环境
async function initAMDEnv(params: {
materialName: string;
materialVersion: string;
}) {
const { materialName, materialVersion } = params;
if (!window.requirejs) {
await loadScript('/public/cdn/pkg/requirejs/2.3.6/require.js');
}
const paths: Record<string, string> = {};
// 注册Vue的AMD模块,AMD和当前应用共用同一个Vue.js运行时
window.define('vue', [], () => Vue);
paths[materialName] = `/public/cdn/material/${materialName}/${materialVersion}/index.amd`;
window.requirejs.config({
paths
});
}
// 创建动态物料预览模块 的 Vue.js组件
function createEditModule(params: {
materialName?: string;
materialVersion?: string;
materialData?: any;
// 省略其它参数...
}) {
const { materialName, materialVersion, materialData } = params;
// 物料挂载的 "外壳组件"
const EditModule: DefineComponent = defineComponent({
setup() {
const container = ref<HTMLElement>();
onMounted(async () => {
if (!(materialName && materialVersion)) {
return;
}
const params = {
name: materialName,
version: materialVersion
};
// 初始化 AMD 运行时环境
await initAMDEnv({ materialName, materialVersion });
// 加载物料样式
await loadMaterialStyle(params);
window.require( ['vue', materialName],
(Vue: any, MaterialComponent: any) => {
// 这里拿到的Vue是跟当前应用是同一个Vue.js运行时
// 动态加载物料组件的AMD模块
const App = Vue.h(MaterialComponent, materialData || {});
const app = Vue.createApp(App, {});
// 在"外壳组件"中动态挂载物料的AMD格式Vue.js组件
app.mount(container.value);
}
);
});
// 中间省略其它代码 ....
return () => {
return (
<div style={{ width }} class="module-page-edit-module">
{/* 中间省略其它代码 .... */}
<div class="page-edit-module-content" ref={container}></div>
</div>
);
};
}
});
return EditModule;
}
从代码和注释,你可以看出,填充物料的技术实现,就是动态渲染物料的Vue.js组件。核心步骤是先初始化 AMD 运行时环境,然后动态加载AMD格式组件,再生成动态的外壳组件,最后在外壳组件里,挂载物料的Vue.js组件。
第三步,编辑物料的数据源。这一步骤,可以直接复用上节课编辑物料数据源的表单组件,结合对话框组件一起使用,实现动态获取数据源的JSON Schema,动态渲染数据源表单,最后实现编辑数据源的功能。
第四步,发布页面布局数据。这要开发对应的HTTP服务接口,支持提交数据到服务端,并且存储到数据库中。具体技术实现都是重复性的SQL操作和Node.js逻辑代码实现,可以复制修改之前物料操作的代码,对数据库中的页面数据进行“增改查”操作。
第五步,支持编辑页面布局数据。这一步最简单,基于上述四个步骤实现的功能,添加默认数据填充和提交数据接口的修改。
到这里,基于页面搭建的功能逻辑设计,我们就实现了技术方案设计,最终的完整实现代码,你可以看课程对应的代码案例。
总结
通过今天的学习,相信你已经掌握了页面搭建的功能实现操作。从功能实现视角总结。
- 页面搭建流程主要分成两个部分,第一是要先做布局设计,第二是填充物料到布局中。所以页面搭建是围绕着“操作布局”和“填充物料”来进行的。
- 页面搭建核心点是“操作布局”和“填充物料”,底层核心数据就是“布局数据”和“填充的物料模块数据”。所以页面搭建的数据设计,首先要考虑到“布局”和“布局填充”的数据。
我们再从技术视角总结一下。
- 页面搭建过程中,对布局和物料的编辑等操作,会遇到大量表单场景,技术方案选择动态表单,能大大提高开发效率。
- 拖拽技术不一定能适用所有页面搭建功能,要考虑到页面超长出现浏览器滚动场景,拖拽操作就体验不友好。
- 页面布局操作,涉及大量的数据设计和数据操作,所以类似搭建项目,要尽量使用TypeScript进行开发,限制数据类型格式。
思考题
留给作业给你,在今天中的页面搭建技术方案设计中,为什么渲染物料组件考虑用AMD模块格式,而不选用ESM模块格式呢?
期待能在留言区看到你的想法。通过今天的学习,希望你能掌握页面搭建的实现要点,举一反三,不管之后遇到什么样的搭建场景,都能游刃有余地按照套路进行功能实现和技术思考。下节课见。
完整的代码在这里
- cyw0220 👍(1) 💬(1)
AMD动态加载比较方便吧
2023-02-13 - ifelse 👍(0) 💬(0)
学习打卡
2024-09-27 - Geek_12e8fd 👍(0) 💬(0)
在页面搭建技术方案设计中,选择使用AMD模块格式而不是ESM模块格式来渲染物料组件,主要是基于以下几个方面的考虑: 异步加载:AMD(Asynchronous Module Definition)模块允许模块在需要时异步加载,这对于页面搭建来说非常有用。因为页面搭建过程中,可能需要根据用户的操作动态地加载和渲染不同的物料组件。AMD的异步加载特性可以确保在需要某个组件时再进行加载,从而优化页面加载性能。 浏览器兼容性:虽然现代浏览器普遍支持ESM(ECMAScript Modules),但在一些较旧的浏览器版本中,ESM的支持可能不够完善。使用AMD模块可以确保更好的浏览器兼容性,特别是对于那些需要支持旧版本浏览器的页面搭建场景。 配置和工具链集成:在某些情况下,现有的构建工具链和配置可能已经针对AMD模块进行了优化。例如,一些打包工具(如RequireJS)默认就支持AMD模块,这使得在现有工具链中集成AMD模块更加容易。相比之下,集成ESM模块可能需要额外的配置和工具支持。 模块依赖管理:AMD模块定义中明确指出了模块的依赖关系,这有助于在模块加载过程中进行依赖管理。在页面搭建过程中,物料组件之间可能存在复杂的依赖关系,使用AMD模块可以确保这些依赖关系得到正确的处理。 需要注意的是,随着现代前端技术的发展和浏览器对ESM支持的不断完善,ESM模块格式在前端项目中的应用也越来越广泛。因此,在选择使用AMD还是ESM模块格式时,需要根据具体的项目需求和场景来做出决策。在一些新的或者对性能要求较高的项目中,ESM模块格式可能是一个更好的选择。
2024-06-14 - Geek_c5f625 👍(0) 💬(0)
老师能帮忙看下具体的错误吗,按照课程代码运行在page-editor.tsx中出现 ReferenceError: __VUE_HMR_RUNTIME__ is not defined
2023-09-16