属性 | 描述 |
---|---|
hash | 设置或返回从井号 (#) 开始的 URL(锚)。 |
host | 设置或返回主机名和当前 URL 的端口号。 |
hostname | 设置或返回当前 URL 的主机名。 |
protocol | 设置或返回当前 URL 的协议。 |
href | 设置或返回完整的 URL。 |
pathname | 设置或返回当前 URL 的路径部分。 |
port | 设置或返回当前 URL 的端口号。 |
search | 设置或返回从问号 (?) 开始的 URL(查询部分)。 |
方法 | 描述 |
---|---|
assign() | 加载新的文档。 |
reload() | 重新加载当前文档。 |
replace() | 用新的文档替换当前文档。 |
hashchange() | 监听hashchange变化。 |
上面的属性和方法中除了hash
,其他都会重新加载页面。
属性 | 描述 |
---|---|
length | 历史记录数组的长度 |
state | 表示当前的处在哪个记录上 |
方法 | 描述 |
---|---|
back() | 等效于用户点击回退按钮 |
forward() | 等效于用户点击前进按钮 |
go(num) | 通过指定一个相对于当前页面位置的数值加载页面 |
pushState(json, "", url) | 添加一条记录 |
replaceState(json, "", url) | 替换掉一条记录 |
其中pushState
方法和replaceState
方法可以分别增加和替换掉一条记录(必须同源),而不会重新加载页面。
window.location.hash和window.history.pushState(或replaceState)唯一的不同是通过hash改变url带入#,而后者不会。而Vue 路由的两种模式就是基于location和history这2个对象的。
观赏性 | 兼容性 | 实用性 | 命名空间 | |
---|---|---|---|---|
Hash | 丑 | >ie8 | 直接使用 | 同一document |
History | 美 | >ie10 | 需后端配合 | 同源 |
当我们使用history模式时,如果没有进行配置,刷新页面会出现404。
1. 原因: 因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。
2. 解决方案: 使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。
1. HashHistory.push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
transitionTo()
方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()
方法最主要的是对window
的hash
进行了直接赋值:
window.location.hash=route.fullPath
hash
的改变会自动添加到浏览器的访问历史记录中。
那么视图的更新是怎么实现的呢,我们来看看父类History
中的transitionTo()
方法:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
...
})
}
updateRoute (route: Route) {
this.cb && this.cb(route)
}
listen (cb: Function) {
this.cb = cb
}
可以看到,当路由变化时,调用了Hitory
中的this.cb
方法,而this.cb
方法是通过History.listen(cb)
进行设置的,回到VueRouter
类定义中,找到了在init()
中对其进行了设置:
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
app
为Vue
组件实例,但是Vue
作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route
,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter
的install()
方法中混入Vue
对象的,install.js
的源码:
export function install (Vue) {
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
registerInstance(this, this)
},
})
}
通过Vue.mixin()
方法,全局注册一个混合,影响注册之后所有创建的每个Vue
实例,该混合在beforeCreate
钩子中通过Vue.util.defineReactive()
定义了响应式的_route
属性。所谓响应式属性,即当_route
值改变时,会自动调用Vue
实例的render()
方法,更新视图。
$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()
2. HashHistory.replace()
replace()
方法与push()
方法不同之处在于,它并不是将新路由添加到浏览器访问历史栈顶,而是替换掉当前的路由:
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
可以看出,它与push()
的实现结构基本相似,不同点它不是直接对window.location.hash
进行赋值,而是调用window.location.replace
方法将路由进行替换。
上面的VueRouter.push()
和VueRouter.replace()
是可以在vue
组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory
中这一功能通过setupListeners
监听hashchange
实现:
setupListeners () {
window.addEventListener('hashchange', () => {
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
})
}
该方法设置监听了浏览器事件hashchange
,调用的函数为replaceHash
,即在浏览器地址栏中直接输入路由相当于代码调用了replace()
方法。
1. HTML5History.push()
window.history.pushState(stateObject,title,url)
stateObject
:当浏览器跳转到新的状态时,将触发popState
事件,该事件将携带这个stateObject
参数的副本title
:所添加记录的标题url
:所添加记录的url
2. HTML5History.replace()
window.history,replaceState(stateObject,title,url)
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
3. 监听地址栏
在HTML5History
中添加对修改浏览器地址栏URL
的监听popstate
是直接在构造函数中执行的:
constructor (router: Router, base: ?string) {
window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
handleScroll(router, route, current, true)
}
})
})
}
1. 环境搭建
/*
* package.json
*/
{
"name": "web_router",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config ./webpack.config.js"
},
"author": "",
"license": "",
"devDependencies": {
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.28.1",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
}
}
/*
* webpack.config.js
*/
'use strict';
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].js'
},
devServer: {
clientLogLevel: 'warning',
hot: true,
inline: true,
open: true,
//在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html (解决histroy mode 404)
historyApiFallback: true,
host: 'localhost',
port: '8089',
compress: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
})
]
};
2. 实现代码
/*
* src/index.js
*/
import { HashRouter } from './hash';
import { HistoryRouter } from './history';
import { ROUTELIST } from './routeList';
//路由模式
const MODE = 'hash';
class WebRouter {
constructor({ mode = 'hash', routeList }) {
this.router = mode === 'hash' ? new HashRouter(routeList) : new HistoryRouter(routeList);
}
push(path) {
this.router.push(path);
}
replace(path) {
this.router.replace(path);
}
go(num) {
this.router.go(num);
}
}
const webRouter = new WebRouter({
mode: MODE,
routeList: ROUTELIST
});
/*
* src/routeList.js
*/
export const ROUTELIST = [
{
path: '/',
name: 'index',
component: 'This is index page'
},
{
path: '/hash',
name: 'hash',
component: 'This is hash page'
},
{
path: '/history',
name: 'history',
component: 'This is history page'
},
{
path: '*',
name: 'notFound',
component: '404 NOT FOUND'
}
];
/*
* src/index.html
*/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>前端路由</title>
</head>
<body>
<div id="page"></div>
</body>
</html>
/*
* src/base.js
*/
const ELEMENT = document.querySelector('#page');
export class BaseRouter {
//list = 路由列表
constructor(list) {
this.list = list;
}
render(state) {
//匹配当前的路由,匹配不到则使用404配置内容 并渲染~
let ele = this.list.find(ele => ele.path === state);
ele = ele ? ele : this.list.find(ele => ele.path === '*');
ELEMENT.innerText = ele.component;
}
}
/*
* src/hash.js
*/
import { BaseRouter } from './base.js';
export class HashRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
//监听hash变化事件,hash变化重新渲染
window.addEventListener('hashchange', e => {
this.handler();
});
}
//渲染
handler() {
this.render(this.getState());
}
//获取当前hash
getState() {
const hash = window.location.hash;
return hash ? hash.slice(1) : '/';
}
//获取完整url
getUrl(path) {
const href = window.location.href;
const i = href.indexOf('#');
const base = i >= 0 ? href.slice(0, i) : href;
return `${base}#${path}`;
}
//改变hash值 实现压入 功能
push(path) {
window.location.hash = path;
}
//使用location.replace实现替换 功能
replace(path) {
window.location.replace(this.getUrl(path));
}
//这里使用history模式的go方法进行模拟 前进/后退 功能
go(n) {
window.history.go(n);
}
}
/*
* src/history.js
*/
import { BaseRouter } from './base.js';
export class HistoryRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
//监听历史栈信息变化,变化时重新渲染
window.addEventListener('popstate', e => {
this.handler();
});
}
//渲染
handler() {
this.render(this.getState());
}
//获取路由路径
getState() {
const path = window.location.pathname;
return path ? path : '/';
}
//使用pushState方法实现压入功能
//PushState不会触发popstate事件,所以需要手动调用渲染函数
push(path) {
history.pushState(null, null, path);
this.handler();
}
//使用replaceState实现替换功能
//replaceState不会触发popstate事件,所以需要手动调用渲染函数
replace(path) {
history.replaceState(null, null, path);
this.handler();
}
go(n) {
window.history.go(n);
}
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。