1. 什么是路由
路由是根据不同的 url 地址展示不同的内容或页面
早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。
后来页面越来越复杂,服务器压力越来越大,随着 ajax(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也可以控制 url 自行管理,前端路由由此而生。
单页面应用的实现,就是因为有了前端路由这个概念。
2. 前端路由的两种实现原理
1 Hash路由
我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,路由里的 # 不叫锚点,我们称之为 hash,大型框架的路由系统大多都是哈希实现的。
我们需要一个根据监听哈希变化触发的事件 —— hashchange 事件
window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。
我们用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。
window.addEventListener('hashchange', function () {
<!--这里你可以写你需要的代码-->
});
复制代码
2 History 路由
HTML5的History API 为浏览器的全局history对象增加的扩展方法。
重点说其中的两个新增的API history.pushState 和 history.replaceState
这两个 API 都接收三个参数,分别是
状态对象(state object) — 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。
标题(title) — FireFox浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。
地址(URL) — 新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。
我们在控制台输入
window.history.pushState(null, null, "https://www.baidu.com/?name=lvpangpang");
可以看到浏览器url的变化
注意:这里的 url 不支持跨域,比如你在不是百度域名下输入上面的代码。
不过这种模式之前在vue或者react里面选择了这种模式,发现一刷新页面就会到月球。
原因是因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。说的通俗一点就是这种模式会被服务器识别,会做出相应的处理。
对于这种404的问题,我们有很多解决方式。
A 配置webpack(开发环境)
historyApiFallback:{
index:'/index.html'//index.html为当前目录创建的template.html
}
复制代码
B 配置ngnix(生产环境)
location /{
root /data/nginx/html;
index index.html index.htm;
error_page 404 /index.html;
}
复制代码
3. 路由demo
接下来会一步一步来讲解怎么样写一个前端路由。
也就是把我们的知识转为技能的过程。
上面我们也看到了路由是根据不同的 url 地址展示不同的内容或页面。对于前端路由来说就是根据不同的url地址展示不同的内容。
于是有了下面这版代码。
<!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>Document</title>
</head>
<body>
<div id="root">
<a href="#/index">首页</a>
<a href="#/list">列表</a>
</div>
<script>
const root = document.querySelector('#root');
window.onhashchange = function (e) {
var hash = window.location.hash.substr(1);
if(hash === '/index') {
root.innerHTML = '这是index组件';
}
if (hash === '/list') {
root.innerHTML = '这是list组件';
}
}
</script>
</body>
</html>
复制代码
上面只能说是一个小demo,为了让我们能最直观地感受到前端路由。这次为了能有更好的效果,特意引入了gif。
4. 路由js版
看好了demo,是不是迫不及待想实现一个路由了,那就让我们一起来一步一步实现它吧。这里给他取个名-炼狱,主要是方便下文的指代。
4.1 炼狱的参数配置
这里我是仿造vue,react里面的路由配置的,默认是一个路由对象数组。
//路由配置
const routes = [{
path: '/index',
url: 'js/index.js'
}, {
path: '/list',
url: 'js/list.js'
}, {
path: '/detail',
url: 'js/detail.js'
}];
var router = new Router(routes);
复制代码
可以看到上面的路由配置是不是和vue以及react很像呢。只不过这里的url指向的是js文件而不是组件(其实组件也是js文件,一个组件包含html, css, js ,最终都会被编译到一个js文件)
4.1 炼狱的整体框架
function Router(opts = []) {
}
Router.prototype = {
init: function () {
},
// 路由注册
initRouter: function () {
},
// 解析url获取路径以及对应参数数组化
getParamsUrl: function () {
},
// 路由处理
urlChange: function () {
},
// 渲染视图(执行匹配到的js代码)
render: function (currentHash) {
},
// 单个路由注册
map: function (item) {
},
// 切换前
beforeEach: function (callback) {
},
// 切换后
afterEach: function (callback) {
},
// 路由异步懒加载js文件
asyncFun: function (file, transition) {
}
}
复制代码
4.1 炼狱的内部解刨
上面已经列出来炼狱的整体代码框架,下面我们就来对每一个函数进行编写。
A init函数
这是炼狱插件在被调用的时候就会执行的方式,当然是用来注册路由以及绑定对应的路由切换事件的。
init() {
var oThis = this;
// 注册路由
this.initRouter();
// 页面加载匹配路由
window.addEventListener('load', function () {
oThis.urlChange();
});
// 路由切换
window.addEventListener('hashchange', function () {
oThis.urlChange();
});
}
}
复制代码
B initRouter函数+map函数
注册路由,作用就是将路由对象数组参数在初始化的时候就做好路由匹配,比如/index路由对应/js/index.js。
// 路由注册
initRouter: function() {
var opts = this.opts;
opts.forEach((item, index) => {
this.map(item);
});
}
// 单个路由注册
map: function (item) {
path = item.path.replace(/\s*/g, '');// 过滤空格
this.routers[path] = {
callback: (transition) => {
return this.asyncFun(item.url, transition);
}, // 回调
fn: null // 缓存对应的js文件
}
}
复制代码
this.routers用来存储路由对象,执行每一个路由的callback函数就是加载对应的js文件。
每一个router对象里面的fn函数的作用是已经加载过的js文件,可以做到加载一次多次使用,在路由切换的时候。
C asyncFun函数
这个函数的作用是异步加载目标js文件。原理就是利用手动生成javascript标签动态插入页面。当然在加载真实js文件前需要做一个判断,目标js是否已经加载过。
// 路由异步懒加载js文件
asyncFun: function (file, transition) {
// console.log(transition);
var oThis = this,
routers = this.routers;
// 判断是否走缓存
if (routers[transition.path].fn) {
oThis.afterFun && oThis.afterFun(transition)
routers[transition.path].fn(transition)
} else {
var _body = document.getElementsByTagName('body')[0];
var scriptEle = document.createElement('script');
scriptEle.type = 'text/javascript';
scriptEle.src = file;
scriptEle.async = true;
SPA_RESOLVE_INIT = null;
scriptEle.onload = function () {
oThis.afterFun && oThis.afterFun(transition)
routers[transition.path].fn = SPA_RESOLVE_INIT;
routers[transition.path].fn(transition)
}
_body.appendChild(scriptEle);
}
}
复制代码
D render函数
看名字都知道这个函数的主要作用就是渲染页面,在这里也就是执行加载路由对应的js文件。这里做了一个判断,如果存在路由守护的话则走路由守护。
// 渲染视图(执行匹配到的js代码)
render: function (currentHash) {
var oThis = this;
// 全局路由守护
if (oThis.beforeFun) {
oThis.beforeFun({
to: {
path: currentHash.path,
query: currentHash.query
},
next: function () {
// 执行目标路由对应的js代码(相当于是组件渲染)
oThis.routers[currentHash.path].callback.call(oThis, currentHash)
}
});
} else {
oThis.routers[currentHash.path].callback.call(oThis, currentHash);
}
}
复制代码
E beforeEach函数
路由守护函数,在这里可以做一些比如登录权限判断的事情,这一点是不是和vue-router的全局路由守护很像呢。
// 切换前
beforeEach: function (callback) {
if (Object.prototype.toString.call(callback) === '[object Function]') {
this.beforeFun = callback;
} else {
console.trace('请传入函数类型的参数');
}
},
复制代码
好了,上面写好了炼狱的主要代码,下面我们就可以看到对应的效果了。