前后端分离项目实践--BaseURL与跨域

无论是前端还是后端,从开发阶段开始,很多工程师都习惯使用根(/)来访问自己的页面或者接口,这没问题。但到了部署阶段,往往就会面临变更部署路径的问题。

很多人会说,为啥要改啊,凭啥要改啊,多麻烦呀。

但使用 / 就意味要独占一个域,生产环境中,单个应用通常不可能独占一个域,更常见的是以下这种情况:

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 访问。

这时前端需要做哪些修改呢?主要关注两个方面:

  1. 资源引用路径
  2. 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/'
        : '/'
    }
    

根据上述官方文档的介绍,我们可以知道:

  1. 要将应用部署在 /appA 目录下,只需要将 vue.config.js 中的 publicPath 修改为 "/appA/" 即可。
  2. publicPath 也可以设置为相对路径 ./ 或者 '', 可以让应用部署在任意目录而不影响使用,但是有些现在条件。
  3. 可以通过 env 文件,根据环境不同,灵活设置 publicPath

Vue Router

publicPath 更多是面向 资源打包路径 的设置,那 router 的路径如何设置呢?

vue cli 官方文档在客户端侧代码中使用环境变量 一节中提到

BASE_URL(环境变量) 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。

同时,在 Vue 后端配置例子 的文档中提到:

如果想部署到一个子目录,你需要使用 Vue CLI 的 publicPath 选项 (opens new window) 和相关的 router base property (opens new window)

这里要吐槽一下 VUE 的中文文档,在没看对应的英文文档之前,我根本看不懂。

If you deploy to a subfolder, you should use the publicPath option of Vue CLI and the related base 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。

小结

综上,跨域问题的最优解永远是如何规避,而不是如何解决。绝大多数情况下,工程师不应该面对跨域问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容