在单页应用上,前端路由并不陌生。不少前端框架也会有独立开发或推荐配套使用的路由系统。那么,当咱们在谈前端路由的时候,还能够谈些什么?本文将简要分析并实现一个的前端路由,并对 react-router 进行分析。html
说一下前端路由实现的简要原理,以 hash 形式(也可使用 History API 来处理)为例,当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不一样的操做,进行不一样的内容的展现。直接看代码或许更直观。前端
function Router() { this.routes = {}; this.currentUrl = ''; } Router.prototype.route = function(path, callback) { this.routes[path] = callback || function(){}; }; Router.prototype.refresh = function() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl](); }; Router.prototype.init = function() { window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } window.Router = new Router(); window.Router.init();
上面路由系统 Router 对象实现,主要提供三个方法html5
Router 调用方式以及呈现效果以下:点击触发 url 的 hash 改变,并对应地更新内容(这里为 body 背景色)react
<ul> <li><a href="#/">turn white</a></li> <li><a href="#/blue">turn blue</a></li> <li><a href="#/green">turn green</a></li> </ul> var content = document.querySelector('body'); // change Page anything function changeBgColor(color) { content.style.backgroundColor = color; } Router.route('/', function() { changeBgColor('white'); }); Router.route('/blue', function() { changeBgColor('blue'); }); Router.route('/green', function() { changeBgColor('green'); });
以上为一个前端路由的简单实现,点击查看完整代码,虽然简单,但实际上不少路由系统的根基都立于此,其余路由系统主要是对自身使用的框架机制的进行配套及优化,如与 react 配套的 react-router。git
react-router 是基于 history 模块提供的 api 进行开发的,结合的形式本文记为 包装方式。因此在开始对其分析以前,先举一个简单的例子来讲明如何进行对象的包装。github
// 原对象 var historyModule = { listener: [], listen: function (listener) { this.listener.push(listener); console.log('historyModule listen..') }, updateLocation: function(){ this.listener.forEach(function(listener){ listener('new localtion'); }) } } // Router 将使用 historyModule 对象,并对其包装 var Router = { source: {}, init: function(source){ this.source = source; }, // 对 historyModule的listen进行了一层包装 listen: function(listener) { return this.source.listen(function(location){ console.log('Router listen tirgger.'); listener(location); }) } } // 将 historyModule 注入进 Router 中 Router.init(historyModule); // Router 注册监听 Router.listen(function(location){ console.log(location + '-> Router setState.'); }) // historyModule 触发回调 historyModule.updateLocation();
可看到 historyModule 中含有机制:historyModule.updateLocation() -> listener( ),Router 经过对其进行包装开发,针对 historyModule 的机制对 Router 也起到了做用,即historyModule.updateLocation() 将触发 Router.listen 中的回调函数 。点击查看完整代码 这种包装形式可以充分利用原对象(historyModule )的内部机制,减小开发成本,也更好的分离包装函数(Router)的逻辑,减小对原对象的影响。后端
react-router 以 react component 的组件方式提供 API, 包含 Router,Route,Redirect,Link 等等,这样可以充分利用 react component 提供的生命周期特性,同时也让定义路由跟写 react component 达到统一,以下api
render((
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
), document.body)
就这样,声明了一份含有 path to component 的各个映射的路由表。 react-router 还提供的 Link 组件(以下),做为提供更新 url 的途径,触发 Link 后最终将经过如上面定义的路由表进行匹配,并拿到对应的 component 及 state 进行 render 渲染页面。跨域
<Link to={`/user/89757`}>'joey'</Link>
这里不细讲 react-router 的使用,详情可见:https://github.com/reactjs/react-router
主要是由于触发了 react setState 的方法从而可以触发 render component。 从顶层组件 Router 出发(下面代码从 react-router/Router 中摘取),可看到 Router 在 react component 生命周期之组件被挂载前 componentWillMount 中使用 this.history.listen 去注册了 url 更新的回调函数。回调函数将在 url 更新时触发,回调中的 setState 起到 render 了新的 component 的做用。
Router.prototype.componentWillMount = function componentWillMount() { // .. 省略其余 var createHistory = this.props.history; this.history = _useRoutes2['default'](createHistory)({ routes: _RouteUtils.createRoutes(routes || children), parseQueryString: parseQueryString, stringifyQuery: stringifyQuery }); this._unlisten = this.history.listen(function (error, state) { _this.setState(state, _this.props.onUpdate); }); }; 上面的 _useRoutes2 对 history 操做即是对其作一层包装,因此调用的 this.history 实际为包装之后的对象,该对象含有 _useRoutes2 中的 listen 方法,以下 function listen(listener) { return history.listen(function (location) { // .. 省略其余 match(location, function (error, redirectLocation, nextState) { listener(null, nextState); }); }); }
可看到,上面代码中,主要分为两部分
以上,为起始注册的监听,及回调的做用。
这里还得从如何更新 url 提及。通常来讲,url 更新主要有两种方式:简单的 hash 更新或使用 history api 进行地址更新。在 react-router 中,其提供了 Link 组件,该组件能在 render 中使用,最终会表现为 a 标签,并将 Link 中的各个参数组合放它的 href 属性中。能够从 react-router/ Link 中看到,对该组件的点击事件进行了阻止了浏览器的默认跳转行为,而改用 history 模块的 pushState 方法去触发 url 更新。
Link.prototype.render = function render() { // .. 省略其余 props.onClick = function (e) { return _this.handleClick(e); }; if (history) { // .. 省略其余 props.href = history.createHref(to, query); } return _react2['default'].createElement('a', props); }; Link.prototype.handleClick = function handleClick(event) { // .. 省略其余 event.preventDefault(); this.context.history.pushState(this.props.state, this.props.to, this.props.query); };
对 history 模块的 pushState 方法对 url 的更新形式,一样分为两种,分别在 history/createBrowserHistory 及 history/createHashHistory 各自的 finishTransition 中,如 history/createBrowserHistory 中使用的是 window.history.replaceState(historyState, null, path); 而 history/createHashHistory 则使用 window.location.hash = url,调用哪一个是根据咱们一开始建立 history 的方式。
更新 url 的显示是一部分,另外一部分是根据 url 去更新展现,也就是触发前面的监听。这是在前面 finishTransition 更新 url 以后实现的,调用的是 history/createHistory 中的 updateLocation 方法,changeListeners 中为 history/createHistory 中的 listen 中所添加的,以下
function updateLocation(newLocation) { // 示意代码 location = newLocation; changeListeners.forEach(function (listener) { listener(location); }); } function listen(listener) { // 示意代码 changeListeners.push(listener); }
4. 总结
能够将以上 react-router 的整个包装闭环总结为
至于前进与后退的实现,是经过监听 popstate 以及 hashchange 的事件,当前进或后退 url 更新时,触发这两个事件的回调函数,回调的执行方式 Link 大体相同,最终一样更新了 UI ,这里就再也不说明。
react-router 主要是利用底层 history 模块的机制,经过结合 react 的架构机制作一层包装,实际自身的内容并很少,但其包装的思想笔者认为很值得学习,有兴趣的建议阅读下源码,相信会有其余收获。
早期的路由都是后端实现的,直接根据 url 来 reload 页面,页面变得愈来愈复杂服务器端压力变大,随着 ajax 的出现,页面实现非 reload 就能刷新数据,也给前端路由的出现奠基了基础。咱们能够经过记录 url 来记录 ajax 的变化,从而实现前端路由。
本文主要讲两种主流方式实现前端路由。
这里不细说每个 API 的用法,你们能够看 MDN 的文档:https://developer.mozilla.org...
重点说其中的两个新增的API history.pushState
和 history.replaceState
这两个 API 都接收三个参数,分别是
相同之处是两个 API 都会操做浏览器的历史记录,而不会引发页面的刷新。
不一样之处在于,pushState会增长一条新的历史记录,而replaceState则会替换当前的历史记录。
咱们拿大百度的控制台举例子(具体说是个人浏览器在百度首页打开控制台。。。)
咱们在控制台输入
window.history.pushState(null, null, "https://www.baidu.com/?name=orange");
好,咱们观察此时的 url 变成了这样
咱们这里不一一测试,直接给出其它用法,你们自行尝试
window.history.pushState(null, null, "https://www.baidu.com/name/orange"); //url: https://www.baidu.com/name/orange window.history.pushState(null, null, "?name=orange"); //url: https://www.baidu.com?name=orange window.history.pushState(null, null, "name=orange"); //url: https://www.baidu.com/name=orange window.history.pushState(null, null, "/name/orange"); //url: https://www.baidu.com/name/orange window.history.pushState(null, null, "name/orange"); //url: https://www.baidu.com/name/orange
注意:这里的 url 不支持跨域,当咱们把
www.baidu.com
换成baidu.com
时就会报错。
Uncaught DOMException: Failed to execute 'pushState' on 'History': A history state object with URL 'https://baidu.com/?name=orange' cannot be created in a document with origin 'https://www.baidu.com'and URL 'https://www.baidu.com/?name=orange'.
回到上面例子中,每次改变 url 页面并无刷新,一样根据上文所述,浏览器会产生历史记录
这就是实现页面无刷新状况下改变 url 的前提,下面咱们说下第一个参数 状态对象
若是运行 history.pushState()
方法,历史栈对应的纪录就会存入 状态对象,咱们能够随时主动调用历史条目
此处引用 mozilla 的例子
<!DOCTYPE HTML> <!-- this starts off as http://example.com/line?x=5 --> <title>Line Game - 5</title> <p>You are at coordinate <span id="coord">5</span> on the line.</p> <p> <a href="?x=6" onclick="go(1); return false;">Advance to 6</a> or <a href="?x=4" onclick="go(-1); return false;">retreat to 4</a>? </p> <script> var currentPage = 5; // prefilled by server!!!! function go(d) { setupPage(currentPage + d); history.pushState(currentPage, document.title, '?x=' + currentPage); } onpopstate = function(event) { setupPage(event.state); } function setupPage(page) { currentPage = page; document.title = 'Line Game - ' + currentPage; document.getElementById('coord').textContent = currentPage; document.links[0].href = '?x=' + (currentPage+1); document.links[0].textContent = 'Advance to ' + (currentPage+1); document.links[1].href = '?x=' + (currentPage-1); document.links[1].textContent = 'retreat to ' + (currentPage-1); } </script>
咱们点击 对应的 url 与模版都会 +1,反之点击 就会都 -1,这就知足了 url 与模版视图同时变化的需求Advance to ?retreat to ?
实际当中咱们不须要去模拟 onpopstate 事件,官方文档提供了 popstate 事件,当咱们在历史记录中切换时就会产生 popstate 事件。对于触发 popstate 事件的方式,各浏览器实现也有差别,咱们能够根据不一样浏览器作兼容处理。
咱们常常在 url 中看到 #,这个 # 有两种状况,一个是咱们所谓的锚点,好比典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,路由里的 # 不叫锚点,咱们称之为 hash,大型框架的路由系统大多都是哈希实现的。
一样咱们须要一个根据监听哈希变化触发的事件 —— hashchange 事件
咱们用 window.location
处理哈希的改变时不会从新渲染页面,而是看成新页面加到历史记录中,这样咱们跳转页面就能够在 hashchange 事件中注册 ajax 从而改变页面内容。
http://codepen.io/orangexc/pe...
hashchange 在低版本 IE 须要经过轮询监听 url 变化来实现,咱们能够模拟以下
(function(window) { // 若是浏览器不支持原生实现的事件,则开始模拟,不然退出。 if ( "onhashchange" in window.document.body ) { return; } var location = window.location, oldURL = location.href, oldHash = location.hash; // 每隔100ms检查hash是否发生变化 setInterval(function() { var newURL = location.href, newHash = location.hash; // hash发生变化且全局注册有onhashchange方法(这个名字是为了和模拟的事件名保持统一); if ( newHash != oldHash && typeof window.onhashchange === "function" ) { // 执行方法 window.onhashchange({ type: "hashchange", oldURL: oldURL, newURL: newURL }); oldURL = newURL; oldHash = newHash; } }, 100); })(window);
大型框架的路由固然不会这么简单,angular 1.x 的路由对哈希、模版、处理器进行关联,大体以下
app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { $routeProvider .when('/article', { templateUrl: '/article.html', controller: 'ArticleController' }).otherwise({ redirectTo: '/index' }); $locationProvider.html5Mode(true); }])
这套路由方案默认是以 # 开头的哈希方式,若是不考虑低版本浏览器,就能够直接调用 $locationProvider.html5Mode(true)
利用 H5 的方案而不用哈希方案。
两种方案我推荐 hash 方案,由于照顾到低级浏览器,就是不美观(多了一个 #),二者兼顾也不是不可,只能判断浏览器给出对应方案啦,不过也只支持 IE8+,更低版本兼容见上文!
这个连接的 demo 含有判断方法:http://sandbox.runjs.cn/show/... 。同时给出 Github 仓库地址: minrouter,推荐你们读下源码,仅仅 117 行,精辟!
若是在上面连接测试时你的 url 里多了一个 #,说明你的浏览器该更新啦。
文章出自 orange 的 我的博客 http://orangexc.xyz/