美文网首页前端开发那些事儿VUE学习vue
vue的多页后台管理系统搭建

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

作者: 本然酋长 | 来源:发表于2020-07-22 09:23 被阅读0次

    前言

    最近我在公司用的前端技术组合是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,就可以了。后面我们开始写页面吧。

    修改登陆密码页面

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

    <template>
    <div>
        <el-form ref="form" :model="form" label-width="80px">
            <el-form-item label="原密码">
                <el-input v-model="form.oldPwd" show-password></el-input>
            </el-form-item>
            <el-form-item label="新密码">
                <el-input v-model="form.newPwd" show-password></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="onSubmit">修改登陆密码</el-button>
            </el-form-item>
        </el-form>
    </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          form: {
            oldPwd: '',
            newPwd: ''
          }
        }
      },
      methods: {
        onSubmit() {
          console.log('submit!')
        }
      }
    }
    </script>
    
    <style>
    </style>
    

    上面的代码,是只绘制了页面的样子的代码,可以看到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部分的代码:

    <template>
    <needAuthTemplate>
        <el-form ref="form" :model="form" label-width="80px">
            <el-form-item label="原密码">
                <el-input v-model="form.oldPwd" show-password></el-input>
            </el-form-item>
            <el-form-item label="新密码">
                <el-input v-model="form.newPwd" show-password></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="onSubmit">修改登陆密码</el-button>
            </el-form-item>
        </el-form>
    </needAuthTemplate>
    </template>
    

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

    <template>
    <div>
      <slot></slot>
    </div>
    </template>
    <script>
    import config from '@/config.js'
    console.log(config)
    
    export default {
      data() {
        return {
          // configData: config.authUrl
          configData: 'http://localhost/page/sys/sessionCheck.html'
        }
      },
      methods: {
        sendCheckCmd(e) {
          console.log('sendCheckCmd:' + e)
        }
      },
      mounted: function() {
        /**
         * 注册监听事件
         */
        window.addEventListener(
          'message',
          function(e) {
            //如果e中含有type,说明是系统的消息,否则则尝试解析自定义消息
            if (e.data.type != undefined) {
              return
            }
            console.log(e.data)
            if (e == undefined || e == null) {
              return
            }
    
            var cmdData = JSON.parse(e.data) //获取json对象
            //如果cmdType不存在,说明入参非法
            if (cmdData.cmdType == undefined || cmdData == null) {
              return
            }
    
            switch (cmdData.cmdType) {
              case 'info':
                console.log(cmdData.sessionId)
                if (cmdData.sessionId == undefined || cmdData.sessionId == null) {
                  //跳转登录
                  window.location = 'http://localhost'
                } else {
                  //刷新会话标志
                  localStorage.setItem('sessionId', cmdData.sessionId)
                  //加载后续页面
                }
                break
            }
          },
          false
        )
        //创建用来验证会话的iframe
        let bodyDoc = document.querySelector('body')
        let iframe = document.createElement('iframe')
        iframe.style.width = '0px'
        iframe.style.height = '0px'
        iframe.style.display = 'none'
    
        iframe.onload = () => {
          var cmdData = {}
          cmdData.cmdType = 'check'
          iframe.contentWindow.postMessage(JSON.stringify(cmdData), '*')
          console.log('加载完成') // 这样每次都会触发
        }
        iframe.src = this.configData
    
        //将dom附加到窗口中
        bodyDoc.appendChild(iframe)
        console.log('mounted')
      }
    }
    </script>
    <style>
    </style>
    

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

    • 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,
            ……
          }
        }
      }
    }
    

    父组件的使用代码如下:

    <template>
    ……
      <el-dialog title="添加灯具规格" :visible.sync="control.saveFormVisible" width="500px">
        <SaveLampSpecification :lampSpecificationId="control.selectedLampSpecificationId"></SaveLampSpecification>
      </el-dialog>
    ……
    </tempalte>
    <script>
    import SaveLampSpecification from 'components/basicData/saveLampSpecification/saveLampSpecification.vue'
    
    export default {
      components: {
        SaveLampSpecification
      },
      data() {
        ……
          control: {
            //保存灯具规格的显隐设置
            saveFormVisible: false,
            selectedLampSpecificationId: 1
          }
        }
      },
    ……
    }
    </script>
    

    然后我们总体来说一下。首先是在子组件中,需要声明props以说明都有哪些属性可以使用。官网说着这里有个驼峰转横杠的自动命名转换。亲测,没有。然后,你声明的这个属性的访问方式其实和data里的是一样的,在我的代码中可以看到。但是,为了方便使用,我还是在data里声明了另一个属性,让它等于这个属性。这就是子组件的全部。而父组件,在使用的时候,将该属性和data的某个值进行绑定,也就形成了子组件和外部组件属性的关联。不过呢,这个时候你会发现一件事。当你改变外面的属性值后,里面的属性值是不会跟着改变的。这里需要在子组件中添加一个监听代码来搞定它:

      watch: {
        lampSpecificationId() {
          let self = this
          self.form.lampSpecificationId = self.lampSpecificationId
        }
      },
    

    watch是vue的标准用法,我就不写这段代码的上下文了。总之,这样就完成了内外属性的关联绑定了。

    重复打开弹窗遇到的坑

    首先,无意中发现,默认情况下弹窗在任意非弹窗位置点击就关闭了。不过,找到属性,设置掉就可以了。另外我还遇见了另一个属性,即每次关闭都销毁相关的元素。调整后代码如下:

      <el-dialog title="添加灯具规格" 
        :visible.sync="control.saveFormVisible" width="500px" 
        :close-on-click-modal="false" 
        :destroy-on-close = "false">
        <SaveLampSpecification 
          :lampSpecificationId="control.selectedLampSpecificationId" 
          v-on:saved="saveLampSpecification">
        </SaveLampSpecification>
      </el-dialog>
    

    这些呢,只能算是小坑。后面预见一个大坑。编辑数据的时候,除了第一次,没次打开都是上次的数据。这让我很震惊。我是将加载数据写在mounted里面的,难道这里有问题?开始得时候我以为是因为异步加载数据导致数据没更新,就查了半天vue更新数据不更新界面怎么弄。后来我发现,每次查询数据的日志是在我关闭界面的时候打的,于是我怀疑生命周期不是我预想的样子,于是把加载放到了上面写的watch里面,一切就都好了。

    重点说明下destroy-on-close

    上面的destroy-on-close属性,默认值就是false,只是我发现了它之后将它设置成了true。这将导致每次弹窗关闭后,其组件都会被销毁,下次打开就需要重新渲染。

    • 你在页面中是无法拿到这个组件的实例的,因为每次不显示的时候它都被销毁了。
    • 由于我一个组件同时干了创建和编辑的事情,而我又无法获取它的实例,于是上面的逻辑bug就很难处理。比如,我点开创建的时候,里面是空的。再点击一个编辑,加载了数据。关闭后再点开同一个编辑,就是空的。为什么呢?因为watch没有被触发,而组件又被销毁了,所以它理所当然的变成了初始的样子。而将这个设置为false后,一切都ok了。

    综上所述,该属性的使用场景需要仔细考虑。

    input的点击事件

    很神奇的是,你会发现,在element ui的文档中,input是没有点击事件的。但是,其实这个事件我们用的并不少。于是找了下怎么加点击事件,结果说,你可以使用原生的click事件就可以了,代码大概如下:

    <el-input v-on:click.native='clickSelectNode' ></el-input>
    

    component 动态切换

    在我的一个功能点上,我需要某个位置的组件根据不同的数据进行不同的展示。由于我数据都是按照产品型号进行划分的,所以不同的型号我做成了不同的组件。它们在动态组件component标签上进行切换。但是这种切换其实并不是说换一个属性就完了这么简单。为什么这么说呢?首先,假设,我有三个型号,每个型号有三十条数据。当我点开型号一的时候,它会触发mounted事件。但是,当我再点开另一条型号一的数据时,这个事件就不会再被触发了,vue把它存起来了。而在组件的生命周期中,我并没有找到准确得符合这个时机的事件。这就是它恶心的地方。
    开始,我尝试了一种简单粗暴的解决方法,设置component的ref,然后换了组件后直接通过ref调用里面一个我约定了的方法。结果就是,我得等下次才能够调用到上次的ref。我没有找到确切的原因,猜测是因为我设置的是组件的名称,创建是异步的,所以我拿到的实际还是上一次的实例。
    后来,我又查到了直接实例化组件。其实我成功了,但是,我只是成功调用到了方法,它里面注入的组件,组件里的数据都不存在,也就是它没有经过完善的生命周期。
    最后,我回到了原始的处理思路。设置一个props传参,watch它,结合mounted事件,覆盖了所有的变化时机。

    element ui 表格行单击和行内按钮重叠

    鉴于我写的是个管理系统,表格就变成了交互的重点之一。为了减少需要使用的按钮,我将查看记录详情的操作变成了单击某行。但是,行间也有操作按钮。这个时候点击这些操作的按钮就变成了它们既执行自己的事件,也会执行行单击事件。这就有些蛋疼了。不过这件事是由于js的冒泡导致的。知道原因就好说了。但是吧,要说纯js,好说,这使用vue和element ui封装过的,怎么处理就有些讨厌了。不过查了下,还是有简单的方式的。在操作按钮那里套个div,注明组织点击事件传递即可,代码如下:

        <el-table-column
          label="操作">
          <template slot-scope="scope">
            <div @click.stop>
            <el-button v-on:click="modifyLampSpecification(scope.row.sensorId)">编辑</el-button>
            <el-button v-on:click="deleteLampSpecification(scope.row.sensorId)">删除</el-button>
            </div>
          </template>
        </el-table-column>
    

    这里只展示了一个column的代码,不过应该足够看明白了。

    element ui timepicker

    其实这个组件使用起来相当简单。让我非常满意的是很灵活。之前我用layui的时候,选择时间,是无法配置只选择小时、分钟的,必须连秒一起选择。而这里,通过配置format就可以控制只有小时和分钟两个选择,棒棒哒。另外,值得一说的就是,设置它的值默认是js的时间对象,如果先用字符串,需要你设置value-format才可以。

    相关文章

      网友评论

        本文标题:vue的多页后台管理系统搭建

        本文链接:https://www.haomeiwen.com/subject/jvwscktx.html