背景
开发一个活动时,往往有多个页面,创建项目后进行路由管理。
提到路由管理,我们很容易想到vue-router,以往路由的component属性我们会传入一个已经import的组件或组件结构,如下所示
import List from './list.vue';
new VueRouter({
routes: [
{
path: '/',
component: {
template: `<div>Home</div>`
}
}, {
path: '/list',
component: List
}, {
path: '/detail',
component: import('./detail.vue')
}
]
});
然而一般低代码平台内页面渲染并没有用import这种方式,而是通过项目projectSchema中pages字段描述的页面pageSchema渲染对应页面
{
"pages": [{
"title": "页面1",
"name": "page1",
"path": "/page1",
"elements": [{
"_id": "el-button-1",
"elName": "el-button",
"propsValue": {
"schema": {
"text": {
"type": "String",
"value": "按钮1"
}
}
}
}]
}]
}
这个时候路由该如何配置,以支持我们在SPA项目中实现多页/路由的需求呢?
本文将对上述问题做出解答,会设涉及一些低代码的核心渲染原理。
技术方案
技术选型
vuex:页面pageSchema的状态管理
vue-router:页面路由切换
主要流程
定义项目数据 projectSchema
export default {
// 默认显示页面的name
"defaultPageName": "page2",
// 页面schema数组
"pages": [{
// 页面标题
"title": "页面1",
// 页面name
"name": "page1",
// 页面路径
"path": "/page1",
// 页面元素
"elements": [{
// 组件id
"_id": "el-button-1",
// 组件name
"elName": "el-button",
// 组件属性
"propsValue": {
// 组件schema
"schema": {
"text": {
// 属性类型
"type": "String",
// 属性值
"value": "按钮1"
}
}
}
}]
},
{
"title": "页面2",
"name": "page2",
"path": "/page2",
"elements": [{
"_id": "el-text-2",
"elName": "el-text",
"propsValue": {
"schema": {
"text": {
"type": "String",
"value": "文字2"
}
}
}
}]
}]
};
定义文本组件 (el-text)
const ElText = {
name: 'el-text',
props: {
schema: {
type: Object,
default() {
return {
text: {
type: 'String',
value: '文字'
}
}
}
}
},
template: `<span>{{this.schema.text.value}}</span>`
};
Vue.component('el-text', ElText);
export default ElText;
定义按钮组件 (el-button)
const ElButton = {
name: 'el-button',
props: {
schema: {
type: Object,
default() {
return {
text: {
type: 'String',
value: '按钮'
}
}
}
}
},
template: `<button>{{this.schema.text.value}}</button>`
};
Vue.component('el-button', ElButton);
export default ElButton;
定义 store 状态
假设进入页面我们默认显示projectSchema中name为page2的页面,因此我们的store应该有一个记录当前页name的state,和一个更新name的mutation,以及一个获取当前页pageSchema的getter。
// 状态
const store = new Vuex.Store({
state: {
// 项目schema
projectSchema: projectSchema,
// 当前页name
activePageName: projectSchema.defaultPageName
},
getters: {
// 当前页schema
activePageSchema: state => state.projectSchema.pages.find(page => page.name === state.activePageName)
},
mutations: {
// 更新当前页name
updateActivePageName(state, payload) {
state.activePageName = payload;
}
}
});
这里记录当前页用的是activePageName,但是projectSchema中pages是个数组,用数组索引index会不会更方便?
需要考虑到一种情况,当我们切换路由的时候往往是用this.$router({ name }),因此采用name更合适。
定义页面模板 PageTemplate 组件(渲染核心)
export default {
template: `
<div class="page-wrapper">
<div v-for="element in pageSchema.elements" :key="element._id">
<component :is="element.elName" v-bind="element.propsValue" />
</div>
</div>
`,
props: {
pageSchema: {
type: Object,
default () {
return {
title: '',
name: '',
path: '',
elements: []
}
}
}
}
};
在页面模板中通过component标签循环pageSchema的elements字段,根据组件elName属性渲染当前页的所有组件,属性则使用v-bind指令将propsValue透传给对应组件。
定义路由 router(重点)
const routes = [
{
name: 'home',
path: '/',
redirect: '/page1'
},
{
name: 'page1',
path: '/page1',
component: PageTemplate,
props: route => ({ pageSchema: store.getters.activePageSchema})
},
{
name: 'page2',
path: '/page2',
component: PageTemplate,
props: route => ({ pageSchema: store.getters.activePageSchema})
}
];
route配置和我们平时写的不太一样,多了props属性,这是vue-router路由组件传参的函数模式,可以很方便的将属性传递给路由对应的组件,当然也可以用属性传参。
我们将store中当前页的schema即activePageSchema传给页面模板组件,这样在切换路由时就能渲染页面模板PageTemplate对应的页面元素了。
定义页面入口文件
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover" name="viewport" />
<title>vue-router</title>
<style type="text/css">
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<ul class="page-change">
<li v-for="page in projectSchema.pages" :key="page.name" @click="() => handlePageChange(page.name)"><a href="javascript:;">{{page.title}}</a></li>
</ul>
<router-view></router-view>
</div>
<script src="./vue.min.js"></script>
<script src="./vuex.js"></script>
<script src="./vue-router.js"></script>
<script type="module">
import './components/el-text.js';
import './components/el-button.js';
import PageTemplate from './pageTemplate.js';
import projectSchema from './projectSchema.js';
// 状态
const store = new Vuex.Store({
state: {
projectSchema: projectSchema,
activePageName: projectSchema.defaultPageName
},
getters: {
activePageSchema: state => state.projectSchema.pages.find(page => page.name === state.activePageName)
},
mutations: {
updateActivePageName(state, payload) {
state.activePageName = payload;
}
}
});
// 路由
const routes = [
{
name: 'home',
path: '/',
redirect: '/page1'
},
{
name: 'page1',
path: '/page1',
component: PageTemplate,
props: route => ({ pageSchema: store.getters.activePageSchema}),
// 路由独享的守卫
beforeEnter: (to, from, next) => {
console.log('route.beforeEnter', to, from);
next();
}
},
{
name: 'page2',
path: '/page2',
component: PageTemplate,
props: route => ({ pageSchema: store.getters.activePageSchema})
}
];
// 创建路由
const router = new VueRouter({
routes
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
console.log('router.beforeEach', to, from);
next();
});
// 全局解析守卫
router.beforeResolve((to, from, next) => {
console.log('router.beforeResolve', to, from);
next();
});
// 全局后置钩子
router.afterEach((to, from) => {
console.log('router.afterEach', to, from);
});
// 创建APP
const app = new Vue({
el: '#app',
store,
router,
components: {
PageTemplate
},
computed: {
...Vuex.mapState({
projectSchema: state => state.projectSchema,
activePageName: state => state.activePageName
}),
...Vuex.mapGetters({
activePageSchema: 'activePageSchema'
})
},
methods: {
handlePageChange(name) {
this.$router.push({
name
}).catch(err => err);
},
},
watch: {
$route: {
handler(to, from) {
// 查找当前页
const { name } = this.$router.options.routes.find(route => route.name === to.name);
this.$store.commit('updateActivePageName', name);
},
deep: true,
immediate: true
}
}
});
</script>
</body>
</html>
示例代码
下载后请将后缀名.lake改为.zip并解压
windows电脑双击目录中的server.bat即可
mac电脑用terminal切换到当前目录,运行node ./server.js命令,根据终端显示的预览地址(例:http://localhost:8081),浏览器打开即可