原文:http://www.html-js.com/article/JavaScript-version-100-lines-of-code-to-achieve-a-modern-version-of-Router

当前到处可见单页应用,而对于单页应用来说我们必须有一个有效的路由机制。像Emberjs就是建立在一个Router类上的框架。虽然我不是太确信这个是不是我喜欢的东东,但是肯定的是AbsurdJS必须有一个内置的Router。和这个框架中的其他功能一样,这个Router应该非常小巧简单的。让我们看下这个模块应该是什么样子呢。编辑:github 原文链接:A modern JavaScript router in 100 lines

需求

这里设计的router应该是这样的:

  • 少于100行代码
  • 支持散列输入的URL,比如http://site.com#products/list
  • 能够支持History API
  • 提供简单可用的接口
  • 不会自动运行
  • 可以监听变化
  • 采用单例模式

我决定只用一个router实例。这个可能是一个糟糕的选择,因为我曾经做过需要几个router的项目,但是反过来说着毕竟不常见。如果我们采用单例模式来实现我们将不用在对象和对象之间传递router,同时我们也不担心如何创建它。我们只需要一个实例,那我们就自然而然这样创建了:

  1. var Router = {
  2. routes: [],
  3. mode: null,
  4. root: '/'
  5. }

这里有3个属性:

  • routes-它用来保存当前已经注册的路由。
  • mode-取值有hash和history两个选项,用来判断是否使用History API
  • root-应用的根路径。只有当我们使用pushState我们才需要它。

配置

我们需要一个方法去启动router。虽然只要设定两个属性,但是我们最好还是使用一个方法来封装下。

  1. var Router = {
  2. routes: [],
  3. mode: null,
  4. root: '/',
  5. config: function(options) {
  6. this.mode = options && options.mode && options.mode == 'history'
  7. && !!(history.pushState) ? 'history' : 'hash';
  8. this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
  9. return this;
  10. }
  11. }

只有在支持pushState的情况下才会支持history模式,否则我们就运行于hash模式下。root默认被设定为‘/’。

获得当前URL

这是router中非常重要的一部分,因为它告诉我们当前我们在哪里。因为我们有两个模式,所以我们要一个if判断。

  1. getFragment: function() {
  2. var fragment = '';
  3. if(this.mode === 'history') {
  4. fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
  5. fragment = fragment.replace(/\?(.*)$/, '');
  6. fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
  7. } else {
  8. var match = window.location.href.match(/#(.*)$/);
  9. fragment = match ? match[1] : '';
  10. }
  11. return this.clearSlashes(fragment);
  12. }

两种条件下我们都是用了全局对象window.location。在history模式下我们需要删除掉URL中的root部分,同时还需要通过正则(/\?(.*)$/)去删除所有get的参数。hash模式下比较简单。注意下方法clearSlashes,它是用来删除斜线的。这非常有必要,因为我们不想强制开发者使用固定格式的URL。所有他传递进去后都转换为一个值。

  1. clearSlashes: function(path) {
  2. return path.toString().replace(/\/$/, '').replace(/^\//, '');
  3. }

增加和删除route

设计AbsurdJS的时候,我是尽量把控制权交给开发者。在大多数router实现中,路由一般被设计成字符串,但是我倾向于正则表达式。这样通过传递疯狂的正则表达式,可以使系统的可扩展性更强。

  1. add: function(re, handler) {
  2. if(typeof re == 'function') {
  3. handler = re;
  4. re = '';
  5. }
  6. this.routes.push({ re: re, handler: handler});
  7. return this;
  8. }

这个方法用来填充routes数组。如果只传递了一个方法,那我们就把它当成一个默认路由处理器,并且把它当成一个空字符串。注意这里大多数方法返回了this,这是为了方便级联调用。

  1. remove: function(param) {
  2. for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
  3. if(r.handler === param || r.re === param) {
  4. this.routes.splice(i, 1);
  5. return this;
  6. }
  7. }
  8. return this;
  9. }

如果我们传递一个合法的正则表达式或者handler给删除方法,那就可以执行删除了。

  1. flush: function() {
  2. this.routes = [];
  3. this.mode = null;
  4. this.root = '/';
  5. return this;
  6. }

有时候我们需要重置类,那我们就需要一个flush方法来执行重置。

Check-in

当前我们已经有增加和删除URL的API了,同时也要可以获得当前的地址。那么接下来的逻辑就是去比对注册了的实体。

  1. check: function(f) {
  2. var fragment = f || this.getFragment();
  3. for(var i=0; i<this.routes.length; i++) {
  4. var match = fragment.match(this.routes[i].re);
  5. if(match) {
  6. match.shift();
  7. this.routes[i].handler.apply({}, match);
  8. return this;
  9. }
  10. }
  11. return this;
  12. }

我们使用getFragment来创建fragment或者直接把函数的参数赋值给fragment。然后我们使用了一个循环来查找这个路由。如果没有匹配上,那match就为null,否则match的只应该是下面这样的[”products/12/edit/22”, “12”, “22”, index: 1, input: ”/products/12/edit/22”]。他是一个对象数组,包含了匹配上的字符串和子字符串。这意味着如果我们能够匹配第一个元素的话,我们就可以通过正则匹配动态的URL。例如:

  1. Router
  2. .add(/about/, function() {
  3. console.log('about');
  4. })
  5. .add(/products\/(.*)\/edit\/(.*)/, function() {
  6. console.log('products', arguments);
  7. })
  8. .add(function() {
  9. console.log('default');
  10. })
  11. .check('/products/12/edit/22');

脚本输出:

products [”12”, “22”]

这就是我们能够处理动态URL的原因。

监控变化

我们不能一直运行check方法。我们需要一个在地址栏发生变化的时候通知我们的逻辑。我这里说的变化包括触发浏览器的返回按钮。如果你接触过History API的话你肯定会知道这里有个popstate 事件。它是当URL发生变化时候执行的一个回调。但是我发现一些浏览器在页面加载时候不会触发这个事件。这个浏览器处理不同让我不得不去寻找另一个解决方案。及时在mode被设定为hash的时候我也去执行监控,所以我决定使用setInterval。

  1. listen: function() {
  2. var self = this;
  3. var current = self.getFragment();
  4. var fn = function() {
  5. if(current !== self.getFragment()) {
  6. current = self.getFragment();
  7. self.check(current);
  8. }
  9. }
  10. clearInterval(this.interval);
  11. this.interval = setInterval(fn, 50);
  12. return this;
  13. }

我需要保存一个最新的URL用于执行比较。

改变URL

在我们router的最后,我们需要一个方法可以改变当前的地址,同时也可以触发路由的回调。

  1. navigate: function(path) {
  2. path = path ? path : '';
  3. if(this.mode === 'history') {
  4. history.pushState(null, null, this.root + this.clearSlashes(path));
  5. } else {
  6. window.location.href.match(/#(.*)$/);
  7. window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
  8. }
  9. return this;
  10. }

同样,我么能这对不同的模式做了分支判断。如果History API当前可用的话,我们就是用pushState,否则我们我们就是用window.location。

最终代码

下面是最终版本的router,并附了一个小例子:

  1. var Router = {
  2. routes: [],
  3. mode: null,
  4. root: '/',
  5. config: function(options) {
  6. this.mode = options && options.mode && options.mode == 'history'
  7. && !!(history.pushState) ? 'history' : 'hash';
  8. this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
  9. return this;
  10. },
  11. getFragment: function() {
  12. var fragment = '';
  13. if(this.mode === 'history') {
  14. fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
  15. fragment = fragment.replace(/\?(.*)$/, '');
  16. fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
  17. } else {
  18. var match = window.location.href.match(/#(.*)$/);
  19. fragment = match ? match[1] : '';
  20. }
  21. return this.clearSlashes(fragment);
  22. },
  23. clearSlashes: function(path) {
  24. return path.toString().replace(/\/$/, '').replace(/^\//, '');
  25. },
  26. add: function(re, handler) {
  27. if(typeof re == 'function') {
  28. handler = re;
  29. re = '';
  30. }
  31. this.routes.push({ re: re, handler: handler});
  32. return this;
  33. },
  34. remove: function(param) {
  35. for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
  36. if(r.handler === param || r.re === param) {
  37. this.routes.splice(i, 1);
  38. return this;
  39. }
  40. }
  41. return this;
  42. },
  43. flush: function() {
  44. this.routes = [];
  45. this.mode = null;
  46. this.root = '/';
  47. return this;
  48. },
  49. check: function(f) {
  50. var fragment = f || this.getFragment();
  51. for(var i=0; i<this.routes.length; i++) {
  52. var match = fragment.match(this.routes[i].re);
  53. if(match) {
  54. match.shift();
  55. this.routes[i].handler.apply({}, match);
  56. return this;
  57. }
  58. }
  59. return this;
  60. },
  61. listen: function() {
  62. var self = this;
  63. var current = self.getFragment();
  64. var fn = function() {
  65. if(current !== self.getFragment()) {
  66. current = self.getFragment();
  67. self.check(current);
  68. }
  69. }
  70. clearInterval(this.interval);
  71. this.interval = setInterval(fn, 50);
  72. return this;
  73. },
  74. navigate: function(path) {
  75. path = path ? path : '';
  76. if(this.mode === 'history') {
  77. history.pushState(null, null, this.root + this.clearSlashes(path));
  78. } else {
  79. window.location.href.match(/#(.*)$/);
  80. window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
  81. }
  82. return this;
  83. }
  84. }
  85.  
  86. // configuration
  87. Router.config({ mode: 'history'});
  88.  
  89. // returning the user to the initial state
  90. Router.navigate();
  91.  
  92. // adding routes
  93. Router
  94. .add(/about/, function() {
  95. console.log('about');
  96. })
  97. .add(/products\/(.*)\/edit\/(.*)/, function() {
  98. console.log('products', arguments);
  99. })
  100. .add(function() {
  101. console.log('default');
  102. })
  103. .check('/products/12/edit/22').listen();
  104.  
  105. // forwarding
  106. Router.navigate('/about');

  

总结

router类大概有90行代码。它支持散列输入的RUL和History API。如果你不想用整个框架的话我想这个还是非常有用的。

这个类是AbsurdJS类的一部分,你可以在这里查看到这个类的说明。

源代码可以在github下载到。

100行代码实现现代版Router的更多相关文章

  1. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)【转】

    转自:http://blog.csdn.net/leixiaohua1020/article/details/8652605 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] ...

  2. 【编程教室】PONG - 100行代码写一个弹球游戏

    大家好,欢迎来到 Crossin的编程教室 ! 今天跟大家讲一讲:如何做游戏 游戏的主题是弹球游戏<PONG>,它是史上第一款街机游戏.因此选它作为我这个游戏开发系列的第一期主题. 游戏引 ...

  3. 第一行代码 Android 第二版到货啦

    今日android第一行代码[第二版]已到,收获的季节到了 先看一下封面 书签: 以后就把空闲时间送给它吧 先来看一下本书的目录: 第1章 开始启程--你的第1行Android代码 第2章 先从看得到 ...

  4. 【转】100行代码实现最简单的基于FFMPEG+SDL的视频播放器

    FFMPEG工程浩大,可以参考的书籍又不是很多,因此很多刚学习FFMPEG的人常常感觉到无从下手.我刚接触FFMPEG的时候也感觉不知从何学起. 因此我把自己做项目过程中实现的一个非常简单的视频播放器 ...

  5. 200行代码实现简版react🔥

    200行代码实现简版react

  6. 用JavaCV改写“100行代码实现最简单的基于FFMPEG+SDL的视频播放器 ”

    FFMPEG的文档少,JavaCV的文档就更少了.从网上找到这篇100行代码实现最简单的基于FFMPEG+SDL的视频播放器.地址是http://blog.csdn.net/leixiaohua102 ...

  7. 100行代码让您学会JavaScript原生的Proxy设计模式

    面向对象设计里的设计模式之Proxy(代理)模式,相信很多朋友已经很熟悉了.比如我之前写过代理模式在Java中实现的两篇文章: Java代理设计模式(Proxy)的四种具体实现:静态代理和动态代理 J ...

  8. GuiLite 1.2 发布(希望通过这100+行代码来揭示:GuiLite的初始化,界面元素Layout,及消息映射的过程)

    经过开发群的长期验证,我们发现:即使代码只有5千多行,也不意味着能够轻松弄懂代码意图.痛定思痛,我们发现:虽然每个函数都很简单(平均长度约为30行),可以逐个击破:但各个函数之间如何协作,却很难说明清 ...

  9. 100行代码实现HarmonyOS“画图”应用,eTS开发走起!

    本期我们给大家带来的是"画图"应用开发者Rick的分享,希望能给你的HarmonyOS开发之旅带来启发~ 介绍 2021年的华为开发者大会(HDC2021)上,HarmonyOS ...

随机推荐

  1. Flask中的before_request和after_request

    1.@app.before_request 在请求(request)之前做出响应 @app.before_request 也是一个装饰器,他所装饰的函数,都会在请求进入视图函数之前执行 2.@app. ...

  2. Codeforces 1058 D. Vasya and Triangle(分解因子)

    题目:http://codeforces.com/contest/1058/problem/D 题意:有一个大小为N*M的矩阵内,构造一个三角形,使面积为(n*m)/k.若存在输出三个顶点(整数). ...

  3. 尝试利用slmail的漏洞来getshell

    作者:Joe   本文属于Arctic shell原创内容计划文章,转载请注明原文地址! 二进制,计算机才可以理解的低级语言,简单来说它是一种信号,用电信号为例,0就是断电,而1就是有电,这样子010 ...

  4. Linux巩固记录(9) keepalived+nginx搭建高可用负载分发环境

    环境准备(继续服用hadoop节点) slave1  192.168.2.201(CentOs 7) slave2  192.168.2.202(CentOs 7) slave1 和 slave2 上 ...

  5. 对drf的初步认识

    web应用模式 1.前后端不分离 在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高. 这种应用模式比较适合纯网 ...

  6. vue中封装公共方法,全局使用

    1.以封装的合并单元格为例,首先建立一个util.js 2.在main.js中引用 3.直接在使用该方法的地方调用就可以了

  7. 用开源 ASP.NET MVC 程序 Bonobo Git Server 搭建 Git 服务器(转)

    用开源 ASP.NET MVC 程序 Bonobo Git Server 搭建 Git 服务器   现在不用Git,都不好意思说自己是程序员. 当你想用Git,而源代码服务器是Windows系统时,你 ...

  8. [工具]Tomcat CVE-2017-12615 远程代码执行

    工具: K8_TocmatExp编译: VS2012  C# (.NET Framework v2.0)组织: K8搞基大队[K8team]作者: K8拉登哥哥博客: http://qqhack8.b ...

  9. 课程一(Neural Networks and Deep Learning),第二周(Basics of Neural Network programming)—— 2、编程作业常见问题与答案(Programming Assignment FAQ)

    Please note that when you are working on the programming exercise you will find comments that say &q ...

  10. Nginx单向认证的安装配置

    Nginx单向认证的安装配置 首先系统要已经安装了openssl,以下是使用openssl安装配置单向认证的执行步骤与脚本: #------------------------------------ ...