本文源自掘金 https://juejin.im/user/5b52fd...
通过用户请求的url导航到具体的html页面;每跳转到不同的URL,都是重新访问服务端,然后服务端返回页面,页面也可以是服务端获取数据,然后和模板组合,返回HTML,也可以是直接返回模板HTML,然后由前端js再去请求数据,使用前端模板和数据进行组合,生成想要的HTML
只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次 页面跳转:使用js中的append/remove或者show/hide的方式来进行页面内容的更换; 数据传递:可通过全局变量或者参数传递,进行相关数据交互
使用场景: 适用于高度追求高度支持搜索引擎的应用
使用场景: 高要求的体验度,追求界面流畅的应用
这里的 hash 就是指 url 后的 # 号以及后面的字符。比如说 "www.baidu.com/#hashhash" ,其中 "#hashhash" 就是我们期望的 hash 值。
由于 hash 值的变化不会导致浏览器像服务器发送请求,而且 hash 的改变会触发 hashchange 事件,浏览器的前进后退也能对其进行控制,所以在 H5 的 history 模式出现之前,基本都是使用 hash 模式来实现前端路由。
// 监听hash变化,点击浏览器的前进后退会触发 window.addEventListener('hashchange', function(event){ let newURL = event.newURL; // hash 改变后的新 url let oldURL = event.oldURL; // hash 改变前的旧 url },false)
下面实现一个路由对象
class HashRouter{ constructor(){ //用于存储不同hash值对应的回调函数 this.routers = {}; window.addEventListener('hashchange',this.load.bind(this),false) } //用于注册每个视图 register(hash,callback = function(){}){ this.routers[hash] = callback; } //用于注册首页 registerIndex(callback = function(){}){ this.routers['index'] = callback; } //用于处理视图未找到的情况 registerNotFound(callback = function(){}){ this.routers['404'] = callback; } //用于处理异常情况 registerError(callback = function(){}){ this.routers['error'] = callback; } //用于调用不同视图的回调函数 load(){ let hash = location.hash.slice(1), handler; //没有hash 默认为首页 if(!hash){ handler = this.routers.index; } //未找到对应hash值 else if(!this.routers.hasOwnProperty(hash)){ handler = this.routers['404'] || function(){}; } else{ handler = this.routers[hash] } //执行注册的回调函数 try{ handler.apply(this); }catch(e){ console.error(e); (this.routers['error'] || function(){}).call(this,e); } } }
再来一个例子
<body> <div id="nav"> <a href="#/page1">page1</a> <a href="#/page2">page2</a> <a href="#/page3">page3</a> <a href="#/page4">page4</a> <a href="#/page5">page5</a> </div> <div id="container"></div> </body>
let router = new HashRouter(); let container = document.getElementById('container'); //注册首页回调函数 router.registerIndex(()=> container.innerHTML = '我是首页'); //注册其他视图回到函数 router.register('/page1',()=> container.innerHTML = '我是page1'); router.register('/page2',()=> container.innerHTML = '我是page2'); router.register('/page3',()=> container.innerHTML = '我是page3'); router.register('/page4',()=> {throw new Error('抛出一个异常')}); //加载视图 router.load(); //注册未找到对应hash值时的回调 router.registerNotFound(()=>container.innerHTML = '页面未找到'); //注册出现异常时的回调 router.registerError((e)=>container.innerHTML = '页面异常,错误消息:<br>' + e.message);
在 HTML5 之前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面的跳转:
history.go(-1); // 后退一页 history.go(2); // 前进两页 history.forward(); // 前进一页 history.back(); // 后退一页
在 HTML5 的规范中,history 新增了以下几个 API
history.pushState(); // 添加新的状态到历史状态栈 history.replaceState(); // 用新的状态代替当前状态 history.state // 返回当前状态对象
由于 history.pushState() 和 history.replaceState() 可以改变 url 同时,不会刷新页面,所以在 HTML5 中的 histroy 具备了实现前端路由的能力。
对于单页应用的 history 模式而言,url 的改变只能由下面四种方式引起:
下面实现一个路由对象
class HistoryRouter{ constructor(){ //用于存储不同path值对应的回调函数 this.routers = {}; this.listenPopState(); this.listenLink(); } //监听popstate listenPopState(){ window.addEventListener('popstate',(e)=>{ let state = e.state || {}, path = state.path || ''; this.dealPathHandler(path) },false) } //全局监听A链接 listenLink(){ window.addEventListener('click',(e)=>{ let dom = e.target; if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){ e.preventDefault() this.assign(dom.getAttribute('href')); } },false) } //用于首次进入页面时调用 load(){ let path = location.pathname; this.dealPathHandler(path) } //用于注册每个视图 register(path,callback = function(){}){ this.routers[path] = callback; } //用于注册首页 registerIndex(callback = function(){}){ this.routers['/'] = callback; } //用于处理视图未找到的情况 registerNotFound(callback = function(){}){ this.routers['404'] = callback; } //用于处理异常情况 registerError(callback = function(){}){ this.routers['error'] = callback; } //跳转到path assign(path){ history.pushState({path},null,path); this.dealPathHandler(path) } //替换为path replace(path){ history.replaceState({path},null,path); this.dealPathHandler(path) } //通用处理 path 调用回调函数 dealPathHandler(path){ let handler; //没有对应path if(!this.routers.hasOwnProperty(path)){ handler = this.routers['404'] || function(){}; } //有对应path else{ handler = this.routers[path]; } try{ handler.call(this) }catch(e){ console.error(e); (this.routers['error'] || function(){}).call(this,e); } } }
再来一个例子
<body> <div id="nav"> <a href="/page1">page1</a> <a href="/page2">page2</a> <a href="/page3">page3</a> <a href="/page4">page4</a> <a href="/page5">page5</a> <button id="btn">page2</button> </div> <div id="container"> </div> </body>
let router = new HistoryRouter(); let container = document.getElementById('container'); //注册首页回调函数 router.registerIndex(() => container.innerHTML = '我是首页'); //注册其他视图回到函数 router.register('/page1', () => container.innerHTML = '我是page1'); router.register('/page2', () => container.innerHTML = '我是page2'); router.register('/page3', () => container.innerHTML = '我是page3'); router.register('/page4', () => { throw new Error('抛出一个异常') }); document.getElementById('btn').onclick = () => router.assign('/page2') //注册未找到对应path值时的回调 router.registerNotFound(() => container.innerHTML = '页面未找到'); //注册出现异常时的回调 router.registerError((e) => container.innerHTML = '页面异常,错误消息:<br>' + e.message); //加载页面 router.load();
从零开始构建一个webpack项目
总结几个webpack打包优化的方法
总结前端性能优化的方法
几种常见的JS递归算法
搭建一个vue-cli的移动端H5开发模板
封装一个toast和dialog组件并发布到npm
一文读尽前端路由、后端路由、单页面应用、多页面应用
关于几个移动端软键盘的坑及其解决方案
浅谈JavaScript的防抖与节流