关于前端路由的一点理解

关于路由

路由其实是根据不同的URL地址展示不同的内容或页面;
广义上来说,访问路由会映射到相应的函数里,然后由相应的函数来决定返回给这个URL的内容。路由就是一个匹配过程;

后端路由

在Web前端开发早期,一直是后端路由占据主导地位,不管是PHP,还是JSP、ASP,用户通过URL访问页面时,大多是通过后端路由匹配之后再返回给浏览器。经典面试题[说说浏览器地址栏输入www.baidu.com到网页展示的过程]其实也是在讲这个道理。
不管是什么语言的Web后端框架,都会有一个专门的路由模块或路由区域,用于匹配用户给出的URL地址,以及一些表单提交、AJAX请求的地址。通常遇到无法匹配的路由,后端会返回一个404状态码,这也是404 NOT FOUND的由来。

服务端渲染

在后端为主导的年代,网页HTML一般是后端通过模板引擎渲染好之后响应给前端,这就是服务端渲染。浏览器在地址栏中切换不同的URL时,每次都会向后端发出请求,服务器响应请求。
服务端渲染的好处有很多,比如对SEO友好,一些对安全性要求高的页面采用服务端渲染更保险。
Node.js诞生以后,前端拥有自己的后端模板引擎成为了现实,常见的有ejs、nunjucks。这些模板引擎搭配Express、KoaNode框架也风靡一时。
不过,随着Web应用的开发越来越复杂,单纯的服务端渲染问题开始慢慢暴露了出来:耦合性太强!耦合性问题虽然能通过良好的代码结构、规范来解决,但jQuery时代的页面不好维护也是有目共睹的,全局变量满天飞,代码入侵性太高;后续维护通常也是在给前面的代码打补丁;页面切换白屏问题虽然可以通过AJAXiframe等方案解决,但实际上却进一步增加了可维护性的难度。

前端路由

前端路由:页面跳转的URL规则匹配由前端来控制;应用最广泛的例子就是当今的SPA的Web项目。
前端渲染:以Vue项目为例,浏览器从服务器拿到的HTML里只有一个

,并搭配一系列js文件。所以,我们看到的页面其实是通过这些js渲染出来的。

前端渲染把渲染的任务交给了浏览器,通过客户端的算力来解决页面的构建,在很大程度上缓解了服务端的压力。而且配合前端路由,无缝的页面切换体验,自然对用户是友好的。不过带来的坏处就是对SEO不友好,毕竟搜索引擎的爬虫只能爬到上面那个空荡荡的HTML,而且对浏览器的版本也会有相应的要求。

注意:只要在浏览器地址栏输入URL再回车,是一定会去后端服务器请求一次的。而如果是在页面里通过点击按钮等操作,利用router库的api来进行的URL更新,则不会去后端服务器请求。

前端路由主要有两种方式:

  • hash模式,锚点操作,利用hash值的变化感知路由变化,优点是兼容性高,缺点是URL带有#号不好看,而且有些场景如微信分享 会破坏掉#后面的内容;
  • HTML5history模式,优点是URL不带#号,缺点是需要浏览器和后端同时支持。

hash模式

hash 是浏览器URL# 后面的内容,包含#hashURL中的锚点,代表网页中的一个位置,单单改变 # 后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。

  • # 是用来指导浏览器动作的,对服务器完全无用,HTTP请求中并不包含#
  • 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,点击后退按钮,就可以回到上一个位置。

所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

https://www.abc.com/xv/Home/index#plan
https://www.abc.com/xv/Home/index/#/add/index

hash值的变化不会让浏览器重新发起请求,但会触发 window.onhashChange 事件;

触发hashChange事件的情况:

  • 直接更改浏览器地址,在最后面增加或改变#hash
  • 改变 location.hreflocation.hash 的值;
  • 通过触发点击带锚点的链接;
  • 浏览器前进后退可能导致hash的变化,前提是两个地址中的文档相同、但hash值不同。

如果我们在 hashChange 事件中获取当前的hash值,根据hash值来修改页面内容,就能达到前端路由的目的。
另外, hashChange 事件回调的对象参数中,有两个比较重要的属性newURL、oldURL,分别表示当前变化前、后的URL

简略版

#html:

#script: 应该封装成Router const app = document.getElementById('app') function hashChange(e){ // 当前跳转的新URL 上次的旧URL console.log(e.newURL, e.oldURL) // 根据 hash 值决定显示什么内容 switch (location.hash) { case '#index': app.innerHTML = '

这是首页内容

' break case '#news': app.innerHTML = '

这是新闻内容

' break case '#user': app.innerHTML = '

这是个人中心内容

' break default: app.innerHTML = '

404

' } } window.onhashchange = hashChange hashChange()

除此之外,还需要记录当前URL,监听刷新事件(onload),在onhashchange中实现回退和前进等功能。。。

history模式

history其实浏览器历史栈(历史记录)的一个接口,基于window.history对象的方法

https://www.plysummer.com/#/plan/index  // hash模式路由 
https://www.plysummer.com/plan/index  // history模式路由
  1. HTML4中,已经支持window.history对象来控制页面历史记录跳转,常用的方法包括:
    • history.forward() 在历史栈中前进一步;
    • history.back() 在历史栈中后退一步;
    • history.go(n) 在历史栈中跳转 n 步骤,n=0为刷新本页,n=-1为后退一页。
  2. HTML5中,window.history对象得到了扩展,新增的API包括:
    • history.pushState(data [,title] [,url]) 向历史栈中追加一条记录,data 表示需要保存的数据,在触发popstate事件时,可以在event.state里获取;
      # 当前url:https://www.xxx.com/a/
      # 1. 对新URL使用绝对路径
      history.pushState(null, null, '/qq/')  // https://www.xxx.com/qq/
      # 2. 对新URL使用相对路径
      history.pushState(null, null, './qq/')  // https://www.xxx.com/a/qq/
      # 3. 对新URL使用完整的同源路径
      history.pushState(null, null, 'https://www.xxx.com/kk/qq')
      // https://www.xxx.com/kk/qq
      
    • history.replaceState(data [,title] [,url]) 替换当前页在历史栈中的记录,其他特性与pushState一致;
    • history.state 是一个属性,可以得到当前页的state信息;
    • history.length 当前历史栈中的记录数;
    • window.onpopstate 是一个事件,只有在点击浏览器前进、后退按钮,js调用forward()、back()、go()时触发。
  3. 注意:
    • IE9及其以下版本浏览器是不支持的,IE10开始支持。vue-router会检测浏览器版本,当无法启用history模式时会自动降级为hash模式;
    • pushState()/replaceState()虽然可以改变历史栈,让浏览器地址栏中的URL发生变化,但并不会向后端发起请求!
    • pushState()/replaceState()URL 的修改受同源策略限制,防止恶意脚本模仿其他网站的URL欺骗用户,所以当违背同源策略时将会报错;
    • 火狐目前会忽略 title 参数。
  4. 简略版
# html

# script: 应该封装为Router document.querySelector('#menu').addEventListener('click', e => { if(e.target.nodeName === 'A') { e.preventDefault() // 阻止 的默认事件,默认的跳转会刷新页面 //获取超链接的href,改为 pushState 跳转,不刷新页面 const path = e.target.getAttribute('href') // 修改浏览器中显示的 url window.history.pushState(null, null, path) // 根据path,更改页面内容 render(path) } }) const app = document.getElementById('app') function render(path) { switch (path) { case '/index': app.innerHTML = '

这是首页内容

' break case '/news': app.innerHTML = '

这是新闻内容

' break case '/user': app.innerHTML = '

这是个人中心内容

' break default: app.innerHTML = '

404

' } } //监听浏览器前进后退事件,并根据当前路径渲染页面 window.onpopstate = e => { render(location.pathname) } //第一次进入页面显示首页 render('/index')

我们还可以通过自定义事件,实现对history.pushStatehistory.replaceState的监听。

var _rewrite = function(type) {
   var fn = window.history[type]   // 保存原函数的引用
   var evt = new Event(type)  // 自定义事件
   return function() {  // 闭包
       // 调用原函数
       var res = fn.apply(this, arguments)
       evt.arguments = arguments  // 把参数塞进去
       window.dispatchEvent(evt)  // 分发事件
       return res
   }
}
// 重写方法
window.history.pushState = _rewrite('pushState')
window.history.replaceState = _rewrite('replaceState')
// 监听自定义事件
window.addEventListener('replaceState', e => {
   console.log('replaceState: ', e.arguments)
})
window.addEventListener('pushState', e => {
   console.log('pushState: ', e.arguments)
})
404问题

在前端做页面跳转时,通常是利用history API完成的,router库调用history.pushState() 跟后端没有任何关系。但是一旦从浏览器地址栏里输入一个URL(不管是否有效)并回车或者手动刷新页面,那就会向后端发起一个GET请求。而后端路由表中又没有配置相应的路由,那么自然就会返回404 NOT FOUND!这也就是为什么很多人在生产模式下遇到404页面的原因。

  • hash模式,发送的HTTP请求是不变的,不包含锚点部分,本质上始终请求的是打包后的index.html
  • history模式,发送的HTTP请求是完整的浏览器地址,对后端来说,这样的路由是不存在,所以会出现404

这也就是history模式为什么需要后端同时支持。
vue-router文档上给出了一个配置例子:在所有后端路由规则的最后,加上一个默认匹配规则 -- 如果URL匹配不到任何静态资源,则响应同一个 index.html 给前端。
这样就解决了后端路由抛出的404问题,前端拿到的也始终是打包后的index.html了。再通过路由库的处理,获取地址栏的URL信息,告知前端库(Vue、React)渲染对应的页面。到了这一步就跟hash模式类似了。

这样在后端配置之后,404页面的处理权又交回了前端。以Nginxvue-router为例,同时解决手动刷新浏览器和手动输入URL 并回车的 404问题:

    # 后端:nginx.conf
    server {
        listen       8080;
        server_name  xx.xxx.xxx.xx;
        root html;  # vue项目的打包后的dist
        location / {
            # 指向下面的@router,解决刷新出现404问题
            try_files $uri $uri/ @router;
            index   index.html index.htm;
        }
        location @router {
            # 重写到index.html中,然后交给前端路由去处理请求资源
            rewrite ^.*$ /index.html last;
        }
    }

    # 前端:vue-router
    {
        path: "/404",
        name: "404",
        component: () => import('@/views/404.vue')
    },
    // 当输入不存在的URL时,在前端重定向到404页面
    { path: "*", redirect: "/404" }

问题延伸:
IE浏览器下刷新仍然还是404,是因为IE自作聪明,对于页面大小 < 1024b 会被认为十分不友好,所以ie就将改页面给替换成自己的错误提示页面了,而SPA打包后的 index.html 可能会小于临界值。

资源路径问题

history模式下,访问路由和嵌套路由页面,显示正常,但是刷新页面的时候,嵌套路由页面就出异常了!查看网络请求发现,请求加载的静态资源(图片、CSS、JS...)都是404!查看请求路径发现,根路径发生了变化。
而资源的引入方式:

你可能感兴趣的:(关于前端路由的一点理解)