彻底不NG前端路由

说下标题取名的含义:NG在电影行业以及技术测试行业的意思是No Good的缩写,我加了一个不,双重否定表肯定,意思是掌握好前端路由,其次NG还代表Nginx服务器,也是我今天分享的内容之一。可以看出这次分享不仅仅给前端分享,也涉及到部分后端在Nginx的配置。

其实前端部署一直属于模糊的地带,大多数前端对服务器不熟悉,觉得部署是后端的工作,这归后端管,但后端由于不熟悉前端的路由差异,统一配置时遇到问题却无从下手解决,从而相互推卸责任,恰巧在前段时间北京的前端同事就找我问了这个问题,他说本地项目都没问题,放服务器就有问题了,是后端问题。

前端部署究竟是归前端管,还是后端管,现在都没有人明确的说清,那我本次分享就是为了针对这些痛点问题来做的,下面进入我们的主题分享。

本次分享的学习收获

  • 📝 更好掌握:深度对比两种路由模式,掌握每种路由的优势和劣势
  • 🎨 更好使用:区分在不同项目中,更适合使用哪种路由
  • 🧑 更好解决:解决不同路由模式下可能遇到的问题

从实战中分析问题

为了更好的掌握两种路由的区别,我不直接告诉你这两种路由的区别,而是通过创建两个简单的项目上手、部署来认识它们的优劣。

通过 vue 脚手架创建两个项目,一个使用hash路由,一个使用history路由

修改路由配置

  • hash 路由
js 复制代码
const router = new VueRouter({
  mode: "hash",
  routes,
});
  • history 路由
js 复制代码
const router = new VueRouter({
  mode: "history",
  routes,
});

为了区分 hash 路由和 history 路由在前端的路径,我将 hash 路由项目的前缀添加/va标识,history 路由项目的前缀添加/v1标识

分别在vite.config.js配置项中加入base配置

  • hash 路由
js 复制代码
export default () =>
  defineConfig({
    base: "/va/",
    ...
  });
  • history 路由
js 复制代码
export default () =>
  defineConfig({
    base: "/v1/",
    ...
  });

启动两个项目,此时项目打开的默认地址分别为

可发现,目前两种路由模式,仅在配置mode选参上有区别。

但。。真的是这样吗?

尝试添加一些页面

新建两个页面,分别为首页、关于页面,我们使用router去切换页面

路由router.js配置

js 复制代码
import Vue from "vue";
import VueRouter from "vue-router";
import Index from "../views/Index.vue";
import About from "../views/About.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/index",
    name: "Index",
    component: Index,
  },
  {
    path: "/about",
    name: "About",
    component: About,
  },
];

const router = new VueRouter({
  mode: "history", // hash项目此处为hash
  routes,
});

export default router;

首页Index.vue页面

html 复制代码
<template>
  <div class="home">首页</div>
</template>

<script>
export default {};
</script>

<style>
.home {
  height: 500px;
  width: 500px;
  margin: 30px auto;
  background-color: pink;
}
</style>

关于About.vue页面

html 复制代码
<template>
  <div class="about">关于</div>
</template>

<script>
export default {};
</script>

<style>
.about {
  height: 500px;
  width: 500px;
  margin: 30px auto;
  background-color: aqua;
}
</style>

App.vue页面

html 复制代码
<template>
  <div id="app">
    <div class="nav">
      <button @click="changeMenu('Index')">首页</button>
      <button @click="changeMenu('About')">关于</button>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
  export default {
    methods: {
      // 切换页面
      changeMenu(name) {
        this.$router.push({
          name,
        });
      },
    },
  };
</script>

此时页面似乎都还正常,但真的是这样吗?

仔细分析

仔细观察两类路由的完整地址

对比一下两种路由模式的路径区别,不难发现,在history路由下,切换到首页或者关于页面时,路由中的 /v1路径丢失了

我们所期待的 history 模式下切换到首页的路径是:http://localhost:1567/v1/index

这会影响我们使用吗?实际上这对我们影响非常大!不信你刷新页面试试看。

思考: 既然影响如此大,我们需要如何达到我们所期待的路径?

实现期待值

方法 1

为 history 模式下的每个页面path添加多一个前缀/v1

js 复制代码
const routes = [
  {
    path: "/v1/index",
    name: "Index",
    component: Index,
  },
  {
    path: "/v1/about",
    name: "About",
    component: About,
  },
];

但随着页面多,我们都需要手动添加一遍的话就太麻烦了

方法 2

为路由添加一个base选参

js 复制代码
const router = new VueRouter({
  base: "v1",
  mode: "history",
  routes,
});

打包部署对比

我们分别打包两个项目,并且使用 nginx 作为我们的服务器,将前端部署上去

安装好 nginx 后,进入 nginx 目录下的/html文件夹,创建va作为hash项目的文件夹,v1作为history项目的文件夹

最后把打包好的前端项目放入相应的文件夹下

1、打开配置/conf/nginx.conf文件,并且添加如下配置

bash 复制代码
# history项目
location /v1 {
    alias   html/v1;
    index  index.html index.htm;
}

# hash项目
location /va {
    alias   html/va;
    index  index.html index.htm;
}

2、最后执行nginx -s reload指令重载配置

3、分别访问:

此时分别访问项目没有任何问题,切换菜单页面也是正常,并且 history 下的路径没有丢失 /v1 路径,大功告成!

但是,真的能够如我们所愿吗?

在 history 项目的访问路径下,我们再次刷新页面试试。

天啊!怎么这玩意又出现了。又是怎么出现的呢?为什么 hash 路由不会出现这样的情况?

由于 hash 模式时 url 带的#号后面是哈希值不会作为 url 的一部分发送给服务器,而 history 模式下当刷新页面之后浏览器会直接去请求服务器,而服务器没有这个路由,于是就出现 404。

解决顽固份子 404

打开配置/conf/nginx.conf文件,并且为history项目的配置添加如下配置

bash 复制代码
# history项目
location /v1 {
    alias   html/v1;
    try_files $uri $uri/ /v1/index.html;
    index  index.html index.htm;
}

修改配置后,记得执行nginx -s reload指令重载配置

此时再刷新页面就好了

那一把梭哈 hash 不就好了?

既然配置 history 需要这么多注意事项,那我在所有前端项目一把梭哈 hash 模式不就好了?为什么还要在history上纠结?

这届网页给的答案

  • hash 值前面需要加#, 不符合 url 规范,也不美观
  • 每次 URL 的改变不属于一次 http 请求,所以不利于 SEO 优化
  • hash 的传参是基于 URL 的, 如果要传递复杂的数据, 会有体积限制

其实像这些答案,都不是致命的,因为很多项目根本不需要考虑 url 美观与否、seo,更不需要担心在 url 传参数据过多的问题

那照你这么说,我一把梭哈 hash 也不是什么问题。

如果你是这么想的,我只能告诉你:大多数项目都没什么问题。

除非什么?我们更多关心的是:使用hash路由时,会给我们项目上带来一些什么样的难题。

例如你项目中需要用到锚点定位时!

当 hash 遇到锚点定位

historyhash项目中分别再加入一个 Anchor 页面

Anchor.vue 页面完整代码

html 复制代码
<template>
  <div class="anchor">
    <div class="menu">
      <a class="menu-item" v-for="item in menuList" :key="item.name" :href="'#'+item.name">
        {{ item.name }}
      </a>
    </div>
    <div class="content">
      <div v-for="item in menuList" 
        :key="item.name" :id="item.name" 
        class="content-item" :style="{'background':item.color}">
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        menuList:[
         {name:"菜单一",content:"我是菜单一功能",color:"red"},
          {name:"菜单二",content:"我是菜单二功能",color:"green"},
          {name:"菜单三",content:"我是菜单三功能",color:"pink"},
        ]
      }
    }
  }
</script>

<style lang="scss" scoped>
  .anchor{
    width: 800px;
    display: flex;
    margin: 30px auto;
    .menu{
      width: 200px;
      height: 300px;
      background-color: aquamarine;
      &-item{
        height: 50px;
        line-height: 50px;
        text-align: center;
        cursor: pointer;
        display: block;
      }
    }
    .content{
      margin-left: 30px;
      width: 570px;
      background-color: #Ff9900;
      height: 600px;
      overflow: auto;
      padding: 0 30px;
      &-item{
        height: 400px;
        line-height: 400px;
        width: 100%;
        margin-bottom: 40px;
        text-align: center;
        background-color: purple;
        color: #fff;
      }
    }
  }
</style>

分别点击左侧菜单,右边的内容都可以通过路由锚点定位到相应的区域,似乎好像没什么区别

但尝试刷新下,发现了什么呢?

hash项目下的页面变空白了!是什么原因导致的?

仔细观察当前hash项目的 url:http://localhost:1567/va/#/菜单三

但是锚点页面的 url 本应是:http://localhost:1567/va/#/anchor

好家伙!妥妥的狸猫换太子,可以发现anchor菜单三替换了

而 hash 路由是通过#后面的内容去匹配的,而菜单三不在我们路由配置识别的文件范围内,所以当前匹配不到相应的页面

包括 vue 的官网,vue2 的 api 项目文档是点击左侧api是通过锚点定位的,vue3 的 api 项目文档则改成了 history 路由去切换,而右侧还多了一些小功能点,这些小功能点才是通过锚点定位的。可想而知,vue的api文档也遇到了这样的问题,而考虑使用history路由模式去构建

vue2:v2.cn.vuejs.org/v2/api#opti...

vue3:cn.vuejs.org/api/applica...

但这个问题也不是无解

解决 hash 路由锚点问题

第一步:停止使用 a 标签 href 来锚点定位

第二步:使用scrollIntoView这个 api 来定位

参数 说明
behavior [可选] 控制滚动的状态,默认为"auto"。auto / instant / instant
block [可选] 控制垂直方向上的位置,"center"。start / center / end / nearest
inline [可选] 控制水平方向上的位置,默认为"nearest"。start / center / end / nearest

完整代码

html 复制代码
<template>
  <div class="anchor">
    <div class="menu">
      <a
        class="menu-item"
        v-for="item in menuList"
        :key="item.name"
        :href="'#'+item.name"
      >
        {{ item.name }}
      </a>
      <!-- <a class="menu-item" v-for="item in menuList" :key="item.name" href="javasctipt:void;" @click="toHere(item.name)">
        {{ item.name }}
      </a> -->
    </div>
    <div class="content">
      <div
        v-for="item in menuList"
        :key="item.name"
        :id="item.name"
        class="content-item"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        menuList: [
          { name: "菜单一", content: "我是菜单一功能" },
          { name: "菜单二", content: "我是菜单二功能" },
          { name: "菜单三", content: "我是菜单三功能" },
        ],
      };
    },
    methods: {
      toHere(name) {
        this.$el.querySelector(`#${name}`).scrollIntoView({
          behavior: "smooth",
          block: "start",
        });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .anchor {
    width: 800px;
    display: flex;
    margin: 30px auto;
    .menu {
      width: 200px;
      height: 300px;
      background-color: aquamarine;
      &-item {
        height: 50px;
        line-height: 50px;
        text-align: center;
        cursor: pointer;
        display: block;
      }
    }
    .content {
      margin-left: 30px;
      width: 570px;
      background-color: #ff9900;
      height: 600px;
      overflow: auto;
      padding: 0 30px;
      &-item {
        height: 400px;
        line-height: 400px;
        width: 100%;
        margin-bottom: 40px;
        text-align: center;
        background-color: purple;
        color: #fff;
      }
    }
  }
</style>

那是不是就可以放心使用 hash 路由了

什么项目不建议使用hash路由:

  • 需要seo的项目
  • 在分布式微前端项目中,嵌套的子应用和主应用都使用 hash 模式时,由于 hash 模式的 URL 路径只能存在一个#,会导致子应用和主应用在定义 URL 路径上存在困难。

学会了 Nginx,是不是可以无脑 history

也有的朋友觉得,既然hash路由存在使用限制,那我就学会history的路由配置,还有Nginx配置,是不是所有项目就可以无脑冲history路由了

存在则合理,hash 路由不仅仅是 Nginx 配置起来简单,而且对前端也是很友好

试想想在有这么一个场景:当前我们的项目属于一期阶段,客户增加需求,开发进入二期阶段,我们需要开放一个二期的临时路径给客户看到开发的效果进度

例如:

xml 复制代码
<!-- hash项目 -->
一期路径为:http://127.0.0.1/va

二期路径为:http://127.0.0.1/vb


<!-- history项目 -->
一期路径为:http://127.0.0.1/v1

二期路径为:http://127.0.0.1/v2

一、修改二期代码

我们现在对hashhistory项目做出一些改动

分别在Index.vue项目做出如下改动

html 复制代码
<div class="home">二期首页</div>

二、打包部署

修改vite.config.js里的base配置

  • hash 项目
js 复制代码
defineConfig({
    base: "/vb/",
    ...
  });
  • history 项目
js 复制代码
defineConfig({
    base: "/v2/",
    ...
  });

在 Nginx 的 html 目录下新建v2vb目录,并且将相应的二期代码改动部署上去

三、新增 Nginx 配置

bash 复制代码
# hash项目二期升级
location /vb {
    alias   html/vb;
    try_files $uri $uri/ /vb/index.html;
    index  index.html index.htm;
}

# history项目二期升级
location /v2 {
    alias   html/v2;
    try_files $uri $uri/ /v2/index.html;
    index  index.html index.htm;
}

四、访问测试

  • hash 项目升级
  • history 项目升级

在测试过程中,我们发现 history 在访问时,虽然内容是二期的内容,但是 url 路径是 v1 的内容

这是因为当时在配置router配置时,base没修改导致的

修改二期history的 router 配置

js 复制代码
const router = new VueRouter({
  base: "v2",
  mode: "history",
  routes,
});

重新打包部署,就没有这个问题了。

到此,我们发现,好像 history 也就需要比 hash 模式多修改一个配置,然而真的是这样吗?

客户又提出新需求了

他来啦他来啦,他带着新需求走来啦~

客户觉得 URL 的vbv2不好听,需要改成vbbv22

对于开发而已,客户只是想修改前端的路由地址,并没有对我提出业务修改,所以我们希望不修改我们的代码,企图用 Nginx 帮我们完成这个修改

修改 Nginx 配置

bash 复制代码
location /vbb {
    alias   html/vb;
    try_files $uri $uri/ /vb/index.html;
    index  index.html index.htm;
}

location /v22 {
    alias   html/v2;
    try_files $uri $uri/ /v2/index.html;
    index  index.html index.htm;
}

修改配置后,执行nginx -s reload指令重载配置,再次测试

hash 项目访问:http://127.0.0.1/vbb

访问没问题,并且点击首页后,刷新页面也没问题

history 项目访问:http://127.0.0.1/v22

访问没问题,并且点击首页后,此时路径变成v2了,刷新页面直接 404

项目 访问 切换路由 刷新
hash路由项目 正常 正常 正常
history路由项目 正常 页面正常,但是路径仍然为v2 404

分析 history 项目的问题

点击首页,路径变v2这个可以理解,其实还是routerbase问题

修改history项目的 router 配置

js 复制代码
const router = new VueRouter({
  base: "v22",
  mode: "history",
  routes,
});

重新打包再试,此时就没有什么问题了

但回想下,客户的需求没有涉及到业务代码的改动,在history模式下,我们却需要修改前端配置代码,并且需要重新部署才能实现客户的目的。

这是不是比hash路由项目更麻烦一点,如果你还能接受,再提出亿点点改动试试。。。

客户又㕛叒提出新的需求

客户说:二期测试没什么问题了,我们把一期项目屏蔽,但是有部分用户收藏了我们一期的页面,如果用户还是访问一期链接,我们给他自动重定向到二期的链接,达到无感升级的过程。

Nginx有个return 301重定向的功能,或者proxy_pass都可以达到重定向的功能,我们企图使用return 301来达到目的

修改 Nginx 配置

bash 复制代码
location /va {
    # alias   html/va;
    # try_files $uri $uri/ /va/index.html;
    # index  index.html index.htm;
    return 301 /vbb;
}

location /v1 {
    # alias   html/v1;
    # try_files $uri $uri/ /v1/index.html;
    # index  index.html index.htm;
    return 301 /v22;
}

修改配置后,执行nginx -s reload指令重载配置,再次测试

再次访问一期的页面,注意:我们这里访问关于页面,并且携带一个参数

hash 一期项目:http://127.0.0.1/va/#/about?name=测试

history 一期项目:http://127.0.0.1/v1/about?name=测试

可以发现,history 又出问题了。页面只重定向到了http://127.0.0.1/v22/,后面的/about路径丢失了,而且?name=测试参数也丢失了

再次改造 Nginx 配置

bash 复制代码
location /v1 {
    # alias   html/v1;
    # try_files $uri $uri/ /v1/index.html;
    # index  index.html index.htm;
    # return 301 /v22;
    rewrite ^/v1(/.*)?$ /v22$1 permanent;
}

修改配置后,执行nginx -s reload指令重载配置,再次测试

此时页面终于好了,而且参数也保留了

回想一下,我们为了满足客户的需求,都做了那些付出

hash 项目

  • 改造 Nginx 配置:return 301

history 项目

  • 改造前端路由 router 的 base 配置
  • 重新打包部署
  • 改造 Nginx 配置:return 301,发现行不通,需要写正则等复杂配置

经过这么对比,hash 的优势还是很大。

总结

情景 history hash
需要SEO ✅︎ ⭕️
微前端框 ✅︎ ⭕️
项目存在复杂迭代 ⭕️ ✅︎

⭕️ 是并不是指不能使用,只是选择级别不那么靠前

大多数项目优先考虑使用hash路由

附录

  • hash路由实现原理
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>hash路由原理</title>
  <style>
    .app{
      text-align: center;
    }
    li{
      list-style: none;
    }
    .nav{
      display: flex;
      align-items: center;
      justify-content: space-around;
      width: auto;
      margin:  0 auto;
      width: 100px;

    }
    .home,.about{
      height: 500px;
      width: 500px;
      margin: 30px auto;
    }
    .home{
      background-color: pink;
    }
    .about{
      background-color: aqua;
    }
  </style>
</head>
<body>
  <!-- 模拟单页页面应用 -->
  <ul class="nav">
      <li><a href="#/home">首页</a></li> 
      <li><a href="#/about">关于</a></li>
      <!-- 判断url的变化,绑定点击事件不好,页面过多就很累赘,有个hashchange的官方方法 -->
  </ul>

  <div id="routeView">
      <!-- 放一个代码片段 点击首页首页代码片段生效,反之关于生效-->

  </div>
  <script>
      const routes = [
          {
              path: '#/home',
              component: '<div class="home">首页页面内容</div>'
          },
          {
              path: '#/about',
              component: '<div class="about">关于页面内容</div>'
          }
      ]
      
      const routeView = document.getElementById('routeView')
      window.addEventListener('DOMContentLoaded', onHashChange) // 与vue的声明周期一个道理,dom一加载完毕就触发
      window.addEventListener('hashchange', onHashChange)
      
      function onHashChange() {
          routes.forEach((item, index) => {
              if(item.path === location.hash) {
                  routeView.innerHTML = item.component
              }
          })
      }
  </script>
</body>
</html>
  • history路由实现原理
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>history路由原理</title>
  <style>
    #app{
      text-align: center;
    }
    li {
        list-style: none;
    }
    .nav{
      display: flex;
      align-items: center;
      justify-content: space-around;
      width: auto;
      margin:  0 auto;
      width: 100px;
      cursor: pointer;
    }
    .home,.about{
      height: 500px;
      width: 500px;
      margin: 30px auto;
    }
    .home{
      background-color: pink;
    }
    .about{
      background-color: aqua;
    }
  </style>
</head>
<body>
  <div id="app">
    <ul class="nav">
        <li><a name="/home">首页</a></li> 
        <li><a name="/about">关于</a></li>
    </ul>    
      <div id="routeView"></div>
  </div>

  <script>
      const routes = [
          {
              path: '/home',
              component: '<div class="home">首页页面内容</div>'
          },
          {
              path: '/about',
              component: '<div class="about">关于页面内容</div>'
          }
      ]
      
      const routeView = document.getElementById('routeView')

      window.addEventListener('DOMContentLoaded', onLoad)
      window.addEventListener('popstate', onPopState)

      function onLoad() {
          const links = document.querySelectorAll('li a') // 获取所有的li下的a标签
          // console.log(links)
          links.forEach((a) => {
              // 禁用a标签的默认跳转行为
              a.addEventListener('click', (e) => {
                  /*
                   * history.pushState是HTML5提供的一个新特性,
                   * 它允许我们以一种优雅的方式来改变当前浏览器地址栏的url,并且不会像传统的URL跳转那样重新加载整个页面。
                   * */ 
                  history.pushState(null, '', a.getAttribute('name')) // 核心方法  a.getAttribute('href')获取a标签下的href属性
                  // 映射对应的dom
                  onPopState()
              })
          })
      }

      function onPopState() {
          routes.forEach((item) => {
              if(item.path === location.pathname) {
                  routeView.innerHTML = item.component
              }
          })
      }
  </script>
</body>

</html>
相关推荐
熊的猫26 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人2 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人2 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR2 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香2 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596932 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书