浅谈前端 localhost 本地开发最佳实践
背景
所有前端教程或脚手架都可以做到「开箱即用」,npm i
之后运行 npm start
或 npm run serve
之类的命令,就可以在本地起一个 localhost:xxxx
的服务开始开发了,还带 HMR(热更新)的,非常的方便快捷。
但是,落实到实际业务中,往往没这么简单。比如本地开发需要调用真实的线上接口,此时往往会出现跨域问题;还有登录种 cookie,甚至 SSO 问题。笔者经常看到,为了解决本地开发问题,项目各种本地加代理,起服务,开 APP,绕来绕去的解决方案,笔者认为大可不必。
本文试着梳理一下各个场景下比较方便的本地开发实践,供大家参考。
原则
- 一切以线上环境最优为前提,不可以为了方便本地开发而做一些额外的工作。比如专门为 localhost 开启跨域访问等;
- 本地开发时尽量少依赖其他工具,比如浏览器插件、本地代理、抓包 APP 等;
前提
- 本文项目均默认以
webpack
为底层搭建,并默认读者熟知NODE_ENV
和webpack
常用配置; - 工程中有多个
webpack
配置文件,传入不同NODE_ENV
参数时读取不同配置,举例如下;
# 目录结构
├── config # 工程配置文件
│ ├── webpack.local.js
│ ├── webpack.dev.js
│ ├── webpack.prod.js
│ └── webpack.config.js # webpack-merge + NODE_ENV 判断 require 上面哪个 js
├── package.json
NODE_ENV
变量要同步注入到浏览器当中,方便使用,代码如下:
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
})
]
- 假设现在有一个项目,分为生产(a.com)、测试(a-test.com)、开发(a-dev.com)三个环境,域名如括号中所示。且后端所有的接口都挂在同域的
/api
相对路径下。
正文
同域调用接口
假设我们本地开发 localhost:3000
,需要调用开发环境的接口,即 a-dev.com/api/xxx
。这个时候明显是跨域访问的,要如何解决这个问题呢?
我们知道,后端服务之间的调用是没有跨域概念的,所以解决思路自然是通过后端服务转发,也可称之为代理。如何实现这个代理呢?
Chrome 浏览器有个 Proxy SwitchyOmega
插件,功能很强大,可以解决这个问题,但是下载需要翻墙,所以还是不那么方便的。另外,通过 Charles
、Fiddler
等抓包工具也可以实现代理的功能,但是需要运行额外的 APP,还得有额外的学习成本,还是太麻烦。
那么最佳实践是什么呢?别忘了,我们本地开发的时候,已经有一个现成的 webpack devServer
服务在运行了,理论上它就能做代理的事情。实事也的确如此,devServer.proxy 就是做这个事的,我们来看下用法:
module.exports = {
//...
devServer: {
// server: 'https', // webpack 5
// https: true, // webpack 4
proxy: {
'/api': {
target: 'http://a-dev.com',
// secure: false,
// pathRewrite: { '^/api': '' },
},
},
},
};
如果做了上述配置,本地开发时所有访问 localhost:3000/api/*
的请求,都会被 devServer 「原封不动」(实际上还是动了一些的)地转发到 http://a-dev.com/api
上。
没错,就这么简单,不需要依赖任何其他软件或插件,就加这么几行代码就行。当然,如果你想调用生产或测试环境的接口,只要改一下 target
的值就可以了。
同域实现登录
上面的是最简单的情况,当开发的系统需要用户登录,也就是前后端需要传递 cookie
时情况就稍微复杂一些了,我们来分别看一下。
先是比较简单的场景,登录接口和服务都是同域的后端实现的,比如 /api/login
。还是以开发环境举例,线上使用时,cookie 会被种到 a-dev.com
这个 domain 下,因为前后端是同域的,所以携带 cookie 不会有任何问题。但是如果换到本地开发,会有什么影响呢?
答案是不会有影响,仍然可以正常使用。不同的是本地开发时,cookie 会被种到 localhost
domain 下。此时访问 localhost:3000/api
,cookie 会被带到 devServer 服务,然后 devServer 经过处理后(比如修改 referrer 之类的属性)将 cookie 传到 a-dev.com/api
,所以开发环境的后端还是可以正常消费 cookie 的,其返回也会被 devServer 经过适当处理后再返回给浏览器。
所以,对于同域实现登录的服务,devServer.proxy
的方式仍然可以正常运行,不需要做任何修改。
SSO 登录
说完了同域实现登录服务,我们再来看看更复杂的一个场景,SSO 实现登录服务。关于 SSO 的原理和细节,本文就不做展开了,这里推荐两篇文章:《机房夜话》,《从密码到token, 一个授权的故事》,写的生动形象,深入浅出,笔者认为非常经典,自己读过好几遍,常读常新。下面引用文章里的一张图来说明一下问题:
如上图所示的最后一步,SSO 服务会返回给浏览器一个真实的带 ticket 参数的 302 跳转命令。放到本文的例子就是 a-dev.com/landing?ticket=T123
(通常这个地址会被网关特殊处理),注意,一旦浏览器跳转到了这个地址,就再也不能自动跳回 localhost:3000
了。就算用某些方式能跳转回来,但是 cookie 也只会被种到 a-dev.com
这个 domain 下,而不会种到 localhost
下。所以此种情况下,只使用 devServer.proxy
是无法正常开发的。
上面这段内容对于不太理解 SSO 流程的同学可能有些难理解,不过没关系,只需要记住 SSO 登录的服务,无法单纯使用 devServer.proxy
解决就可以了。那如何解决呢?
推荐方案 - 手动种 cookie
从上文的论述过程中,我们可以发现,关键的问题就在于 cookie 没有成功种在 localhost 上。那么最简单粗暴的解决方式就是把 a-dev.com 下的 cookie 手动种到 localhost 上。听起来比较 low,但是的确有效。具体代码就不贴了,说一下解决思路和需要注意的点:
- 每个项目中要有一个
config-local.json
文件,里面存放 cookie 变量,值即为手动搬运过来的 cookie; - 因为每个人的 cookie 不一样,该文件不应该入库,即应该被写入
.gitignore
。可以写一个config-local-demo.json
文件入库,方便本地开发时复制该文件,改名为config-local.json
即可; - 修改
devServer.proxy['/api'].headers.cookie
的值,即 config-local.json 中的 cookie 值;上文说过将NODE_ENV
变量注入到浏览器中,所以可以根据NODE_ENV === 'local'
的条件,在 Ajax 请求的公共 headers,选择是否携带上config-local.json
中的 cookie。
有的同学可能会问了,那每次 cookie 过期,我都要手动更新一次吗?没错,理论上是这样的。但是,通常 cookie 都有「自动续约」的机制。也就是说如果你每天都会开发这个项目,理论上你碰到 cookie 过期的几率也不是那么大,实际情况更可能是每周手动操作一次。
修改 hosts 不行吗
有的同学会问,那我手动修改下 hosts 文件,甚至用代理工具,把 a-dev.com
强行映射到 localhost:3000
是不是就可以了。很遗憾,所有 a-dev.com
的请求都被你这样「截胡」了,这意味着 a-dev.com/landing?ticket=T123
请求也无法正常发送到后端或网关的,前端是无法处理这种情况的。
前端 landing 页
还有一种场景,一些项目只有后端接口请求过网关,前端页面路由不过网关(所以没法自动完成 ticket 消费的闭环),而是直接定向到对象存储的 index.html
文件上。这种情况一般会做如下处理:
前端专门留出一个 /landing
的空白页面,来处理 ticket
的流转。即前端从 query 中获取到 ticket 后,向后端一个专门的接口(比如 /api/ticket
,下同)发起请求,并携带 ticket。后端得到 ticket 后再向 SSO 发起校验 ticket 的流程,用这种方式完成闭环。此方案的特点如下:
- 本地开发要修改 hosts,将线上真实地址
a-dev.com
解析到 localhost,且本地开发时使用a-dev.com
; - SSO 服务种 ticket 的
redirect_uri
只能固定为a-dev.com/landing
; - 后端有专门的接口,比如
/api/ticket
来接收前端发送的 ticket,并去 SSO 完成验证 ticket 的程序闭环; - SSO 不会因为
redirect_uri
与触发 ticket 验证的地址(/api/ticket
)不同而验证失败; - 所有应用内页面跳转逻辑由前端完成,建议使用
location.replace
跳转。
此方案虽然不需要手动种 cookie 了,但是还是需要修改 hosts,好在修改频率也很低。
当然,即使是这种场景,你也可以选择用手动种 cookie 的方式来解决问题。
跨域调用接口
如果项目中真的存在一些必须跨域调用的接口,要怎么处理呢?情况稍微有些复杂,我们分几个维度来看:
- 被调用方是否允许
localhost
跨域访问?- 不允许:只能修改 hosts 了;
- 允许:请前往第 2 问;
- 是否需要跨域携带 cookie?
- 不需要:可以愉快的使用 localhost 开发了;
- 需要:只能修改 hosts 了;
可以看到,只有满足「被调用方允许 localhost 跨域访问」且「不需要跨域携带 cookie」的情况才能够愉快的使用 localhost 方式开发。看似条件比较苛刻,但是理论上允许跨域调用的服务,就应该具有这个特点。
总结
简单进行一下总结:
- 不需要登录或同域实现登录的,只需要用
devServer.proxy
就能实现本地开发; - SSO 实现登录的,基本手动种 cookie 就能解决问题,但是会有一定的手动成本;
- 跨域调用接口的某些情况,可能需要修改 hosts 才能解决问题。
真实情况其实是很复杂的,很难穷举,本文提供的解决方案肯定不可能 100% 覆盖所有场景。但是按照本文的思路去分类思考,应该也足够应对自己的实际情况了。万变不离其宗,抓住问题的本质,自然能够游刃有余的应对看起来复杂的问题。
昔之善战者,先为不可胜,以待敌之可胜。不可胜在己,可胜在敌。——《孙子兵法·军形篇》
转载自:https://juejin.cn/post/7119722331320090632