阅读 2067

JS 专题系列-前端路由

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('请传入函数类型的参数');
    }
  },

复制代码

好了,上面写好了炼狱的主要代码,下面我们就可以看到对应的效果了。

关注下面的标签,发现更多相似文章
评论