彻底不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>
相关推荐
余生H3 分钟前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿8 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~15 分钟前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫18 分钟前
高德地图自定义折线矢量图形
前端·vue.js·vue
m0_7482509320 分钟前
html 通用错误页面
前端·html
来吧~29 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
鎈卟誃筅甡42 分钟前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅1 小时前
二、创建第一个VUE项目
前端·javascript·vue.js
m0_748239331 小时前
前端(Ajax)
前端·javascript·ajax
Fighting_p1 小时前
【记录】列表自动滚动轮播功能实现
前端·javascript·vue.js