无论是前端还是后端,从开发阶段开始,很多工程师都习惯使用根(/)
来访问自己的页面或者接口,这没问题。但到了部署阶段,往往就会面临变更部署路径的问题。
很多人会说,为啥要改啊,凭啥要改啊,多麻烦呀。
但使用 /
就意味要独占一个域,生产环境中,单个应用通常不可能独占一个域,更常见的是以下这种情况:
http://example.com/xxx --> appA
http://example.com/yyy --> appB
http://example.com/zzz --> appC
....
更不要说单个应用的前端或者后端要独占一个域了,那简直就是天方夜谭。人为的将前后端部署在不同的域,纯属就是自找麻烦。
对于前端而言,生产环境的部署路径,往往是前端工程师无法掌控的,甚至在开发阶段这些都是不确定的。开发阶段可以使用 / 作为部署路径,但是上预览环境、生产环境的时候,前端工程师需要知道怎么修改部署路径。
这里引用小岳岳的一句话:
许我不要,不许你没有。
对于后端而言,如果是单纯的 RPC 服务,不包含前端调用的接口,完全不需要关心什么域啊,context-path 之类的东西。但是如果包含前端调用,生产环境必然要对服务进行映射,也就是会和其他应用共用一个域。不管是专门的应用网关,还是nginx,都需要通过 context-path 对流量进行切分。所以没有 context-path 是不行的。虽然我们可以通过一些技术手段,例如 Web Server 的 rewrite 功能,来调整后端服务的 context-path,但是,效果不如直接变更后端服务的 context-path。
从技术角度来讲,修改几个路径并不麻烦,也并不困难。但是沟通起来确实是很麻烦。为什么麻烦呢?很多时候是团队内部对问题的描述没有统一的“语言”。两个人沟通,往往处于张不开嘴 和 听不明白的叠加态。
比如 BaseURL,不同的场景,可能会幻化出无数个分身,子目录?二级目录?router base?baseURL?publicPath?server.context-path?上下文(名字/路径)?甚至是“项目名”。面对这种模拟信号式的沟通,效率可想而知。
BaseURL 直译就是 基础URL
(VUE 的中文文档中经常出现),但是这很模糊,到底什么意思呢?这里的 Base 更多是 Prefix 的意思,也就是前缀。通俗一点讲,BaseURL 是指多个 URL 中公共或者相同的前缀。比如:
那么我们可以说,这两个 url 的 baseURL 是 http://example.com/A/ 仅此而已。BaseURL 这个词太普通了,很容易被误导,所以使用时通常需要强调是什么东西的BaseURL,或者使用更加场景化的术语,比如:publicPath, context-path 等等。
前端如何修改部署路径
以下,以 vue 为例,其他框架类似。
场景:原先开发阶段前端部署在 /
, 我们可以通过 http://127.0.0.1:3000/index.html 访问,部署阶段我们要把应用部署在一个子目录中, 使其能通过 http://example.com/appA/index.html 访问。
这时前端需要做哪些修改呢?主要关注两个方面:
- 资源引用路径
- router base
Vue CLI
baseUrl
Deprecated since Vue CLI 3.3, please use publicPath instead.
Vue CLI 3.3 之前,使用 baseUrl 进行配置,但是 3.3 之后改用 publicPath 进行配置
至于为什么修改,可以参考 https://github.com/vuejs/vue-cli/pull/3143 。大概意思是Vue CLI 的 baseUrl 和 webpack 的 publicPath 的关系不是很清楚,容易误导用户,索性改成一个名字。
publicPath
Type:
string
Default:
'/'
部署应用包时的基本 URL(known as
baseUrl
before Vue CLI 3.3)。用法和 webpack 本身的output.publicPath
一致,但是 Vue CLI 在一些其他地方也需要用到这个值,所以请始终使用publicPath
而不要直接修改 webpack 的output.publicPath
。默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如
https://www.my-app.com/
。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在https://www.my-app.com/my-app/
,则设置publicPath
为/my-app/
。这个值也可以被设置为空字符串 (
''
) 或是相对路径 ('./'
),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。相对 publicPath 的限制
相对路径的
publicPath
有一些使用上的限制。在以下情况下,应当避免使用相对publicPath
:
- 当使用基于 HTML5
history.pushState
的路由时;- 当使用
pages
选项构建多页面应用时。这个值在开发环境下同样生效。如果你想把开发服务器架设在根路径,你可以使用一个条件式的值:
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/production-sub-path/' : '/' }
根据上述官方文档的介绍,我们可以知道:
- 要将应用部署在 /appA 目录下,只需要将 vue.config.js 中的 publicPath 修改为 "/appA/" 即可。
- publicPath 也可以设置为相对路径 ./ 或者 '', 可以让应用部署在任意目录而不影响使用,但是有些现在条件。
- 可以通过 env 文件,根据环境不同,灵活设置 publicPath
Vue Router
publicPath 更多是面向 资源打包路径 的设置,那 router 的路径如何设置呢?
在 vue cli 官方文档 的 在客户端侧代码中使用环境变量
一节中提到
BASE_URL
(环境变量) 会和vue.config.js
中的publicPath
选项相符,即你的应用会部署到的基础路径。
同时,在 Vue 后端配置例子 的文档中提到:
如果想部署到一个子目录,你需要使用 Vue CLI 的
publicPath
选项 (opens new window) 和相关的 routerbase
property (opens new window)。
这里要吐槽一下 VUE 的中文文档,在没看对应的英文文档之前,我根本看不懂。
If you deploy to a subfolder, you should use the
publicPath
option of Vue CLI and the relatedbase
property of the router.
综上,我们可以使用 process.env.BASE_URL 来设置 router 的 base
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
这样就可以达到 publicPath 和 router base 的统一。
后端如何修改 BaseURL
以 springboot 为例
对于 springboot 应用,修改 BaseURL 只需要修改 server.servlet.context-path 即可。
以下内容根据来自 spring-context-vs-servlet-path
上下文路径(context path)是用于访问 Web 应用程序的名称。它是应用程序的根。默认情况下,Spring Boot 使用根 ("/")作为上下文路径(context path) 提供服务。
因此,任何具有默认配置的 Boot 应用程序都可以通过 http://localhost:8080/ 访问
但是,在某些情况下,我们可能希望更改应用程序的上下文。有多种方法可以配置上下文路径,application.properties就是其中之一。此文件位于src/main/resources文件夹下。
让我们使用application.properties文件对其进行配置:
server.servlet.context-path=/demo
然后,应用访问地址就变成:http://localhost:8080/demo
axios 的 baseURL
如果后端服务由于修改 context-path 导致接口地址变化,前端也需要做相应调整。以下,以 axios 为例进行说明。
baseURL
axios 创建实例,或者发起请求的时候,可以设置 baseURL,官方文档里是这么解释的:
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
// `baseURL` will be prepended to `url` unless `url` is absolute.
// It can be convenient to set `baseURL` for an instance of axios to pass relative URLs
// to methods of that instance.
baseURL 会自动加在请求的 url 之前。所以,如果后端接口地址变了,我们只需要修改 axios 的 baseURL 即可。
那这里说的绝对路径是什么呢?如果看下 axios 的代码就知道了:
/**
* Determines whether the specified URL is absolute
*
* @param {string} url The URL to test
* @returns {boolean} True if the specified URL is absolute, otherwise false
*/
module.exports = function isAbsoluteURL(url) {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};
axios 中所谓的绝对路径,就是以 <scheme>://
(协议头)或者 //
开头的URL,除此之外都算先对路径,都会在头部附加 baseURL。
由于,不同的环境中,后端地址通常是不一样的。一般,都会在 env 文件中定义一个变量,来配置 axios baseURL。例如:
.env.development
VUE_APP_BASE_URL=/api/
.env.production
VUE_APP_BASE_URL=/appname/api/
然后就可以使用一下方式设置 axios
const client = axios.create({
baseURL: process.env.VUE_APP_BASE_URL
});
浏览器对请求地址自动拼装
还有一个问题,在 axios 官方文档,以及网上大多数文档中,给出的示例都是下面这个样子的。
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
这里的重点是:baseURL 必须包含协议头、域名和端口吗?
答案是不需要。axios 是一个前后端都可以使用的包,后端发请求需要带上域名很正常。但是,前端使用的时候,除了跨域的场景,都不需要添加协议头和域名。
浏览器会使用当前浏览的页面的信息,自动将 axios 的请求地址拼装成完整的URL。
浏览器中的相对和绝对路径
这里还涉及到另一个相对路径和绝对路径的问题。这里的绝对路径,指的是以 / 开头的路径,与 axios 中所说的绝对路径不同。
假设,前端部署在 https://example.com/licenses/ 路径下。
如果 baseURL = '/api',注意,baseURL 是以 / 开头的,是一个绝对路径。在执行 axios.get('/licenses')
,axios 会把 baseURL 拼装在 /licenses 之前,变成 /api/licenses,依然是个绝对路径。浏览器最终发出请求的地址是 https://example.com/api/licenses
如果 baseURL = 'api',注意,baseURL 不是以 / 开头的,是一个相对路径。在执行 axios.get('/licenses')
,axios 会把 baseURL 拼装在 /licenses 之前,变成 api/licenses,依然是个相对路径。浏览器最终发出请求的地址是 https://example.com/licenses/api/licenses
,可以看到,相对路径最终的请求地址,取决于发出请求的页面的地址。
关于跨域
关于跨域的背景知识,可以参考一下两篇文章
网上搜索到的绝大多数资料,都在给你讲解跨域问题的技术原理,如何解决跨域问题,有7、8种,甚至10多种办法。但是很少有人告诉你:绝大多数情况下,跨域问题是不应该出现的!
只有当你调用的服务是你无法控制的,比如,公网上的免费接口,第三方合作伙伴的接口,甚至是另一个部门或团队的接口。并且,你没办法把自己的应用和他们部署在同一个域下的时候,你才需要面对跨域问题。除此之外,应用的部署位置绝大多数情况下都是可控的,都是可调整的。完全可以避免跨域的发生。开发环境也更是如此。
跨域问题的出现,往往是因为部署规划没做好。所谓的前后端,指的是同一个应用的前后端。所谓前后端分离,指的是前后端开发的分离,人员的分离,职能的分离,而绝不是前后端部署(域)的分离。非要把同一个应用的两个部分部署在不同的域,然后再想各种办法解决跨域,何苦呢?
开发阶段的跨域问题
开发阶段前后端进行联调的时候,前后端通常不在同一个域下面,例如,前端运行在开发工程师的电脑上,后端有可能是在服务器上,或者后端工程师的电脑上,跨域是必然的。那么这个时候如何规避跨域的问题呢?那就是使用 devserver 的 proxy 功能。网上的例子很多。
假设,后端接口位于 https://192.168.1.30:8085/app/。
修改 vue.config.js 中 devServer 子节点内容,添加一个 proxy:
module.exports = {
devServer:{
...
proxy:{
'/app':{
target: 'http://192.168.1.30:8085',
}}
},
//...
}
devServer.proxy 的作用实际上是将前后端放到同一个域里,从而消除了跨域的问题。
注意事项:
-
axios 的设置
如果 axios 的 baseURL 配置的是绝对路径,例如 'http://192.168.1.30:8085/app/',axios 会直接发送请求而不经过 devServer.proxy。如果后台没有设置
Access-Control-Allow-Origin: *
,该请求就会因为跨域被浏览器拦截。如果 axios 的 baseURL 设置为相对路径 '/app/',则可以正常使用 devserver.proxy 进行请求转发。也不会有跨域问题。
如果前端代码中写死了后端地址,开发阶段可能导致无法使用 devServer.proxy,到了部署阶段还要面临频繁修改后端地址的麻烦。
所以,一般情况下,Web 前端代码中不应该出现后端的ip、端口等信息,都应该在 proxy 中统一配置。
-
changeOrigin
这个参数经常被说是用来解决跨域问题的,其实这个参数和跨域一点关系都没有。
假设当前前端访问地址是 http://localhost:3000/。
如果 changeOrigin 为 false,devServer 在转发请求的时候,不会修改 http 请求的 Host 头,Host 头的值依然是 localhost:3000,如果后端接口是基于域名访问的,就会找不见接口。
如果 changeOrigin 为 true,devServer 在转发请求的时候,会修改 http 请求的 Host 头,Host 头的值为 target 指定的 host,此时就可以正常访问了。
生产环境的跨域问题
生产环境中,通常会使用 nginx 来提供web 服务,一方面用来托管静态文件,另一方面代理后端的接口。这和 devServer.proxy 在开发环境中发挥的作用是完全相同的。如果愿意,开发工程师完全可以在自己电脑上也装个 nginx,来代替 devServer.proxy。
只要把前后端放在同一个域里,就不存在跨域问题了。当然,为了配置的方便,前端项目最好部署在二级目录,而后端则最好配置了 context-path。
小结
综上,跨域问题的最优解永远是如何规避,而不是如何解决。绝大多数情况下,工程师不应该面对跨域问题。