[实践系列]-前端路由

7次阅读

共计 5759 个字符,预计需要花费 15 分钟才能阅读完成。

什么是路由?
路由这概念最开始是在后端出现的, 在以前前后端不分离的时候, 由后端来控制路由, 服务器接收客户端的请求, 解析对应的 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 哈~

正文完
 0