Vue, App与我(五)
- Big-man今天非常的生气,至于为什么?那就是本身的app上没有添加页面跳转记住上一页面的位置功能,上网查询了很多的资料,说得都比较的模糊和不全,所以决定自己写一下。用自己理解的语言。
h5
的History:javascript: history.go(-1);
或者javascript: history.back();
这个是h5自带页面跳转模型(返回上一页),但是这个跳转会出现url
链接整个刷新,所以也就不会记录住上一次浏览的位置以及滑动条位置。所以这里需要去介绍一下h5.history()
的其他属性。
History
介绍:History
接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。- 属性:
History
接口不继承于任何属性。刚看到这句话的时候, Big-man真的很不相信这句话,但是它的原话是The History interface doesn’t inherit any property.既然是官方文档给出的答复,我暂时还是相信一下吧。接下来开始介绍一下具体属性:
History.length
:- 返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。例如,在一个新的选项卡加载的一个页面中,这个属性返回1。
History.current
:- 返回一个代表
session
历史记录中活动的项目URL
的DOMString
,这个属性永远对web内容不可用并且已经不再被任何浏览器支持。可以使用Location.href
来代替它。
History.next()
- 返回表示会话历史记录中下一个项目的
URL
的DOMString
。 此属性从不可用z在网页内容中,并且也不受浏览器支持。
History.previous
- 返回表示会话历史中先前项目的URL的DOMString。 此属性从不可用于网页内容,并不受其他浏览器支持。
History.scrollRestoration
- 允许Web应用程序在历史导航上显式地设置默认滚动恢复行为。此属性可以是自动的(auto)或者手动的(manual)。
History.state
- 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待popstate 事件而查看状态而的方式。
- 方法:
History
接口不继承任何方法。这是官方文档给出的答案,当时我是无法接受的,接口不继承方法,Big-man自我感觉不太可能吧。所以暂时不相信这部分内容。
History.back()
- 前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).刚开始的时候,Big-man已经在论述中进行了讲解。
- 注意:当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。
- 这里可以观察一下效果图:
History.forward()
- 在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1)。
- 注意:当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。
History.go()
:- 通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为-1的时候为上一页,参数为1的时候为下一页. 当整数参数超出界限时( 译者注:原文为
When integerDelta is out of bounds
),例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为-1,那么这个方法没有任何效果也不会报错。调用没有参数的go()
方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为url参数的IE有点不同)。
History.pushState()
- 将给定的数据推送到具有指定标题的会话历史堆栈(如果提供)的URL。 数据被DOM处理为不透明; 您可以指定可序列化的任何JavaScript对象。 请注意,Firefox目前忽略标题参数; 有关更多信息,请参阅操纵浏览器历史记录。
- 注意:在·Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)·中,通过Gecko 5.0(Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2),传递的对象使用JSON进行序列化。 从Gecko 6.0(Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)开始,使用结构化克隆算法序列化对象。 这允许更多种类的物体被安全地通过。
History.replaceState()
- 更新历史堆栈上的最新条目以具有指定的数据,标题和(如果提供的话)URL。 数据被DOM处理为不透明; 您可以指定可序列化的任何JavaScript对象。 请注意,Firefox目前忽略标题参数; 有关更多信息,请参阅操纵浏览器历史记录。
- 在Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)中,通过Gecko 5.0(Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2),传递的对象使用JSON进行序列化。 从Gecko 6.0(Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)开始,使用结构化克隆算法序列化对象。 这允许更多种类的物体被安全地通过。
HTML History
模式:vue-router
默认hash
模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。- 如果不想要很丑的
hash
,我们可以用路由的 history 模式,这种模式充分利用history.pushState
API 来完成 URL 跳转而无须重新加载页面。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
- 当你使用
history
模式,URL
就像正常的url
,例如http://yoursite.com/user/id
, 也好看! - 不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问
http://oursite.com/user/id
就会返回404, 这就不好看了。 - 所以呢?你要在服务器端增加一个覆盖所有情况的候选资源:如果
URL
匹配不到任何静态资源,则应该返回同一个index.html
页面,这个页面就是你app
依赖的页面。
- 后端配置代码:
Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
nginx
location / {
try_files $uri $uri/ /index.html;
}
Node.js (Express)
- Express
- 警告:
- 给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。
vue <keep-alive></keep-alive>
:- 这个不是业务的要求,但是看到每次进入页面就重新渲染
DOM
然后再获取数据更新DOM
,觉得作为一个前端工程师有必要优化下的加载逻辑,正好vue
提供了keep-alive
的功能,所以就试用了下。当然,干任何事儿都不会一帆风顺的,在路上的磕磕碰碰在所难免,故在此记录下遇到的问题,希望看到这篇文章的人能有所帮助。ps:这个也没多难。
HTML
部分:
<template>
<div class="app">
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
JavaScript
部分:
.....
created: function () {
console.log(1)
},
mounted: function () {
console.log(2)
},
activated: function () {
console.log(3)
},
deactivated: function () {
console.log(4)
}
.....
- 什么阶段获取数据:
- 页面生命周期钩子如上面的代码所示,这四个是常用到的部分。这部分需要注意下,当引入
keep-alive
的时候,页面第一次进入,钩子的触发顺序created->mounted->activated
, 退出时触发deactivated
。当再进入(前进或者后退)时,只触发activated
。
keep-alive
之后页面模板第一次初始化解析变成HTML片段,再次进入就不在重新解析而是读取内存中的数据,也就是说只有当数据变化时,才使用VirtualDOM
进行diff
更新。故,页面进入的数据获取应该在activated
中也放一份。数据下载完毕手动操作DOM的部分也应该在activated
中执行才会生效。- 所以,应该
activated
中留一份数据获取的代码,或者不要created
部分,直接将created
中的代码转移到activated
中。
$route
中的数据读不到- 以前的写法是在data中将需要的$route数据进行赋值,便于其余方法使用,但是使用了keep-alive后数据需要进入页面在activated中再次获取,才能达到更新的目的。定义一个initData方法,然后在activated中启动。
initData: function () {
let _this = this;
_this.fromLocation = JSON.parse(this.$route.query.fromLocation);
_this.toLocation = JSON.parse(this.$route.query.toLocation);
_this.activeIndex = parseInt(this.$route.params.activeIndex) || 0;
_this.policyType = parseInt(this.$route.params.policyType) || 0;
},
- 3. 当页动态修改url
需求描述:当页面在进行轮播操作的时候希望能记录当前显示的轮播
ID(activeIndex)
也就是mt-swipe
标签中的index
。当进入下一个页面再返回的时候能记住之前的选择,将轮播打到之前的ID位置。所以我想将这部分信息固化在url中,轮播发生变化时,修改URL。这样实现比较符最小修改原则,其余页面不用变动。之前的写法是将
activeIndex
放在$route
的query
中,当轮播后,将
activeIndex
的值存入$route.query.activeIndex
中,然后$router.replace
当前路由,理论上应该能发生变化,但实际没有。查看文档后说,
$route
是只读模式(readOnly)。当然,对象部分是他监管不到的,我修改了并不是正统的做法。神奇的地方来了:当我将
activeIndex记在params
中,轮播变动修改params
中的参数,然后$router.replace
当前路由,却能发生对应的变化。代码如下:
let swiperInstance = new Swiper('#swiper', {
pagination: '.swiper-pagination',
paginationClickable: false,
initialSlide: activeIndex,
onSlideChangeEnd: function (swiper) {
let _activeIndex = swiper.activeIndex;
_this.$route.params.activeIndex = _activeIndex;
// $router我放到了window上方便调用
window.$router.replace({
name: _this.$route.name,
params: _this.$route.params,
query: _this.$route.query
});
// 根据activeIndex,在这里初始化下面显示的数据
_this.transferDetail = _this.allData.plans[_activeIndex].segments;
_this.clearBusDetailFoldState();
}
});
- 4. 事件如何处理
估计你也能猜到,发生的问题是事件绑定了很多次,比如上传点击input监听change事件,突然显示了多张相同图片的问题。
也就是说,DOM在编译后就缓存在内容中了,如果再次进入还再进行事件绑定初始化则就会发生这个问题。
解决办法:在mounted中绑定事件,因为这个只执行一次,并且DOM已准备好。如果插件绑定后还要再执行一下事件的handler函数的话,那就提取出来,放在activated中执行。比如:根据输入内容自动增长textarea的高度,这部分需要监听textarea的input和change事件,并且页面进入后还要再次执行一次handler函数,更新textarea高度(避免上次输入的影响)。
- 5. document.title修改
这个不是keep-alive的问题,不过我也在这里分享下。
问题是,使用下面这段方法,可以修改Title,但是页面来回切换多次后就不生效了,我也不知道为啥,放到setTimeout中就直接不执行。
document.title = '页面名称';
下面是使用2种环境的修复方法:
纯js实现:
function setDocumentTitle(title) {
"use strict";
//以下代码可以解决以上问题,不依赖jq
setTimeout(function () {
//利用iframe的onload事件刷新页面
document.title = title;
var iframe = document.createElement('iframe');
iframe.src = '/favicon.ico'; // 必须
iframe.style.visibility = 'hidden';
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.onload = function () {
setTimeout(function () {
document.body.removeChild(iframe);
}, 0);
};
document.body.appendChild(iframe);
}, 0);
}
- jQuery/Zepto实现:
function setDocumentTitle(title) {
//需要jQuery
var $body = $('body');
document.title = title;
// hack在微信等webview中无法修改document.title的情况
var $iframe = $('<iframe src="/favicon.ico"></iframe>');
$iframe.on('load', function () {
setTimeout(function () {
$iframe.off('load').remove();
}, 0);
}).appendTo($body);
}
- 6. 地图组件处理:
想必这是使用keep-alive最直接的性能表现。之前是进入地图页面后进行地图渲染+线路标记;现在是清除以前的线路标记绘制新的线路,性能优化可想而知!
我这里使用的是高德地图,在mounted中初始化map,代码示例如下:
export default {
name: 'transferMap',
data: function () {
return {
map: null,
}
},
methods: {
initData: function () {},
searchTransfer: function (type) {},
// 地图渲染 这个在transfer-map.html中使用
renderTransferMap: function (transferMap) {}
},
mounted: function () {
this.map = new AMap.Map("container", {
showBuildingBlock: true,
animateEnable: true,
resizeEnable: true,
zoom: 12 //地图显示的缩放级别
});
},
activated: function () {
let _this = this;
_this.initData();
// 设置title
setDocumentTitle('换乘地图');
_this.searchTransfer(_this.policyType).then(function (result) {
// 数据加载完成
// 换乘地图页面
let transferMap = result.plans[_this.activeIndex];
transferMap.origin = result.origin;
transferMap.destination = result.destination;
// 填数据
_this.transferMap = transferMap;
// 地图渲染
_this.renderTransferMap(transferMap);
});
},
deactivated: function () {
// 清理地图之前的标记
this.map.clearMap();
},
}
keep-alive
带来的问题: Big-man进行了keep-alive
的尝试,尝试代码如下:vue -main.js
const scrollBehavior = (to, from, savedPosition) => {
if(savedPosition) {
return savedPosition
}
else {
return {x:0, y:0}
}
}
<div><keep-alive><router-view class="view"></router-view></keep-alive></div>
- 这里也就需要展示一下效果图了:
keep-alive
会把位置的scroll滚动条下记录下来, 传递给接下来的页面,但是同时页面也会记录scroll滚动条的位置。导致下一个页面的滚动条位置是上一个页面的滚动条位置,也就可想而知,Big-man进行下一个页面的位置调整,同样的道理会传递给返回的页面。- 这样也就没有解决Big-man心目当中想要的效果。
vuet.js
:
- 提及到
vuet.js
, Big-man也是不太熟悉,但是很快Big-man就被其吸引了,那就直接进入主题,看看它与前面的所有路由跳转究竟有何不同。
- 提及到
vuet.js
是什么?Vuet.js
是给Vue.js
提供状态管理的一个工具,与vuex
不同,它是一种崇尚规则定制的状态管理模式。事先将状态更新的规则写好,然后将规则注入到组件中,然后状态按照预订的规则来进行更新。这是官方给出来的解答,而在实际应用中的Big-man来说,他的理解就是,vuet.js
是你事先定义好的一套规则,状态转换规则,就相当于你的论文规范,你必须理解好这个规范才能比较好的毕业,类似地,程序必须遵守vuet.js
写出的规则才能完成程序的执行步骤。
- 主动型和被动型规则
Vuet.js
内置了life
,manual
,need
,once
,route
这几种常见的规则外,除了manual
规则之外,其他的都是属于主动型更新规则,在达到一定条件上会自动触发状态更新。
life
- 描述:每次都会在组件的
beforeCreate
钩子中调用一次更新,组件销毁时在destroyed
钩子,状态会被重置,恢复到初始状态。 - 在一个父组件中,想和自己的子组件进行通信,但是又不希望父组件销毁之后,原来的状态还在,
life
规则就是专门针对这种场景的, 在组件销毁时,模块的状态也会随之恢复到初始状态。
manual
:manual
规则允许将各种更新模块状态的方法集中起来管理,等待用户来手动触发对应的模块更新,比如记录用户点击一个按钮的次数:
<!--index.html-->
<div id="app">
{{ count }}
<button @click="$count.plus">计数</button>
</div>
<script>
// main.js
import Vue from 'vue'
import Vuet, { mapModules, mapRules } from 'vuet'
const vuet = new Vuet({
modules: {
count: {
data () {
return 0
},
manuals: {
plus ({ state }) {
// 允许同步、或者异步的更新
this.setState(++state)
}
}
}
}
})
export default new Vue({
el: '#app',
vuet, // vuet实例注入到vue实例
mixins: [
mapModules({ count: 'count' }), // 组件连接模块
mapRules({
manual: 'count' // 使用manual规则向组件注入操作模块数据的更新方法
})
]
})
</script>
- 通过上面的代码,就可以得知
Vuet.js
是天然的支持多组件进行通信,总之它是简单的,敏捷的。manual
规则默认以$
模块名称将方法集合注入到组件中,使得代码在阅读方面会更友好,更通俗易懂,同时代码也会更优雅。
need
:- 描述: 每次都会在组件的
beforeCreate
钩子中调用一次更新 - 比如有一个消息的数量,我希望每次打开消息页面的时候,消息数量都能自动更新,这种场景使用
need
规则就再合适不过了。
once
:- 描述: 仅第一次在组件的
beforeCreate
钩子中调用一次更新,之后在任何组件都不会再进行更新。 - 比如你A、B、C三个页面,都需要选择省市区,而这些数据几乎是不可变的,所以之后就没有再必要进行更新了。once的规则就能帮你节省了不必要的请求,帮你优化程序。
route
: 这个属性Big-man也是没有真正的领悟它的作用。也就是在vue中的router——实现列表点击详情返回后显示之前的数据。- 下面是进行操练的代码:
- 首先是需要像Big-man这样把需要的库文件安装,请看以下的安装库文件代码:
npm install --save vue vue-router vuet
- 第二步,我们需要创建一个实例:
import Vue from 'vue'
import Vuet from 'vuet'
Vue.use(Vuet)
const { fetch } = window // 位于 WorkerOrGlobalScope 这一个 mixin 中的 fetch() 方法用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。
export default new Vuet({
data () {
return {}
},
modules: {
cnode: { // 定义模块名称
list: { // 这里可以随便起个名称
data () { // 定义这个数据的基本字段
return {
list: []
}
},
routeWatch: 'query', // route插件的配置,如果有多个条件的话,可以设置一个数组
fetch () { // 配置请求的方法,必须return一个Promise
const search = this.app.$route.fullPath.split('?')[1] || ''
return fetch(`https://cnodejs.org/api/v1/topics?${search}`)
.then(response => response.json())
.then((res) => {
return { list: res.data }
})
}
},
detail: { // 这里是详情,和列表页面同理
data () {
return {
id: '',
author_id: '',
tab: '',
content: '',
title: '',
last_reply_at: '',
good: false,
top: false,
reply_count: 0,
visit_count: 0,
create_at: '',
author: {
loginname: '',
avatar_url: ''
},
replies: [],
is_collect: false
}
},
routeWatch: 'params.id',
fetch () {
return fetch(`https://cnodejs.org/api/v1/topic/${this.app.$route.params.id}`)
.then(response => response.json())
.then((res) => {
return res.data
})
}
}
}
}
})
- 第三步,我们创建
Vue
和VueRouter
的实例,main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import vuet from './vuet'
import List from './List'
import Detail from './Detail'
Vue.use(VueRouter)
// 配置路由相关
const router = new VueRouter({
routes: [
{
path: '/',
name: 'index',
component: List
},
{
path: '/:id',
name: 'detail',
component: Detail
}
]
})
export default new Vue({
el: '#app', // 这里是因为演示的目的,随便写的,根据你的程序写对应的初始化元素
vuet, // 在Vue的根组件中安装vuet
router,
render (h) {
return h('router-view')
}
})
- 第四步,我们创建列表组件List.vue
<template>
<div>
<header>
<span v-for="(item, $index) in tabs" :key="item.value">
<router-link :to="{ name: 'index', query: { tab: item.value } }">
{{ item.label }}
</router-link>
</span>
</header>
<ul>
<li v-for="(item, $index) in list.list" :key="item.id">
<router-link :to="{ name: 'detail', params: { id: item.id } }">
{{ item.title }}
</router-link>
</li>
</ul>
</div>
</template>
<script>
import { mapRules, mapModules } from 'vuet'
export default {
mixins: [
mapRules({ route: 'cnode/list' }), // 使用route来做完更新的规则
mapModules({ list: 'cnode/list' })
],
data () {
return {
// 其实这里的数据是写死的数据,大家可以放到local插件中使用的。
// 具体怎么用,大家自己发挥一下想象力咯
tabs: [
{
label: '全部',
value: 'all'
},
{
label: '精华',
value: 'good'
},
{
label: '分享',
value: 'share'
},
{
label: '问答',
value: 'ask'
},
{
label: '招聘',
value: 'job'
}
]
}
}
}
</script>
- 第五步,我们创建详情的组件,Detail.vue
<template>
<div>
<h2>{{ detail.title }}</h2>
<article v-html="detail.content"></article>
</div>
</template>
<script>
import { mapRules, mapModules } from 'vuet'
export default {
mixins: [
mapRules({ route: 'cnode/detail' }), // 使用route来做完更新的规则
mapModules({ detail: 'cnode/detail' })
]
}
</script>
- 总结:
vuet
允许你将有规律的状态更新,封装成一种规则,从而提升你的开发效率,比如说需要定时向服务器更新消息,这也是一种规则,下次有时间,我们可以专门写这样的一个规则。
Jackdan9 Thinking