路由是根据不一样的 url 地址展现不一样的内容或页面javascript
早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。css
后来页面愈来愈复杂,服务器压力愈来愈大,随着 ajax(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也能够控制 url 自行管理,前端路由由此而生。html
单页面应用的实现,就是由于有了前端路由这个概念。前端
咱们常常在 url 中看到 #,这个 # 有两种状况,一个是咱们所谓的锚点,好比典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,路由里的 # 不叫锚点,咱们称之为 hash,大型框架的路由系统大多都是哈希实现的。vue
咱们须要一个根据监听哈希变化触发的事件 —— hashchange 事件java
window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。react
咱们用 window.location 处理哈希的改变时不会从新渲染页面,而是看成新页面加到历史记录中,这样咱们跳转页面就能够在 hashchange 事件中注册 ajax 从而改变页面内容。webpack
window.addEventListener('hashchange', function () {
<!--这里你能够写你须要的代码-->
});
复制代码
HTML5的History API 为浏览器的全局history对象增长的扩展方法。nginx
重点说其中的两个新增的API history.pushState 和 history.replaceStateweb
这两个 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;
}
复制代码
接下来会一步一步来说解怎么样写一个前端路由。
也就是把咱们的知识转为技能的过程。
上面咱们也看到了路由是根据不一样的 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。
看好了demo,是否是火烧眉毛想实现一个路由了,那就让咱们一块儿来一步一步实现它吧。这里给他取个名-炼狱,主要是方便下文的指代。
这里我是仿造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文件)
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) {
}
}
复制代码
上面已经列出来炼狱的总体代码框架,下面咱们就来对每个函数进行编写。
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('请传入函数类型的参数');
}
},
复制代码
好了,上面写好了炼狱的主要代码,下面咱们就能够看到对应的效果了。