vue的多页后台管理系统搭建

前言

最近我在公司用的前端技术组合是layui+knockout.js的组合。为什么呢?因为这是原来公司前端留下来的,以我的前段底子还算摸得清楚,用起来也不算复杂,于是就用下去了。但是吧,越用越难用。为什么呢?之所以使用这个组合,就是希望使用knockout的双向绑定功能取代dom操作,加上layui这样一个相对比较全的ui库,组合起来看似完美。但是吧,有几个组件,比如下拉选择,单选多选等等,就是需要你对其添加单独的绑定事件来处理数据,很多时候就是需要你手动刷新ui绘制才能有效果,这样搞起来就很烦。尤其是现在,越来越多的细致的功能需要去做,而重用的内容却很难搞,就陷入了泥沼之中。
再说说为啥我没有强改成vue。其实吧,我是研究过vue的,也练过手写过几篇博客在这个平台的有https://www.jianshu.com/p/29625af02d79。但是,一直以来我都没有搭建成功过一个多页应用,或者说可以自动扫描多页的应用。现在居然让我碰到了一个模板,地址是:https://github.com/Plortinus/vue-multiple-pages。然后只需要npm install就可以了。接下来我们从项目结构的分析开始吧。

基本情况

运行

这个代码down下来是可以直接运行的,下面是一些基本的操作:

npm run serve # 运行server进行调试
npm run build # 构建项目,会生成dist目录

配置

这里介绍一下主要的配置文件。

/vue.config.js

这个应该是vue项目的主要配置文件,这里可以看到对多页目录的 读取,server的基本配置。

/server.js

这个应该是运行的服务的配置,可以看到其读取的文件目录,貌似使用的是一个叫做express的包,具体我也还没有研究怎么用,后面再说吧。

/title.js

这里配置各个页面的标题,它在vue.config.js中有被使用到。或许我们可以在页面内部解决标题的问题,这个后面研究了再说。

目录结构

源代码的顶级目录分为public、src,其中public的内容很简单,看页面似乎是在js不运行的情况下展示的报错页面。src里面则是我们自己写的各种内容的页面。

/src/assets

静态资源目录

/src/components

组件目录,如果我们希望某个组件可以在不同的页面之间进行重用,把它放在这里。

/src/states

状态管理逻辑,所有的状态管理放在这里会有助于管理整个web端的状态,帮助理清思路。因为,状态,理论上来说是可以跨页面、跨组件的,之和业务本身的生命周期有关。

/src/pages

这里就是各个页面的代码了,要注意的是,页面是按照目录结构组织的,每个目录下面的app.js是该页面的入口,这个可以在vue.config.js文件中找到对应的使用。而每个页面是一个一个文件夹。访问页面的时候url为:http://域名:端口/目录结构.html,也即最后一个app.js所在的目录名字加上.html就是它的url啦。

实战demo

实际写页面的时候,我发现个问题,就是它默认的element-ui的版本太低了,于是我改成了最新的,2.13.2,这样就和官网文档的描述一致了。具体的修改方法是,在package.json文件中,找到element-ui,把后面的版本号改了。然后,在命令行里执行,npm install,就可以了。后面我们开始写页面吧。

修改登陆密码页面

由于我这个项目是补充之前项目的页面,所以不会从登陆、主页这样的写,于是我挑了个最简单的页面用来练手。修改登陆密码页面。先粘贴页面代码吧:






上面的代码,是只绘制了页面的样子的代码,可以看到script里面只有空的data及methods,具体的业务逻辑都没有实现。需要注意的是,在template里面使用到的data也好、methods也好,都需要至少进行声明,否则编译不过去,也就无法调试了。具体的需要注意的细节,也就只有el-input最后的show-password了。这是element-ui内置的一个属性,它帮助我们实现了一个密码框,右侧还有按钮可以控制密码框显示明文还是点号。需要注意的是,这个效果就受限制与element-ui的版本,最初的那个版本就无法正常使用。

修改登陆密码请求

这个页面非常简单,唯一需要的业务,也就是把修改密码的请求提交到后台。这里我们就不能用jquery了,我们将会使用一个叫做axios的框架。安装命令如下:

npm install axios

在script中进行引入,代码如下:

import axios from 'axios'

然后就可以开始进行ajax请求啦,样例代码如下:

      axios.post(
          'http://localhost:8080/api/sys/users/' + userId + '/modifyPassword',
          {
            userId: userId,
            oldPwd: 'admin123',
            newPwd: 'admin1234'
          },
          {
            headers: {
              'Content-Type': 'application/json',
              SessionToken:
                '123456'
            }
          }
        )
        .then(data => {
          console.log(data)
        })

可以看到我们这是进行的post请求。它的请求格式其实是:axios#post(url[, data[, config]]) ,也就是说,第一个参数是要请求的url,这个是必填的,然后是请求的数据。这个data就是你要请求的json字符串。我不确定axios的请求默认是json的,所以,我在后面的config里面加入了json的header,还加入了我用来做会话验证的SessionToken的头。最后在then里面是回调。
这里需要说明的是,这种调用方式肯定不是最终的调用方式,最终还需要进行生产需要的封装,以在写业务的时候可以更少得关注这些细节。但是,有了这个,可以说我们就可以完成这个页面了。剩下的只是,也只是如何使用vue及其生态了。

路径别名

在代码中import自己的js如果总是用相对路径,其实韩式蛮烦人的一件事。但是,这里面的根路径又很不好用,所以就需要引入路径别名。由于我的项目是使用vue-cli 4.0的,所以它的配置文件是vue.config.js,添加如下代码即可:

const path = require('path') //引入path模块
function resolve( dir){
  return path.join(__dirname, dir) //path.join(__dirname)设置绝对路径
}

module.exports = {
  ……
  chainWebpack: (config) => {
    config.plugins.delete('named-chunks')
    config.resolve.alias
      //set第一个参数:设置的别名,第二个参数:设置的路径
      .set('@', resolve('./'))
      .set('components', resolve('./src/components'))
      .set('assets', resolve('./src/assets'))
      .set('pages', resolve('./src/pages'))
      .set('tools', resolve('./src/tools'))
  },
……

结合自己的代码进行改造吧。在使用的时候,我们使用@,就意味着在使用./,依此类推,就有了一些目录的简短的缩写了,方便使用。

加入统一认证中心

认证中心原理

最近能够研究vue+element-ui的技术组合,也是源于设计完成了统一的认证中心才可以在一个web端统一使用两种不同的web技术来进行组合。关键点有以下几点:

  • 一个验证会话的页面:注意,这里是页面,它被放在认证中心,所有需要验证会话的页面都知道它在哪里。
  • 需要验证会话的页面中,创建一个不可见的iframe,里面用来访问验证回话的页面。通信方式使用PostMessage进行,这种通信是基于Html5标准,同时可以跨域。通信的目的是为了从验证回话页面同步会话信息到当前域下面。
  • 基本的流程控制:由于是通过页面进行跨域通信的,所以整个通信都需要在页面加载完成之后进行。待验证页面加载完成之后,才可以操作iframe的dom。iframe页面加载完成之后才可以向其发送验证请求。而验证请求的发送和接收都是异步的,页面并不会进行阻塞等待。如何处理这个流程也是该功能是否流畅稳定的关键。

统一模板组件

如何在每个页面中加入统一认证的过程,又不会在编写页面中引入过多的复杂度,这在vue的组件化体系中,我还是摸索了几种想法的,现在把我的心路历程整理下:

  • 每个页面加入一个验证组件:以前我在写vue的单页应用的例子的时候,登录页面和主页是在同一个页面中存在的,中间使用了一个登录状态标志位进行区分。整个效果是挺灵敏的,但是问题就是每个页面都会显示得直到这个逻辑的存在,而且需要在html的结构和js代码中显示得直到这个知识,这是很讨厌的一件事。但是,因为这个逻辑肯定可行,所以我后面的思路就变成了怎么把这个逻辑替换掉。
  • 页面路由:上述方案肯定能实现这个过程,但是关于验证过程的顺序要求就变得有些麻烦了。onload事件的顺序没有问题,但是PostMessage的异步通信要怎么解决。因为这个时候,页面本身可能也有需要在onload事件里执行的请求,而且需要用到其最终结果,会话标志。之前我思考到的方案是设置一个是否已验证的标志位,在未验证的时候,所有的网络请求会进入一个队列而不是直接请求。在收到验证结果后,依次执行这个队列里的请求。也是因为这个思路,我蛮不想用它的。而如果整个过程有一个页面跳转作为分隔,就变得简单多了。不过,在我找页面路由的实现方法时,我找到了我现在用的方案。
  • 写一个通用组件,里面加一个slot用来呈现页面。在vue中有一种机制叫做slot,他可以让我们改变组件里面的内容。毕竟组件的使用形式是标签,而如果通过标签里面的内容来改变组件的呈现,这似乎就是我想要的。而因为组件还可以有自己的代码逻辑,所以验证的过程就被有效得被隔离开了。于是,一切都变得那么自然,这个通用组件做我页面的顶级元素就可以了。

组件实现与应用

以下是template部分的代码:


其中,needAuthTemplate是我定义的组件,它的代码如下:




这个代码相对较长,有这么而几个点需要注意。

  • template部分,只是一个div嵌套了一个slot,也就是说这个组件展示的内容就是slot的内容。
  • script部分,可以被分为两个部分。第一段是给页面添加消息监听事件,用来处理接收会话消息;第二段则是在当前页面添加iframe的dom,在这个dom里我们访问会话验证页面。而且,注意添加dom的顺序,一定是一切都配置好了再添加到页面上。

但是这个时候其实还是有问题的:

  • vue的版本:这个问题是我在读slot的的文档的时候看到的,我默认的版本是2.5.x的版本,但是2.6.0以后,slot的标签语法发生了变化,所以,我升级到了最新的版本。需要注意的是vue和vue-template-compiler的版本要一起升级,否则编译不过。
  • 要让template中可以使用这个标签,是需要在组件文件夹额外写一个index.js文件的,文件内容为:
import needAuthTemplateComponent from './needAuthTemplate.vue'

const needAuthTemplate = {
  install: function(Vue) {
    Vue.component('NeedAuthTemplate', needAuthTemplateComponent)
  }
}

export default needAuthTemplate

注意,标签的名字是Vue.component的第一个参数。而在使用的页面的app.js的里面引入这个组件的代码也从直接引入vue文件变成了引入组件的目录,如下:

import NeedAuthTemplate from 'components/needAuthTemplate/'

Vue.use(NeedAuthTemplate)

认证中心坑

从实践来看,这次的实现依然完美实现多系统的统一认证,原因有两点:

  1. 每次都加载页面进行验证,其效率并不高,整体性能还是比较慢的。
  2. 这种设计其实并没有解决之前我说的因为异步需要等待的问题。也就是,如果你切换用户,原有页面你要刷新两次才能看到新用户的信息。而如果进行阻塞性的等待,则每次加载页面用户都会有显示的等待时间。

基于上述原因,我还是在登录的地方直接做了多域登录,现在的方式就只作为会话同步的手段了。

vue填坑记

上面我们看到了搭建认证中心过程中的坑。搭建完认证中心后,由于我们已经存在了主框架,所以接下来的重点是编写每个页面。而编写过程中遇到的重要内容,我将会记录在此。

methods中的search方法

记住,方法名千万不能用search,这似乎是methods中的内置方法名。我在用了这个方法名之后,一旦在mounted中调用,就会导致编译报错。该错误,我调试了一天,以此谨记。

通过props与子组件进行通信

这次,我的场景是一个非常常见的场景,添加和修改的弹窗。这两个弹窗我做成了一个组件。组件只是弹窗里面的内容,并不包含弹窗,方便以后复用。我需要将记录的ID传入组件内,以方便根据是否存在需要加载的ID来判断当前是当前是新增还是编辑。
所以,我需要解决这样几个问题:

  • 将ID从父组件传入进来
  • 持续得监听ID的变化

将ID从父组件传入进来

这件事情,说来简单,但是由于官方文档在介绍代码的时候并不是以vue文件的方式来介绍的,而且在说的时候不知道代码是在父组件还是子组件,所以让人困惑的老想做些别的事情。现整理如下:
首先是子组件的script部分,因为和其它部分没有关系,所以就不展示了。

export default {
  name: 'SaveLampSpecification',
  props: ['lampSpecificationId'],
  data() {
    let self = this
    return {
      form: {
        //灯具规格ID
        lampSpecificationId: self.lampSpecificationId,
        ……
      }
    }
  }
}

父组件的使用代码如下: