前言
最近我在公司用的前端技术组合是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)
认证中心坑
从实践来看,这次的实现依然完美实现多系统的统一认证,原因有两点:
- 每次都加载页面进行验证,其效率并不高,整体性能还是比较慢的。
- 这种设计其实并没有解决之前我说的因为异步需要等待的问题。也就是,如果你切换用户,原有页面你要刷新两次才能看到新用户的信息。而如果进行阻塞性的等待,则每次加载页面用户都会有显示的等待时间。
基于上述原因,我还是在登录的地方直接做了多域登录,现在的方式就只作为会话同步的手段了。
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才可以。
网友评论