什么是路由?
路由这概念最开始是在后端出现的,在以前前后端不分离的时候,由后端来控制路由,服务器接收客户端的请求,解析对应的url路径,并返回对应的页面/资源。
简单的说 路由就是根据不同的url地址来展示不同的内容或页面.
前端路由的来源
在很久很久以前~ 用户的每次更新操作都需要重新刷新页面,非常的影响交互体验,后来,为了解决这个问题,便有了Ajax(异步加载方案),Ajax给体验带来了极大的提升。
虽然Ajax解决了用户交互时体验的痛点,但是多页面之间的跳转一样会有不好的体验,所以便有了spa(single-page application)使用的诞生。而spa应用便是基于前端路由实现的,所以便有了前端路由。
如今比较火的vue-router/react-router 也是基于前端路由的原理实现的~
前端路由的两种实现原理
1.Hash模式
window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。
window.onhashchange = function(){
// hash 值改变
// do you want
}
2.History 模式
HTML5的History API 为浏览器的全局history对象增加的扩展方法。
简单来说,history其实就是浏览器历史栈的一个接口。这里不细说history的每个API啦。具体可查阅 传送门
window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变,便会触发该事件。
需要特别注意的是,调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件。
window.onpopstate = function(){
// 历史栈 信息改变
// do you want
}
history提供了两个操作历史栈的API:history.pushState 和 history.replaceState
history.pushState(data[,title][,url]);//向历史记录中追加一条记录
history.replaceState(data[,title][,url]);//替换当前页在历史记录中的信息。
// data: 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。
//title: FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。
//url: 新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。
两种模式优劣对比
对比 | Hash | History |
---|---|---|
观赏性 | 丑 | 美 |
兼容性 | >ie8 | >ie10 |
实用性 | 直接使用 | 需后端配合 |
命名空间 | 同一document | 同源 |
造(cao) 一个简单的前端路由
本demo只是想说帮助我们通过实践更进一步的理解前端路由这个概念,所以只做了简单的实现~
history模式404
当我们使用history模式时,如果没有进行配置,刷新页面会出现404。
原因是因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。
这个问题的解决方案这里就不细说了,google一下,你就知道~ 我们在以下demo使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。
文件结构
|-- package.json
|-- webpack.config.js
|-- index.html
|-- src
|-- index.js
|-- routeList.js
|-- base.js
|-- hash.js
|-- history.js
1.搭建环境
废话不多说,直接上代码~
package.json
{
"name": "web_router",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config ./webpack.config.js"
},
"author": "webfansplz",
"license": "MIT",
"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: '6789',
compress: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
})
]
};
2.开撸
首先我们先初始化定义我们需要实现的功能及配置参数。
前端路由 | 参数 | 方法 |
---|---|---|
x | 模式(mode) | push(压入) |
x | 路由列表(routeList) | replace(替换) |
x | x | go(前进/后退) |
src/index.js
const MODE='';
const ROUTELIST=[];
class WebRouter {
constructor() {
}
push(path) {
...
}
replace(path) {
...
}
go(num) {
...
}
}
new WebRouter({
mode: MODE,
routeList: ROUTELIST
});
前面我们说了前端路由有两种实现方式。
1.定义路由列表
2.我们分别为这两种方式创建对应的类,并根据不同的mode参数进行实例化,完成webRouter类的实现。
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/hash.js
export class HashRouter{
}
src/history.js
export class HistoryRouter{
}
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
});
前面我们已经实现了webRouter的功能,接下来我们来实现两种方式。
因为两种模式都需要调用一个方法来实现不同路由内容的刷新,so~
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>
js/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;
}
}
ok,下面我们来实现两种模式。
Hash模式
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);
}
}
History模式
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);
}
}
3.小功告成
就这样,一个简单的前端路由就完成拉。
如果觉得有帮助到你的话,给个star哈~