Vue 路由及异步组件
背景
后端路由 koa express
前后端开发未分离的时候,路由基本上都是由服务端控制
前端/客户端 发起http 请求 -> 服务端 -> 根据url 路径去匹配不同的路由 / 返回不同的数据(Restful接口)
优点:
- 类似于现在的ssr,直接返回一个html,渲染了页面结构
- seo 的效果好,首屏时间耗时短
首屏时间:浏览器地址栏输入一个url 开始 -> 页面上任意元素展示出来(白屏时间)
缺点: 前端和后端代码融合在一起,开发协同很乱,构建html的过程放在服务器上
现代前端:打包 -> webpack上传到oss或者cdn,对外直接暴露个地址,通过 nginx 代理到html上
单页应用
SPA
页 -> html 单页 -> 只有一个html文件
多页面彼此之间数据不能交互
多页 -> 一个团队维护多个业务,业务又存在一些共同点没办法拆成多个项目 a -> a.html 单页应用 b -> b.html 单页应用
特点:
- 页面中的交互是不刷新页面的,比如从 /home -> /about,并不会刷新页面,比如点击按钮;内存中的变量都可以访问的
- 加载过的公共资源无需重复加载。除了首页之外的其他页面,都通过异步组件的方式注册(只有在访问其他页面的时候才会加载资源)
前端路由 router 原理 及其表现
vue -> hash, history
react -> hash, history
底层都会依托浏览器提供的API
特点:
- 页面间的交互不会刷新页面
- 不同 url/路径/路由 会渲染不同的内容
面试:hash 和 history 的区别?
- hash 有 #,history 没有
- hash 的 # 部分内容不会传给服务端,history的所有url内容服务端都可以获取到
- history 路由,应用在部署时,需要注意 html 文件的访问:
单页应用,只有一个index.html 用history 路由/a /b /c 都会访问index.html 文件,用户在输入index.html 之内的路径,无论那个路径,都指向 index.html
hash 路由在 html 文件后 有index.html#a, index.html#b,本身就是一个文件,不需要用nginx 指向index.html
- 监听变化:hash 通过 hashchange 监听,history 是通过 popstate 监听变化
Hash
特性
- url 中带有一个#,# 知识浏览器/客户端的状态,不会传给服务端
如 www.baidu.com/#/user -> 发起 http 请求,-> 服务端只能拿到 www.baidu.com www.baidu.com/#/list/deta… -> 发起 http 请求,-> 服务端只能拿到 www.baidu.com
- hash 值的改变,不会导致页面的刷新
location.hash = '#aaa'
location.hash = '#bbb'
页面不刷新,url会变
-
hash 值的更改,会在浏览器访问历史中添加一条记录。可以通过浏览器的返回、前进按钮 来控制 hash 的切换(其实 返回,就是上一条hash 记录,同理 前进)
-
hash 值的更改,会触发hashchange时间
监听:
location.hash = '#aaa'
location.hash = '#bbb'
window.addEventListener('hashchange', () => {})
- 如何更改hash
5.1 location.hash = '#aaa'
5.2 点击跳转到user
History (nginx 部署)
特性
- hash有个符号,不美观,服务端无法接收到hash部分
window.history.back() // 后退
window.history.forward() // 前进
window.history.go(-3) // 正数前进,负数后退
window.history.pushState() // 相当于location.href(会刷新页面,但是history不会) 页面的浏览记录里会添加一个历史记录
window.history.replaceState() //相当于location.replace(会刷新页面,但是history不会) 页面的浏览记录里会替换当前的历史记录
pushState / replaceState() 的参数
window.history.pushState(null,'new',path)
- state, 是一个对象,是一个与指定网址相关的对象
- title, 新的页面地址
- url, 页面的新地址
面试题
pushState 时候,会触发popState 事件吗?
pushState: 往页面里推入一个新的页面栈,新的历史记录 popState: 监听页面的弹出等事件
- pushState/replaceState 都不会触发popState 事件,需要手动触发页面的重新渲染。 --> 这里和hash 不同,hash 只要改变里 就会触发 hashchange
popState 什么时候才会触发?
- 点击浏览器后退按钮
- 点击浏览器前进按钮
- js back
- js forward
- js go
history 想要部署的时候,如何配置 nginx?
nginx 配置
- index.html 文件存在服务器本地 -> 如 nginx 部署在 a 服务器,以及 index.html 也在 a 服务器
如要访问 www.lubai.com/main/
配置 nginx
location /main/ {
try_files $uri $uri/ /home/dist/index.html
}
// 当要访问 main 时,会去查找 当前目录下的 index.html 文件
此方法 无论是访问www.lubai.com/main/a 还是 www.lubai.com/main/b 都会指向index.html
如要访问 www.lubai.com/ 根路由
配置 nginx
location / {
try_files $uri $uri/ /home/dist/index.html
}
- index.html 存在于远程地址,oss/cdn 如 nginx 配置在a 服务器,index.html 被传到了cdn 上
cdn: www.lubai-cdn.com/file/index.…
location /main/ {
rewrite ^ /file/index.html break; // 重写访问到的文件全部写成/file/index.html
proxy_pass https://www.lubai-cdn.com; //代理访问
}
mock hash router
实现不同路由不同颜色
<div class="container">
<a href="#gray">灰色</a>
<a href="#green">绿色</a>
<a href="#">白色</a>
<button onclick="window.history.go(-1)">返回</button>
</div>
注册路由:实例.route('/', function() {})
构造函数用:
-
来存储path及callback的对应关系
-
页面加载完毕后首次会触发路由!! => window.addEventListener('load', this.refresh) 渲染当前路径
-
hash 路由需要用hashchange 监听改变,调用刷新
-
this 指向改变
class BaseRouter {
constructor() {
this.routes = {}; // 存储path及callback的对应关系
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh);
window.addEventListener('hashchange', this.refresh);
}
route(path, callback) {
this.routes[path] = callback || function () {}
}
// 渲染当前路径对应的操作
refresh() {
}
}
route 方法:存储 path 和 callback
refresh 方法
- 拿到当前路径的hash
const path = `/${location.hash.slice(1) || ''}` // slice(1) 去掉 #
- 执行path
this.routes[path]()
整体代码
class BaseRouter {
constructor() {
this.routes = {}; // 存储path以及callback的对应关系
this.refresh = this.refresh.bind(this);
// window.addEventListener('load', this.refresh); // 处理页面首次加载
window.addEventListener('hashchange', this.refresh); // 处理页面hash的变化
}
/**
* route
* @param {string} path 路由路径
* @param {function} callback 回调函数
*/
route(path, callback) {
// 向this.routes存储path以及callback的对应关系
this.routes[path] = callback || function () {};
}
refresh() {
// 刷新页面
const path = `/${location.hash.slice(1) || ''}`;
console.log(location.hash);
this.routes[path]();
}
}
// 期望看到的是,点击三个不同的a标签,页面的背景色会随之改变
const body = document.querySelector('body');
function changeBgColor(color) {
body.style.backgroundColor = color;
}
const Router = new BaseRouter();
Router.route('/', function () {
changeBgColor('white');
});
Router.route('/green', function () {
changeBgColor('green');
});
Router.route('/gray', function () {
changeBgColor('gray');
});
注意事项:
- 如果一个路径的改变会有多个callback,那么存储的时候:
this.routes = {
key: array
}
- 监听load事件后refresh,在green 路由中刷新后,还是白色,与路由不符
mock history router
实现不同路由不同颜色
<div class="container">
<a href="/gray">灰色</a>
<a href="/green">绿色</a>
<a href="/">白色</a>
<button onclick="window.history.go(-1)">返回</button>
</div>
history 路由点击/grey 会直接无法访问 index.html 因为 点击 a标签 相当于 执行location.href = '/grey'
因此要在容器父元素.container 中阻止location.href 默认行为,而执行我们自己的router行为
container.addEventListener('click', e => {
if (e.target.tagName === 'A') {
// 阻止默认 location.href 行为
e.preventDefault();
// 执行我们的go 方法
Router.go(e.target.getAttribute('href')); //Router.go('/green')
}
})
构造函数:
route方法:
class BaseRouter {
constructor() {
this.routes = {};
}
route(path, callback) {
this.routes[path] = callback || function () {};
}
不同:监听popState事件
bindPopStat() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path];
})
}
go 方法:
go(path) {
window.history.pushState({
path
}, null, path);
// pushState 时候不会触发popState,因此需要手动执行
const cb = this.routes[path];
if (cb) {
cb();
}
}
同样需要考虑初始情况
constructor() {
this.routes = {};
this.init(location.pathname);
this.bindPopState();
}
init(path) {
window.history.replaceState({
path
}, null, path);
// 执行一下回调
const cb = this.routes[path];
if (cb) {
cb();
}
}
封装后整体代码
class BaseRouter {
constructor() {
this.routes = {};
this.init(location.pathname);
this.bindPopState();
}
init(path) {
window.history.replaceState({
path
}, null, path);
// 执行一下回调
// const cb = this.routes[path];
// if (cb) {
// cb();
// }
this.invokePathCallBack(path);
}
route(path, callback) {
this.routes[path] = callback || function () {};
}
go(path) {
window.history.pushState({
path
}, null, path);
// const cb = this.routes[path];
// if (cb) {
// cb();
// }
// 封装
this.invokePathCallBack(path);
}
invokePathCallBack(path) {
const cb = this.routes[path];
if (cb) {
cb();
}
}
// 初始化后及监听popState事件
bindPopState() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
// this.routes[path] && this.routes[path];
this.invokePathCallBack(path);
})
}
}
const body = document.querySelector('body');
const container = document.querySelector('.container');
function changeBgColor(color) {
body.style.backgroundColor = color;
}
const Router = new BaseRouter();
Router.route('/', function () {
changeBgColor('white');
});
Router.route('/green', function () {
changeBgColor('green');
});
Router.route('/gray', function () {
changeBgColor('gray');
});
container.addEventListener('click', e => {
if (e.target.tagName === 'A') {
// 阻止默认 location.href 行为
e.preventDefault();
// 执行我们的go 方法
Router.go(e.target.getAttribute('href')); //Router.go('/green')
}
})
vue-router
main.js 中引入一个 router 并挂载在实例上
// main.js
import router from "./router/scroll";
// 把整个vue实例挂在到#app元素上
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
//index.js
// 通过use方法,引入vueRouter插件,进而使用vueRouter
Vue.use(VueRouter);
路由配置文件, 可以配置多个
// 路由匹配的顺序是由上而下
const routes: Array<RouteConfig> = [
{
path: "/", // 路径
name: "Home", // 名称
component: Home // 路径对应的组件/页面
},
{
path: "/about",
name: "About",
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue")
},
{
path: "*", // 这个路由可以匹配所有的路径 /list
name: "ErrorPage",
component: () =>
import(/* webpackChunkName: "errorPage" */ "../views/ErrorPage.vue")
}
];
Home 路径直接import 导入,但是about路径,component 为一个函数,指向导入一个views/About.vue 组件
在访问home组件时,只有app.js 以及chunk-vendors.js 用来存资源包,并没要引入 about 路由的文件
当访问 /about 路由时,会有 about.js
因此,初次加载只会通过 import 方式引入主页组件,当访问/about 组件时,通过component: () => import("../views/ErrorPage.vue") 异步引入相关资源文件,=> 大大缩短了首屏时间
path: "*", 通配符兜底所有没有的路由,返回404
路由守卫执行问题: 新建 routeguard.ts,引入
- 导航守卫执行前 router.beforeEach 三个参数 (去哪,来自于,下一个)
// 全局的导航守卫
router.beforeEach((to, from, next) => {
console.log(`Router.beforeEach => from=${from.path}, to=${to.path}`);
document.title = to.meta.title || "默认标题";
// 执行下一个路由导航
next();
});
这里的 to.meta.title 可以在路由的 item 里定义 ``js { path: "/", name: "Home", component: Home, meta: { title: "首页" } }, { path: "/about", name: "About", component: AboutComponent, meta: { title: "关于" } },
2. 导航守卫执行后
```js
router.afterEach((to, from) => {
console.log(`Router.afterEach => from=${from.path}, to=${to.path}`);
});
单个路由的导航守卫,可以放在路由的 item 里
{
path: "/test",
name: "Test",
component: TestComponent,
meta: {
title: "测试导航守卫"
},
children: [
{
path: "",
component: TestComponent
},
{
path: ":id",
component: TestComponent
}
],
beforeEnter: (to, from, next) => {
// 配置数组里针对单个路由的导航守卫
console.log(
`TestComponent route config beforeEnter => from=${from.path}, to=${to.path}`
);
next();
}
}
单个路由的导航守卫,可以放在组件里:使用@Component 装饰器
// Test.vue
@Component({
components: {},
beforeRouteEnter: (to, from, next) => {
// 这里无法通过this来获取页面各种属性和方法
console.log(
`TestComponent beforeRouteEnter => from=${from.path}, to=${to.path}`
);
next();
},
beforeRouteUpdate: (to, from, next) => {
// /test/1 ->? /test/2
// 此行为会触发beforeRouteUpdate
console.log(
`TestComponent beforeRouteUpdate => from=${from.path}, to=${to.path}`
);
next();
},
beforeRouteLeave: (to, from, next) => {
console.log(
`TestComponent beforeRouteLeave => from=${from.path}, to=${to.path}`
);
next();
}
})
导航守卫执行顺序
- 【组件】前一个组件的 beforeRouteLeave
- 【全局】的 router.beforeEach
- 【组件】如果有路由参数变化, /test => /test/1,触发 beforeRouteUpdate
- 【配置文件】里, 下一个的 beforeEnter
- 【组件】内部声明的 beforeRouteEnter
- 【全局】的 router.afterEach
一些参数获取
如在 item 中获取 id
{
// router/routeguard.js
path: "/dynamic",
name: "Dynamic",
component: DynamicComponent,
meta: {
title: "动态传参"
},
children: [
{
path: "",
component: DynamicComponent
},
{
path: ":id",
component: DynamicComponent
},
{
path: ":id/:name",
component: DynamicComponent
}
]
},
在组件中获取
export default class Test extends Vue {
get id() {
return this.$route.params.id;
}
}
一般监听路由变化在组件中监听
// App.vue
export default class Index extends Vue {
@Watch("$route")
handler(to: Route, from: Route) {
console.log(from.path);
console.log(to.path);
}
}
路由动画效果
使用 包裹
<transition name="fade">
<router-view class="child-view"></router-view>
</transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.75s ease;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
.child-view {
position: absolute;
transition: all 0.75s cubic-bezier(0.55, 0, 0.1, 1);
}
scrollBehavior 滚动条
// List.vue
<div class="list">
<h1>This is an list page</h1>
<div v-for="item in list" :key="item" class="item">
<router-link :to="item">{{ item }}</router-link>
</div>
</div>
// Detail.vue
<div>
<h1>{{ id }}</h1>
<router-link to="/list">返回列表</router-link>
</div>
// List.vue
export default class List extends Vue {
get list() {
return new Array(100).fill("").map((item, index) => `/detail/${index}`);
}
}
// Detail.vue
export default class Detail extends Vue {
get id() {
return this.$route.params.id;
}
}
Router 注册
// scroll.js
{
path: "/detail/:id",
name: "Detail",
component: DetailComponent
},
{
path: "/list",
name: "List",
component: ListComponent
},
需求:用户访问某详情页,之后回到主页滚动位置
VueRouter 对象中有个属性:scrollBehavior 可以实现
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
scrollBehavior: (to, from, savedPosition) => {
console.log(savedPosition); // 已报错的位置信息
return savedPosition;
}
});
返回列表页的问题:
- 手动点击浏览器返回或者前进按钮 记住滚动条的位置,基于history,go,back,forward
- router-link,并没有记住滚动条的位置