低代码平台-路由管理

1,266 阅读2分钟

背景

开发一个活动时,往往有多个页面,创建项目后进行路由管理。

提到路由管理,我们很容易想到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>

示例代码

📎vue-router.lake

下载后请将后缀名.lake改为.zip并解压

windows电脑双击目录中的server.bat即可

mac电脑用terminal切换到当前目录,运行node ./server.js命令,根据终端显示的预览地址(例:http://localhost:8081),浏览器打开即可