文章目录

1. 什么是跨域

1.1 本域和同源策略

在了解什么是跨域之前,我们需要先了解两个概念:本域和同源策略

  • 本域:同协议、同域名、同端口
  • 同源策略:为了保护用户隐私和防止恶意网站窃取数据,浏览器默认只允许与本域的接口进行交互

在这里插入图片描述


同源策略规定,一个域下的 JavaScript 脚本不能直接访问或读取另一个域的资源,也不能直接向另一个域发起请求

1.2 跨域的概念

当浏览器发出一个请求时,只要请求URL的协议、域名、端口三者之间任意一个与当前页面URL不同,就称为跨域

当前页面URL请求URL是否跨域原因
http://www.test.comhttp://www.test.com/index.html否同源(协议、域名、端口都相同)
http://www.test.comhttps://www.test.com/index.html是协议不同
http://www.test.comhttp://www.baidu.com是域名不同
http://www.test.com:8080http://www.test.com:8088是端口不同

2.在Vue3项目(由Vite构建)中通过代理解决跨域问题

2.1 编写vite.config.js配置文件

在项目的根目录下找到vite.config.js配置文件,编写与代理相关的配置

在这里插入图片描述

import {fileURLToPath, URL} from 'node:url'

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue()
    ],
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./src', import.meta.url))
        }
    },
    server: {
        proxy: {
            // 在此处编写代理规则
        }
    }
})

2.2 代理规则详解

配置代理规则主要关注proxy中的四个属性:

  • 要触发代理的路径
  • target
  • changeOrigin
  • rewrite

2.2.1 要触发代理的路径

以下述代理规则为例

proxy: {
    // 在此处编写代理规则
    '/api': {
        target: 'http://localhost:7150',
        changeOrigin: true,
        rewrite: (path) => {
            return path.replace(/\/api/, '')
        }
    }
}

只要浏览器发出的请求URL的路径中含有/api,该代理规则就会生效

2.2.2 target和changeOrigin

target要和changeOrigin结合使用,只有changeOrigin的值为true,target才会生效

以上述代理规则为例,如果浏览器发出的请求URL为/api/user/login,经过代理后,浏览器真正发出的请求URL就是http://localhost:7150/user/login


changeOrgin的作用就是改变请求URL的源(也就是域)。在上述例子中,请求URL的源被改成了http://localhost:7150(target)

2.2.3 rewrite

在配置代理规则时,我们不仅可以改变请求URL的源,还可以修改请求URL的路径

rewrite: (path) => {
    return path.replace(/\/api/, '')
}

path.replace()方法接收两个参数

  • 第一个参数可以填写正则表达式,也可以填写纯字符串,代表要匹配的路径
  • 第二个参数填写字符串,代表匹配到路径后,要将匹配到的路径替换成什么样的内容

在上述例子中,浏览器发出的请求URL为/api/user/login,因为请求URL的路径中含有/api,经过代理后,路径中的/api被替换成了空字符串。最后,浏览器真正发出的请求URL就是http://localhost:7150/user/login

2.3 测试代理是否生效

2.3.1 搭建后台服务器

我们用SpringBoot搭建一个后台服务器,编写一个简单的实体类和一个简单的Controller,在 7150 端口上启动,用于接收前端发送的请求

LoginDto.java

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
    
    private String username;
    
    private String password;

    @Override
    public String toString() {
        return "LoginDto{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
    
}

UserController.java

import cn.edu.scau.pojo.LoginDto;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    
    @PostMapping("/login")
    public String login(@RequestBody LoginDto loginDto) {
        System.err.println("登录信息:" + loginDto);
        return "已成功接收到登录信息:" + loginDto;
    }
    
}

2.3.2 前端发起请求

我们用axios发起请求

axios.post('/api/user/login', {
  username: 'admin',
  password: '123456'
}).then((response) => {
  console.log('后端返回的数据:', response.data)
})

后台服务器成功收到数据

在这里插入图片描述


前端也成功收到后台服务器返回的信息

在这里插入图片描述

2.4 配置多个代理规则

配置多个代理规则时,代理规则之间需要用逗号分隔

proxy: {
    // 在此处编写代理规则
    '/api': {
        target: 'http://localhost:7150',
        changeOrigin: true,
        rewrite: (path) => {
            return path.replace(/\/api/, '')
        }
    },
    '/dev': {
        target: 'http://localhost:7150',
        changeOrigin: true,
        rewrite: (path) => {
            return path.replace(/\/dev/, '')
        }
    }
}

2.5 代理举例

为了方便演示,使用黑马程序员提供的接口(大家多多支持黑马程序员的课程)

接口文档地址:B站-AJAX和黑马头条-数据管理平台

https://apifox.com/apidoc/shared-1b0dd84f-faa8-435d-b355-5a8a329e34a8/api-87683404

2.5.1 例一

代理规则

'/areaList': {
    target: 'http://hmajax.itheima.net',
    changeOrigin: true,
    rewrite: (path) => {
        return path.replace(/\/areaList/, '/api/area')
    }
}

前端发起的请求URL

axios.get('/areaList?pname=广东省&cname=广州市')
    .then((response) => {
      console.log('广东省广州市的地区列表:', response.data)
    })

经过代理后真正发出的请求URL

http://hmajax.itheima.net/api/area?pname=广东省&cname=广州市

返回的数据

{
    "message": "获取地区县成功",
    "list": [
        "荔湾区",
        "越秀区",
        "海珠区",
        "天河区",
        "白云区",
        "黄埔区",
        "番禺区",
        "花都区",
        "南沙区",
        "萝岗区",
        "增城市",
        "从化市"
    ]
}

2.5.2 例二

代理规则

'/newsList': {
    target: 'http://hmajax.itheima.net',
    changeOrigin: true,
    rewrite: (path) => {
        return path.replace(/\/newsList/, '/api/news')
    }
}

前端发起的请求URL

axios.get('/newsList')
    .then((response) => {
      console.log('新闻列表:', response.data)
    })

经过代理后真正发出的请求URL

http://hmajax.itheima.net/api/news

返回的数据

{
    "message": "获取新闻列表成功",
    "data": [
        {
            "id": 1,
            "title": "5G渗透率持续提升,创新业务快速成长",
            "source": "新京报经济新闻",
            "cmtcount": 58,
            "img": "http://ajax-api.itheima.net/images/0.webp",
            "time": "2222-10-28 11:50:28"
        },
        {
            "id": 5,
            "title": "为什么说中美阶段性协议再近一步,读懂周末的这些关键信息",
            "source": "澎湃新闻",
            "cmtcount": 131,
            "img": "http://ajax-api.itheima.net/images/4.webp",
            "time": "2222-10-24 09:08:34"
        },
        {
            "id": 6,
            "title": "阿根廷大选结果揭晓:反对派费尔南德斯有话要说",
            "source": "海外网",
            "cmtcount": 99,
            "img": "http://ajax-api.itheima.net/images/5.webp",
            "time": "2222-10-23 17:41:15"
        },
        {
            "id": 8,
            "title": "LV母公司当年史上最大并购:报价145亿美元购Tiffany",
            "source": "澎湃新闻",
            "cmtcount": 119,
            "img": "http://ajax-api.itheima.net/images/7.webp",
            "time": "2222-10-22 03:59:44"
        },
        {
            "id": 9,
            "title": "黄峥当年1350亿蝉联80后白手起家首富:1年中财富每天涨1个亿",
            "source": "胡润百富",
            "cmtcount": 676,
            "img": "http://ajax-api.itheima.net/images/8.webp",
            "time": "2222-10-21 06:19:37"
        }
    ]
}

2.5.3 例三

代理规则

'/weather': {
    target: 'http://hmajax.itheima.net',
    changeOrigin: true,
    rewrite: (path) => {
        return path.replace(/\/weather/, '/api/weather')
    }
}

前端发起的请求URL

axios.get('/weather?city=110100')
    .then((response) => {
      console.log('北京市的天气信息:', response.data)
    })

经过代理后真正发出的请求URL

http://hmajax.itheima.net/api/weather?city=110100

返回的数据

{
    "code": 10000,
    "message": "查询天气成功",
    "data": {
        "date": "2024-05-19",
        "area": "北京市",
        "dateShort": "05月19日",
        "dateLunar": "四月十二",
        "temperature": "23",
        "weather": "晴",
        "weatherImg": "https://hmajax.itheima.net/weather/qingline.png",
        "windPower": "4级",
        "windDirection": "东风",
        "psPm25Level": "良",
        "psPm25": "94",
        "todayWeather": {
            "humidity": "65.0",
            "sunriseTime": "04:56",
            "sunsetTime": "19:27",
            "ultraviolet": "弱",
            "weather": "小雨",
            "temDay": "28",
            "temNight": "17"
        },
        "dayForecast": [
            {
                "date": "05月19日",
                "temDay": "28",
                "weather": "小雨",
                "temNight": "17",
                "windPower": "3-4级",
                "dateFormat": "今天",
                "weatherImg": "https://hmajax.itheima.net/weather/xiaoyu.png",
                "windDirection": "东风"
            },
            {
                "date": "05月20日",
                "temDay": "28",
                "weather": "晴",
                "temNight": "17",
                "windPower": "<3级",
                "dateFormat": "明天",
                "weatherImg": "https://hmajax.itheima.net/weather/qing.png",
                "windDirection": "南风"
            },
            {
                "date": "05月21日",
                "temDay": "31",
                "weather": "晴",
                "temNight": "20",
                "windPower": "<3级",
                "dateFormat": "后天",
                "weatherImg": "https://hmajax.itheima.net/weather/qing.png",
                "windDirection": "南风"
            },
            {
                "date": "05月22日",
                "temDay": "33",
                "weather": "多云",
                "temNight": "21",
                "windPower": "3-4级",
                "dateFormat": "周三",
                "weatherImg": "https://hmajax.itheima.net/weather/duoyun.png",
                "windDirection": "西南风"
            },
            {
                "date": "05月23日",
                "temDay": "32",
                "weather": "多云",
                "temNight": "18",
                "windPower": "3-4级",
                "dateFormat": "周四",
                "weatherImg": "https://hmajax.itheima.net/weather/duoyun.png",
                "windDirection": "东风"
            },
            {
                "date": "05月24日",
                "temDay": "28",
                "weather": "多云",
                "temNight": "18",
                "windPower": "<3级",
                "dateFormat": "周五",
                "weatherImg": "https://hmajax.itheima.net/weather/duoyun.png",
                "windDirection": "西南风"
            },
            {
                "date": "05月25日",
                "temDay": "28",
                "weather": "阴",
                "temNight": "18",
                "windPower": "<3级",
                "dateFormat": "周六",
                "weatherImg": "https://hmajax.itheima.net/weather/yin.png",
                "windDirection": "东南风"
            }
        ]
    }
}

2.5.4 例四

代理规则

'/comment': {
    target: 'https://hmajax.itheima.net',
    changeOrigin: true,
    rewrite: (path) => {
        return path.replace(/\/comment/, '/api/addcmt')
    }
}

前端发起的请求URL

axios.post('/comment', {
  username: "老李",
  content: "大家好!"
}).then((response) => {
  console.log('评论成功:', response.data)
})

经过代理后真正发出的请求URL

https://hmajax.itheima.net/api/addcmt

返回的数据

在这里插入图片描述

3. 在Vue3项目(由Vite构建)中实现代理的原理

在 Vite 项目中,vite.config.js 文件中的代理配置(server.proxy)主要用于开发环境(vite dev),它通过 Vite 的内置开发服务器来实现请求的代理

然而,在生产环境中,Vite 的开发服务器不会被使用,因此代理配置在打包后的代码中不会生效

3.1 代理配置的作用范围(仅适用于开发环境,在生产环境中不会生效)

  • server.proxy 是专门为开发环境设计的功能,用于解决开发阶段的跨域问题
  • server.proxy 依赖于 Vite 的开发服务器(基于 Node.js 的 HTTP 服务)来拦截和转发请求
  • 在生产环境中,Vite 打包后的代码通常会被部署到一个静态文件服务器(如 Nginx、Apache 或其他 CDN),而这些服务器并不具备 Vite 开发服务器的功能,因此代理配置无法生效

3.2 Vite 的运行机制

  • Vite 是一个现代化的前端构建工具,它的开发服务器(vite dev)是基于 Node.js 实现的
  • 在开发阶段,Vite 使用 Node.js 来启动一个本地开发服务器,并提供热更新(HMR)、代理配置等功能
  • 因此,在开发环境中,必须安装 Node.js 才能运行 Vite 的开发服务器

3.3 生产环境的运行机制

  • 在生产环境中,前端代码是静态资源(HTML、CSS、JavaScript 等),它们由 Web 服务器或反向代理服务器(如 Nginx)提供服务
  • 如果需要在生产环境中实现类似的代理功能,则需要在 Web 服务器或反向代理服务器上进行配置

3.4 在生产环境中使用 Nginx 配置代理

在生产环境中,可以通过配置 Nginx 或其他反向代理服务器来实现类似的功能

server {
  listen 80;
  server_name your-domain.com;

  # 静态资源路径
  location / {
    root /path/to/your/dist; # 前端打包后的文件目录
    index index.html;
    try_files $uri /index.html; # 支持前端路由
  }

  # API 代理配置
  location /api {
    proxy_pass http://backend-server-address; # 后端服务地址
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

通过这种方式,Nginx 会在生产环境中接管请求,并将 /api 的请求转发到后端服务

3.5 npm run dev 指令的工作原理

npm run dev 是一个常见的命令,用于启动项目的开发环境,它的原理涉及多个部分,包括 npm、package.json 文件中的脚本配置以及底层工具(如 Vite、Webpack 等)的运行机制


建议观看以下内容之前先阅读我的另一篇博文:package.json文件详解(软件的版本号规则、package.json文件有什么用、package.json文件核心字段讲解、package-lock.json文件、脚本命令的工作原理)

3.5.1 npm run 的作用

  • npm run 是 npm 提供的一个命令,用于执行定义在 package.json 文件中的脚本
  • 具体来说,npm run <script-name> 会查找 package.json 中 scripts 字段下的对应脚本,并执行它
{
  "scripts": {
    "dev": "vite"
  }
}

运行 npm run dev 时,npm 会执行 vite 命令

3.5.2 npm run dev 指令的执行流程

3.5.2.1 查找 package.json 文件中的 scripts 配置
  • 当运行 npm run dev 时,npm 会首先读取项目根目录下的 package.json 文件

  • 它会在 scripts 字段中查找名为 dev 的脚本。例如:

{
  "scripts": {
    "dev": "vite"
  }
}

这里的 dev 脚本定义了要执行的命令:vite

3.5.2.2 解析并执行脚本
  • npm 会解析 dev 脚本的内容(这里是 vite),并尝试找到对应的可执行文件
  • 如果 vite 是一个全局安装的工具,npm 会直接调用它
  • 如果 vite 是项目本地依赖(通常在 node_modules/.bin 目录下),npm 会优先使用本地版本
3.5.2.3 启动开发服务器

在使用 Vite 的情况下,vite 命令会启动 Vite 的开发服务器

开发服务器的主要功能包括:

  • 提供静态资源服务(HTML、CSS、JavaScript 等)
  • 实现模块热更新(HMR,Hot Module Replacement),使得代码修改后无需刷新整个页面
  • 处理代理配置(通过 server.proxy)
  • 支持 ES 模块的按需加载

3.5.3 npm run dev 指令的执行步骤

  1. npm 查找脚本:
    • npm 读取 package.json,找到 scripts.dev 的值为 vite
  2. 解析命令:
    • npm 在项目的 node_modules/.bin 目录中找到 vite 可执行文件
  3. 启动 Vite 开发服务器:
    • Vite 启动开发服务器,默认监听 http://localhost:5173
    • 开发服务器开始处理浏览器的请求,包括静态资源、模块加载和代理配置
  4. 浏览器访问:
    • 打开浏览器访问 http://localhost:5173,Vite 开发服务器会提供 HTML 文件,并动态加载模块
  5. 热更新:
    • 当你修改代码时,Vite 会检测到变化并通知浏览器更新相应的模块

3.6 Vite 开发服务器的工作原理

Vite 是一个现代化的前端构建工具,其开发服务器的设计与传统工具(如 Webpack)有很大不同

3.6.1 原生 ES 模块支持

  • Vite 利用了现代浏览器对原生 ES 模块的支持(<script type="module">)
  • 在开发环境中,Vite 不会对代码进行打包,而是直接将源码作为模块提供给浏览器
  • 浏览器通过 HTTP 请求动态加载模块,Vite 的开发服务器负责拦截这些请求并返回对应的模块内容

3.6.2 按需编译

  • 当浏览器请求某个模块时,Vite 会根据需要对该模块进行即时编译(例如将 TypeScript 编译为 JavaScript)
  • 这种按需编译的方式避免了传统工具(如 Webpack)在开发阶段进行全量打包的性能瓶颈

3.6.3 热更新(HMR)

  • Vite 的开发服务器内置了 HMR 功能
  • 当修改代码时,Vite 会通知浏览器只更新发生变化的模块,而无需刷新整个页面
  • 这大大提高了开发效率,尤其是在大型项目中

3.6.4 代理功能

  • Vite 的开发服务器支持通过 server.proxy 配置代理请求
  • 例如,可以将 /api 的请求代理到后端服务器,从而解决开发阶段的跨域问题

3.7 代理规则的工作机制

在 Vite 的开发环境中,server.proxy 配置的作用是通过 Vite 开发服务器(基于 Node.js)拦截特定的请求,并将这些请求转发到目标服务器

3.7.1 请求必须发送到开发服务器

  • Vite 的代理功能依赖于开发服务器的运行
  • 如果直接向目标服务器发送请求(例如,直接访问 http://target-server.com/api),那么请求不会经过 Vite 开发服务器,因此代理规则不会生效

运行 npm run dev 指令之后可以看到开发服务器占用了哪个端口(默认占用 5173 端口)

在这里插入图片描述

3.7.2 代理规则匹配请求路径

  • 代理规则会根据请求的路径(如 /api)进行匹配
  • 如果请求发送到开发服务器(例如 http://localhost:5173/api),并且路径符合代理规则(如 /api),那么开发服务器会拦截该请求并将其转发到目标服务器

3.7.3 确保开发环境使用代理

在开发环境中,前端代码应该始终通过开发服务器发送请求。例如:

fetch('/api/users')
  .then(response => response.json())
  .then(data => console.log(data))

这里的 /api/users 是相对路径,默认会发送到开发服务器(http://localhost:5173/api/users),从而触发代理规则

4. 补充:为什么直接运行vite指令报错,而运行 npm run dev 指令却正常

如果你在项目的根目录下直接运行 vite 命令,大概率会遇到以下报错

在这里插入图片描述

vite : 无法将“vite”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确
,然后再试一次。
所在位置 行:1 字符: 1
+ vite
+ ~~~~
    + CategoryInfo          : ObjectNotFound: (vite:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

为什么直接运行 vite 指令报错,而运行 npm run dev 指令却正常呢,这两个指令本质上不是一样的吗


建议观看以下内容之前先阅读我的另一篇博文:package.json文件详解(软件的版本号规则、package.json文件有什么用、package.json文件核心字段讲解、package-lock.json文件、脚本命令的工作原理)

4.1 直接运行 vite 失败的原因

4.1.1 vite 没有全局安装

  • 如果你直接在终端中运行 vite,系统会尝试查找一个名为 vite 的可执行命令
  • 如果 vite 没有全局安装(即没有通过 npm install -g vite 安装到全局环境),系统就找不到这个命令,从而报错

4.1.2 本地安装的 vite 不在 PATH 中

  • 在大多数项目中,vite 是作为项目的开发依赖安装的(通过 npm install vite --save-dev)
  • 这种情况下,vite 被安装到了项目的 node_modules/.bin 目录下,而不是全局环境
  • 系统的 PATH 环境变量通常不会包含项目的 node_modules/.bin 目录,因此直接运行 vite 会失败

4.1.3 手动指定路径运行

如果不想使用 npm run dev,也可以手动指定 vite 的路径来运行它。例如:

在 Linux/MacOS 上

./node_modules/.bin/vite

在 Windows 上:

.\node_modules\.bin\vite

这样可以直接调用本地安装的 vite

4.2 npm run dev 为什么可以运行(npm run 自动添加 node_modules/.bin 到 PATH)

当运行 npm run <script> 时,npm 会自动将项目的 node_modules/.bin 目录临时添加到系统的 PATH 环境变量

这意味着,在 npm run dev 执行期间,系统能够找到 node_modules/.bin/vite,即使它没有全局安装


上面说到“运行 npm run <script> 时,npm 会自动将项目的 node_modules/.bin 目录临时添加到系统的 PATH 环境变量中”

但 npm 并不是去修改系统的全局环境变量(比如你在 Windows 的“系统属性”或 Linux/macOS 的 ~/.bashrc、~/.zshrc 文件中设置的 PATH),如果那样做,会非常危险且混乱


当运行 npm run dev 指令时,npm 会创建一个新的子进程(通常是 shell,比如 Linux/macOS 下的 sh 或 Windows 下的 cmd.exe),脚本命令(比如 vite)是在这个子进程中执行的,而不是在当前的终端主进程中执行的

在启动这个子进程之前,npm 会做一件关键的事情:构建一个全新的 PATH 环境变量,并把全新的 PATH 环境变量只传递给这个子进程


上述只是 npm run dev 指令执行过程的简单描述,具体的执行过程可以参考我的另一篇博文:package.json文件详解(软件的版本号规则、package.json文件有什么用、package.json文件核心字段讲解、package-lock.json文件、脚本命令的工作原理)

4.3 为什么 npm run dev 指令更常用

4.3.1 无需全局安装工具

  • 通过 npm run dev,你可以直接使用项目本地安装的工具(如 vite),而不需要全局安装
  • 这种方式更符合现代前端开发的最佳实践,避免了不同项目之间的版本冲突

4.3.2 跨平台兼容性

  • npm run 提供了一种跨平台的方式运行脚本,无论是在 Linux、macOS 还是 Windows 上,都可以正常工作
  • 如果直接运行 ./node_modules/.bin/vite,在 Windows 上需要改为 .\node_modules\.bin\vite,这会导致脚本不够通用

4.3.3 简化开发流程

  • 开发者只需要记住 npm run dev,而不需要关心底层工具的具体路径或安装方式
  • 这使得团队协作更加高效,减少了环境配置的复杂性

4.4 如何直接运行 vite(了解即可,不推荐使用)

如果你确实希望直接运行 vite,可以采取以下方法:

4.4.1 全局安装 vite

npm install -g vite

全局安装后,你可以在任何地方直接运行 vite 命令

4.4.2 修改系统的环境变量

将 node_modules/.bin 添加到系统的 PATH 环境变量中,就可以直接运行 vite 了

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐