前言
由于新加入前端大哥在不到三个月之内就离职了,之前他负责的项目中还缺少了获奖名单的需求,正好我没活了就由我接手了。乍一看代理配置,好家伙,乱中麻(代码虽然能走通,但增加了后续维护的理解成本)。
由于涉及到 SSO 单点登录,需要域名才能正常登录(即请求登录和用户信息的接口),因此需要在 nginx 服务器配置代理,导致了代理绕来绕去。
需要域名的原因:
由于是国际服,账号登录使用的是 SSO 单点登录,需要借助 facebook, google 等登录接口(以 facebook 为例,即登录后 facebook 会返回 token 等用户信息),由于 facebook 规定借助 SSO 单点登录时需要域名,因此团队中通常用的 ip 地址不能作为测试环境,而需要有域名的 nginx 服务器代理到测试环境,才能进行登录。
而在开发过程中,通常设置了登录逻辑,因此必须通过登录(即成功发送登录请求)才能执行其他操作(其他接口请求,页面跳转等)。但由于后端那边的设计,把普通邮箱的登录也设置成了需要域名才能发送请求,因此需要借助 nginx 服务器代理到普通 ip 的测试环境。
正文
在本文中,普通测试地址为 10.10.13.187:8082/,带有域名的 nginx 服务器为 debug-test.example.com:59998/,后端接口 ip 地址分别为 登录接口 12.12.5.205:21301/,业务接口 12.12.5.205:21302/,上传接口 12.12.5.205:21303/
环境变量 env.js 的配置
关于环境变量,由于不同的环境需要不同的跳转链接,代理地址等等,即开发环境有开发环境相匹配的 env,镜像环境有镜像环境的 env,正式环境有正式环境的 env。
在我们团队比较老的项目中,通常是这样部署的
.env
// .env.development
NODE_ENV=development
VUE_APP_ENV=development
PUBLIC_PATH=/account
通过 process.env.VUE_APP_ENV
,拿到此时的开发环境,再做相应的匹配,但这样做交给后端打包部署后,后端是无法看到 env 的配置的
而最新的项目中,则是放到 public 文件下,后端把项目打包后能够暴露出来看到
js
// public/env.js
window.$$env = {}
// src/utils/env.ts
export const env = (window as any).$$env as Env;
axios 的配置
一开始的 axios 的配置
account.js
js
const instance = Axios.create({
baseURL: "https://" + env.baseUrl.login + "/",
});
instance.interceptors.request.use((config) => {
return intercepeReq(
env.baseUrl.login,
env.signature.secret
);
});
从这里可以初略看出,baseURL 过于不合理,按理来说axios应该是由本地发送请求的,但这里居然拼接了https成了一个网址,有点不符合认知。
interceptors
请求拦截部分,关于 intercepeReq
函数,是用于传签名的,但按照语义来看,命名应该是放入签名 signature 中的变量却放入了请求基本路径 baseUrl 的变量当中,并且签名和基本路径居然同用一个变量。
一开始的 env 配置
js
window.$$env = {
baseUrl: {
login: "debug-test.example.com:59998",
contribute: "debug-test.example.com:59998",
upload: "debug-test.example.com:59998",
},
signature: {
secret: "WAxxhxxa8afxxqaWTxxwyt8BxxBeQpHcTxxG6xxaknA=",
},
}
修改后的 axios 配置
js
const instance = Axios.create({
baseURL: env.baseUrl.account,
});
instance.interceptors.request.use((config) => {
return intercepeReq(
env.signature.backend,
env.signature.secret
);
});
修改后的 env 配置
js
window.$$env = {
baseUrl: {
account: "/account-web-sso",
createjam: "/1st-co-creation/createjam",
uploadFile: "/1st-co-creation/upload",
},
signature: {
secret: "WAxxhxxa8afxxqaWTxxwyt8BxxBeQpHcTxxG6xxaknA=",
backend: "debug-test.example.com:59998",
},
}
其中,由于请求头设置了签名的原因,签名中包含了主机 host 即 signature 中的 backend,客户端发出的网络请求的主机host必须与设置的签名的主机host相同,考虑到该项目由于登录权限必须使用到域名,因此签名中的主机host设置统一设置为域名的测试环境主机host,而不是本地 ip 或者其他测试环境的。
一开始其他请求的 axios 配置
contribute.js
js
const instance = Axios.create({
baseURL: "https://" + env.baseUrl.contribute + "/createjam/",
});
instance.interceptors.request.use((config) => {
return intercepeReq(
env.baseUrl.contribute,
env.signature.secret
);
});
upload.js
js
const instance = Axios.create({
baseURL: "https://" + env.baseUrl.upload + "/",
});
instance.interceptors.request.use((config) => {
return intercepeReq(
env.baseUrl.upload,
env.signature.secret
);
});
devServer 配置
一开始 devServer 的配置
js
【1】"/apis/v1/userinfo": {
target: "http://10.10.5.205:21301/",
changeOrigin: true,
},
【1】"/apis/v1/logout": {
target: "http://10.10.5.205:21301/",
changeOrigin: true,
},
【2】"/apis/v1/upload": {
target: "http://10.10.5.205:21303/",
changeOrigin: true,
},
【3】"/apis/": {
target: "http://10.10.5.205:21302/",
changeOrigin: true,
},
再结合 axios 的 各个接口请求配置片段
js
// account.js
export function getUserinfo() {
return instance.get("/apis/v1/userinfo");
}
export function postLogout() {
return instance.get("/apis/v1/logout");
}
// contribute.js
export function contribute(path: string, data: ContributeData) {
return instance.post(`/apis/v1/works/submit/${path}`, data);
}
// upload.js
export function uploadFile (file: File) {
return instance.post(`/apis/v1/upload/anniversary`, file);
}
可以看出,每一个接口请求都基本在 deServer
中配置了,然后其他特殊接口匹配不上就会匹配常用业务接口,难以区分出每个代理的意义,这真的对后续开发人员极其不友好(后续开发者要缕一遍逻辑才能知道这接口是连接谁的)。从上述可以看出, account.js
中的 两个 /apis/v1/xxx
分别配置了两个,都是指向的同一个 ip 地址,这明显不太合适。
修改后
每个 devServer 的配置最好有个前缀,可以用 vue.config.js 中的 publicPath 的值,即标明了这个接口是哪个项目的,并且统一,比如以 /1st-co-creation
为例。
结合 axios 中的 baseUrl 的值,代理前缀 /1st-co-creation
加上 baseUrl,再搭配正则表达式对接口重写来请求后端提供的接口地址。
而关于 devServer 的配置可以看这篇文章 白嫖即食:构建工具的proxy代理配置区别(解决跨域)
js
"/1st-co-creation/createjam": {
target: "http://10.10.5.205:21302/",
changeOrigin: true,
pathRewrite: {
"/1st-co-creation/createjam": "",
},
}
"/1st-co-creation/upload": {
target: "http://10.10.5.205:21303/",
changeOrigin: true,
pathRewrite: {
"/1st-co-creation/upload": "",
},
},
然后再说说通用接口,比如登录接口在业务中肯定有复用的,所以直接表明该接口的功能就行了。
js
"/account-web-sso": {
target: "http://10.10.5.205:21301/",
changeOrigin: true,
pathRewrite: {
"/account-web-sso": "",
},
},
聊到这里,前端的代理基本聊完了,但最坑爹的就是涉及到 SSO 单点登录需要域名,然后我们就需要在 nginx 再做一层代理。所以也不能怪老哥,毕竟搁谁来也得蒙,我隔壁呆了好几年安卓转前端的哥也蒙。
nginx 的配置
而只要你敢用域名代理到本地(即 nginx 中【1】的代理,我就敢说它返回的就是一个 html 文档(整个文档是项目经过webpack打包之后放入代理后服务器中的),然后根据 html 文档里的js请求路径,以此时域名发送请求,即你上面在 devServer 中的代理配置根本就是白费力气,因为它是以debug域名发送请求的,而你的 devServer 代理配置是以代理后服务器请求的。
nginx
【1】location /1st-co-creation/ {
proxy_pass http://10.10.13.187:8082/1st-co-creation/;
}
location /apis/v1/userinfo {
proxy_pass http://10.10.5.205:21301;
}
location /apis/v1/logout {
proxy_pass http://10.10.5.205:21301;
}
location /apis/v1/works/submit {
proxy_pass http://10.10.5.205:21302;
}
location /apis/v1/upload {
proxy_pass http://10.10.5.205:21303;
}
仍然可以看到,犯得错误和 devServer 中的一模一样,而且这里是 nginx 的配置,很多需要用到debug的项目都会到这里配置,所以标明是哪个项目非常重要,并且要注意不要污染了整个配置(即只要你用了整个代理名字,其他人就不能用了)
其实nginx代理的配置和devServer代理的配置原理基本一样,都是匹配成功后把匹配后的字段往后接。 如果在 nginx 没有配置相应的代理或者没有匹配上,会返回 404
修改后的
nginx
location /1st-co-creation/ {
proxy_pass http://10.10.13.187:8082/1st-co-creation/;
}
location /1st-co-creation/createjam/ {
proxy_pass http://10.10.5.205:21301/;
}
location /account-web-sso/ {
proxy_pass http://10.10.5.205:21302/;
}
location /1st-co-creation/upload/ {
proxy_pass http://10.10.5.205:21303/;
}
看看就可以
由于以下设定,导致了客户端的 html 会进行缓存,而 js 或者 css 不会进行缓存,因此当你更新 env.js 时,不能对 env.js 进行删除和改动这两操作,因为当你更新 env.js 并上线后,此时 env.js 是最新的,为了以防客户端拿到是缓存的(即老的html文件),这时 env.js 是最新的,老的 html 文件无法找到 env.js 中东西而导致报错。
nginx
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location ~* \\.(?:css|js)$ {
add_header Cache-Control "no-cache";
root /usr/share/nginx/html;
try_files $uri =404;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
因为借助了 nginx 服务器代理来启动项目,导致UI设计开发时不能热更新,需手动刷新才能更新一次,但在设计UI时可使用本地,涉及到接口时使用域名测试环境。
特典
由于在开发过程中,涉及到了 cookie 相关的问题,即由于代理问题导致了后端没有给前端下发cookie,返回了401的错误
然后经此发现,我对 Cookie 除了简单的了解(它是一个类似于 session 的本地存储空间)外,其他竟一无所知。
cookie
由于 HTTP 的请求是无状态的,浏览器发出的请求后端服务器没办法区分用户的身份和信息。而浏览器每次发送请求时都会在请求头上携带 Cookie 。而 Cookie 会存储一些用户的信息,比如 token,sessionid(通话id),偏好设置等。
通常 Cookie 是由后端设置的,大致流程如下:
- 首次访问网站时,浏览器发送请求中并未携带 Cookie 。(类似于登录)
- 后端看到请求中未携带 Cookie ,在 HTTP 的响应头中加入
Set-Cookie
(里面放了 Cookie 的信息)返回给浏览器。 - 浏览器收到
Set-Cookie
后,会自动将 Cookie 保存下来。 - 之后访问该网站时,HTTP 请求头就会携带 Cookie,用户就不需要再次登录什么的了。
当然,也有由前端收集,后端存储的,比如日夜间模式,语言等偏好设置由前端收集好,发送请求后携带上由后端存储和管理。
掘金token请求中响应头set-Cookie
掘金请求中请求头携带的cookie
注意:请求头中携带的cookie是以key=value,";"隔开的形式携带的。
而想要更直观的看,可以查看浏览器Network中的cookie选项
cookie中属性
属性 | 含义 |
---|---|
Name | Cookie的名称 |
Value | 对应名称的值 |
Domain | Cookie作用的域名 |
Path | Cookie生效的路径 |
Expires | 过期时间,过了这个时间后Cookie失效(具体的时间日期) |
Max-age | 生效时间,表示Cookie在多长时间后失效(倒计时) |
Size | Cookie的长度,为name和value的长度和 |
HttpOnly | 防止通过JavaScript访问Cookie(即在控制台中通过document.cookie读取,设置cookie) |
Secure | 只在HTTPS协议的情况下才会将Cookie传到后端(http达咩) |
SameSite | 是否允许跨站请求时发送Cookie |
Partitioned | 第三方Cookie分区 |
Priority | 优先级 |
作用范围
Domain
设置 Cookie 作用的域名,即 Cookie 在哪个网站生效。比如后端在 Domain 中设置了 .juejin.cn
,那该 Cookie只能在这网站下生效。
Path
当我们希望 Cookie 只在部分路径下生效,就可以使用Path进行限制。如果 path=/user
,相当于除了 .juejin.cn/user
这个路由路径下能使用设置的 Cookie 外,其他路径都不能使用。
.juejin.cn 能使用
.juejin.cn/user 能使用
.juejin.cn/user/2239042325059533 能使用
.juejin.cn/edior 不能使用
作用时间
Session
如果Cookie的有效期为Session,类似于sessionStorge一样,即关闭浏览器就失效。
Expires
指定具体的失效时间,格式大致为2025-02-17T04:44:56.921Z
,即到2025年2月17日4点44分56秒921毫秒
失效
Max-Age
从拿到 Cookie 后开始倒计时设置 的Max-Age的值。例如Max-age=3000
。当获取到该Cookie后开始倒计时,3000秒之后便失效。
Priority优先级
当Cookie的数量超过限制时,浏览器会清除一部分Cookie。清除哪些合适呢?Priority属性用来定义Cookie的优先级,低优先级的Cookie会优先被清除。
Priority属性有三种: Low, Medium, High
其他的过于复杂或者在上述概括中大致提到的就不详细说了。