说下标题取名的含义: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/",
...
});
启动两个项目,此时项目打开的默认地址分别为
-
hash
模式下的项目默认启动路径:http://localhost:1567/va/#/ -
history
模式下的项目默认启动路径:http://localhost:1567/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>
此时页面似乎都还正常,但真的是这样吗?
仔细分析
仔细观察两类路由的完整地址
-
hash 模式
-
history 模式
对比一下两种路由模式的路径区别,不难发现,在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 项目:http://127.0.0.1/v1/
- hash 项目:http://127.0.0.1/va
此时分别访问项目没有任何问题,切换菜单页面也是正常,并且 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 遇到锚点定位
在history
和hash
项目中分别再加入一个 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
一、修改二期代码
我们现在对hash
和history
项目做出一些改动
分别在Index.vue
项目做出如下改动
html
<div class="home">二期首页</div>
二、打包部署
修改vite.config.js
里的base
配置
- hash 项目
js
defineConfig({
base: "/vb/",
...
});
- history 项目
js
defineConfig({
base: "/v2/",
...
});
在 Nginx 的 html 目录下新建v2
、vb
目录,并且将相应的二期代码改动部署上去
三、新增 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 的vb
、v2
不好听,需要改成vbb
、v22
对于开发而已,客户只是想修改前端的路由地址,并没有对我提出业务修改,所以我们希望不修改我们的代码,企图用 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
这个可以理解,其实还是router
的base
问题
修改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>