Vue 项目之 Webpack 中 devServer 的 proxy 配置(1)

381 阅读8分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

1. devServerproxy 配置

  • proxy 是我们开发中非常常用的一个配置选项,它的目的是设置代理来解决跨域访问的问题:

    • 比如假如我们现在通过 http://localhost:8010 请求到了本地服务器中的静态资源,如果在这些静态资源中我们还发送了一个网络请求,请求 http://localhost:8000 这个地址下的内容,也就是说在当前 http://localhost:8010 这个地址下去访问 http://localhost:8000 这个地址,这时就会出现跨域的问题,没办法正常地响应,会报跨域的错误。解决这种跨域问题的方法有很多,比如有以下几种:

      1. 把静态资源和 api 部署在一起,即把静态资源和 api 都部署在同一个服务器(比如 Tomcat/express/koa 服务器)上的同一端口下,这时就不会存在跨域问题;
      2. 直接在服务器上把跨域关闭掉,但是通常情况下,为了安全起见,服务器上不会关闭跨域;
      3. 做一个 nginx 代理,之后不管是静态资源还是 api,都是通过这个 nginx 进行访问,再由 nginx 去访问静态资源和 api

      但不管怎样,真实开发中当项目上线时,如果要解决跨域问题,一般都需要后端来配合(不管是部署到一块,还是关掉跨域,亦或是通过 nginx 来部署,都是和后端有关的)。但是在我们开发阶段遇到这种跨域问题,后端一般是不会帮我们解决的,因为到部署阶段,我们可能用 nginx 来解决跨域,而在开发阶段,我们可以通过配置 devServer 中的 proxy 来解决跨域问题。

    • 简单总结上面的例子:

      • 比如我们的一个 api 请求的是 http://localhost:8000,但是本地启动的服务器的域名是 http://localhost:8010,这个时候发送网络请求就会出现跨域的问题;
      • 那么我们可以将请求先发送到一个代理服务器,代理服务器和 API 服务器没有跨域的问题,就可以解决我们的跨域问题了;
  • 我们可以进行如下的设置:

    • target:表示的是代理到的目标地址,比如 /api-zhj/moment 会被代理到 http://localhost:8000/api-zhj/moment
    • pathRewrite:默认情况下,我们的 /api-zhj 也会被写入到 URL 中,如果希望它不被写入,则可以通过 pathRewrite 对路径进行重写;
    • secure:默认情况下不接收转发到 https 的服务器上,如果希望支持,可以设置为 false
    • changeOrigin:是否修改源,表示是否更新代理后请求的 headers 中的 host 地址,我们一般会将其设置为 true
      • 这个 changeOrigin 官方文档中说得有点模糊,通过查看源码可以发现这个 changeOrigin 其实是要修改代理请求中的 headers 中的 host 属性:
        • 因为我们真实的请求,其实是需要通过比如 http://localhost:8000 来请求的;
        • 但是因为使用了代码,默认情况下它的值是 http://localhost:8010
        • 如果我们需要修改,那么就可以将 changeOrigin 设置为 true

下面,我们就来演示一下如何配置 devServer.proxy

为了方便演示,我们先来安装一个常用的网络请求库:axios

npm install axios

搭建本地接口服务器

这里为了成功发送一个网络请求,就需要一个对应的服务器,我们从 GitHub 上下载一个王红元老师开发好的本地服务器

git clone https://github.com/coderwhy/coderhub.git

然后,我们进入这个项目的目录下:

cd coderhub/

安装依赖:

npm install

在项目目录下新建文件名为 .env 的文件,在该文件中添加如下内容(注意配置 MYSQL_PASSWORD 的值):

APP_HOST=http://localhost
APP_PORT=8000

MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_DATABASE = coderhub
MYSQL_USER = root
MYSQL_PASSWORD = 你的MySQL数据库root用户密码

注:本地需要安装 MySQL 数据库。并且,你需要创建 coderhub 数据库:

CREATE DATABASE IF NOT EXISTS `coderhub`;

然后在 coderhub 数据库中创建以下数据表:

CREATE TABLE IF NOT EXISTS `user`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(30) NOT NULL UNIQUE,
  password VARCHAR(50) NOT NULL,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  avatar_url VARCHAR(200)
);

CREATE TABLE IF NOT EXISTS `moment`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  content VARCHAR(1000) NOT NULL,
  user_id INT NOT NULL,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY(user_id) REFERENCES user(id)
);

CREATE TABLE IF NOT EXISTS `comment`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  content VARCHAR(1000) NOT NULL,
  moment_id INT NOT NULL,
  user_id INT NOT NULL,
  comment_id INT DEFAULT NULL,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  FOREIGN KEY(moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY(comment_id) REFERENCES comment(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE IF NOT EXISTS `label`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(10) NOT NULL UNIQUE,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS `moment_label`(
  moment_id INT NOT NULL,
  label_id INT NOT NULL,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY(moment_id, label_id),
  FOREIGN KEY (moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (label_id) REFERENCES label(id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE IF NOT EXISTS `avatar`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  filename VARCHAR(255) NOT NULL UNIQUE,
  mimetype VARCHAR(30),
  size INT,
  user_id INT,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE
);


CREATE TABLE IF NOT EXISTS `file`(
  id INT PRIMARY KEY AUTO_INCREMENT,
  filename VARCHAR(100) NOT NULL UNIQUE,
  mimetype VARCHAR(30),
  size INT,
  moment_id INT,
  user_id INT,
  createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE
);

启动项目:

npm run start

项目启动成功后,你可以像下面这样通过 POST 方式调用 localhost:8000/users 接口注册一个用户:

image-20211207225700874

然后,你可以用该用户去发布动态了。但在此之前,你需要像下面这样先根据用户名和密码获取 token

image-20211208060727130

之后,将获取到的 token 值添加到请求头中:

image-20211208061131111

并在 body 中添加你想发布的动态内容:

image-20211208061237596

动态发布成功后,你可以像下面这样通过 localhost:8000/moment?offset=0&size=10 接口查询当前用户发布过的动态(如果动态数超过 10 条,则只查询 10 条):

image-20211208061704626

发送网络请求

有了可以调用接口获取数据的本地服务器后,我们将该服务器保持开启状态,然后在浏览器中访问 http://localhost:8000/moment?offset=0&size=10 这个地址:

image-20211210211133274

可以看到,我们成功从本地服务器中请求下来了 2 条数据,那么我们把这一请求的路径复制下来,来到 src/main.js 中,导入 axios,然后通过它发送一个 get 请求,请求的路径就用我们刚才在浏览器中的请求路径:

import { createApp } from 'vue';
import axios from 'axios';

...

// axios.get() 返回的是一个 Promise,所以我们可以直接 .then()
axios.get('http://localhost:8000/moment?offset=0&size=10').then(res => {
  console.log(res);
}).catch(err => {
  console.log(err);
})

同时,为了避免端口号冲突,我们再将 devServer 原来的端口号修改掉(比如我们修改为 8010):

devServer: {
  ...
  port: 8010,
  ...  
},

再来重新运行 npm run serve 命令,重新启动当前的项目(到时候打包的代码中就有我们上面添加的发送网络请求的代码了):

image-20211210212201636

然后在浏览器中查看效果:

image-20211210212358402

你会发现,浏览器控制台报错了。这是因为我们当前是在 http://localhost:8010/ 这个源下面请求 http://localhost:8000/moment?offset=0&size=10 这个源中的内容,而这两个源的端口号不同,所以它们不是相同的源,因此会在浏览器中出现跨域访问的问题。

这时,我们可以通过前面提到的 3 种方案来解决这一跨域问题,而它们都需要服务器那边进行相关配置,但我们现在处于开发阶段,服务器那边可能无法配合我们进行相关配置来解决跨域问题,这时我们就可以通过 webpack-dev-serverproxy 进行配置来解决开发阶段的浏览器跨域问题。

我们来到 webpack 的配置文件中,对 devServer 中的 proxy 进行配置,配置完 proxy 后,就相当于做了一个代理:到时候项目中发送的网络请求就都会交给 devServer,让它来帮我们发送(相当于原来由浏览器向目标服务器发送网络请求,改为现在由 devServer 这台 express 服务器向目标服务器发送网络请求,一台服务器向另一台服务器发送网络请求时是没有跨域问题的,跨域问题是浏览器同源策略的限制导致的),而等到 devServer 这台服务器请求到数据后,再由它返回给我们代码中真正发送网络请求的地方,最终就可以成功获取数据了。所以我们可以通过 devServer 开启一个本地的代理(当然,这只是开发阶段的代理,真正部署时,还是会有跨域问题,所以到时候还需要和后端配合,一起解决部署时的跨域问题)。

CORS

CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing),它是一种机制,用来让网页的受限资源能够被其它域名的页面访问,以避开浏览器的同源策略(Same-origin policy)。

“源”(origin)指的是由“协议名”(URI scheme)、“主机名”(host name)和“端口号”(port number)这三部分组成的组合体。当这三个部分都相同时就称为“同源”1

Footnotes

  1. 参考资料:developer.mozilla.org/en-US/docs/…www.ruanyifeng.com/blog/2016/0…en.wikipedia.org/wiki/Cross-…en.wikipedia.org/wiki/Same-o…zh.wikipedia.org/wiki/%E8%B7…