正式的发起请求以前,我们先了解一下前后台分离应用的典型工作模式。
# 工作模式
在前后台分离的应用中,最典型最简单的模式如下图所示:
![](https://img.kancloud.cn/a2/8b/a28ba6d01e98ebeebbdc5eafb431df3b_430x450.png)
对于我们当前的应用而言,程序执行流程如下:
① 浏览器首先访问服务于4200端口的Angular获取前端的代码。
② 获取后执行前端的代码,前端的代码在执行的过程中向服务于8080端口的Spring Boot发起请求。
③ Spring Boot将数据返回给前端。
④ 前端获取数据后处理后显示给用户。
# 发起请求
## 启动后台
发起后台请求前,我们需要使用命令行模式来启动后台。打开shell,进入项目文件夹,使用`mvn spring-boot:run`命令来启动后台。当看到以下信息时说明程序启动成功:
```
2019-09-26 14:06:54.997 INFO 30922 \--- \[ main\] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-26 14:06:55.002 INFO 30922 \--- \[ main\] c.y.helloWorld.HelloWorldApplication : Started HelloWorldApplication in 2.292 seconds (JVM running for 6.368)
```
最后两条日志信息给我们了两个有效的信息:1. 后台服务运行的端口号;2. 系统成功启动了。
## 发起请求
常见的请求方式有`get`、`post`两种,`get`请求一般用于获取数据,`post`请求一般用于提交数据。很明显,此时向服务器发起请求用`get`更合适。
`HttpClient`发起请求并在控制台打印返回数据。
~~~
import {Component} from '@angular/core';
import {HttpClient} from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass']
})
export class AppComponent {
constructor(private httpClient: HttpClient) {
// 向8080端口的helloWorld路径发起请求
httpClient.get('http://localhost:8080/helloWorld') ➊
.subscribe(success, error); ➋
}
title = 'hello-world';
}
/**
* 在控制台打印传入值
* @param data 任意数据
*/
function success(data) { ➌
console.log('请求成功');
console.log(data);
}
/**
* 在控制台打印传入值
* @param data 任意数据
*/
function error(data) { ➍
console.log('请求失败');
console.log(data);
}
~~~
➊ 调用`HttpClient`的`get`方法,接收的参数类型为`string`,值为:'[http://localhost:8080/helloWorld',表示向'http://localhost:8080/helloWorld'发送一个](http://localhost:8080/helloWorld'%EF%BC%8C%E8%A1%A8%E7%A4%BA%E5%90%91'http://localhost:8080/helloWorld'%E5%8F%91%E9%80%81%E4%B8%80%E4%B8%AA)`get`请求。该方法返回一个对象➀。
➋ 调用➊中返回的对象➀中的`subscribe`方法,该方法接收2个参数,类型均为`function`。当发起请求成功时调用函数➌,并将请求成功的数据及请求信息传给函数➌中的参数`data`;当发起请求失败时调用函数➍,并将请求失败的数据及请求信息传给函数➍中的参数`data`。
## 测试
保存代码后,浏览器自动刷新。打开控制台并查看运行结果:
![](https://img.kancloud.cn/74/2e/742ed41bbb3c8c918a0f94ba05a0838f_1863x149.png)
和我们的预想不一致,竟然报错了。
>[warning] 在开发的道路上,报错永远会伴随我们的左右,这是再正常不过的事情了。错误每出现一次我们便会成长一次,所以我们应该报着开放、包容的态度来迎接错误。错误的类型有千百中,解决的方式也各有不同,在教程中我们力求将团队长年开发中总结出的解决问题的方法与你共同交流。相信去其糟粕后总会有所精华适合你。我们一直说编程学习的是思想、也不是语言。思想有了,语言只是工具。
# 排错
## 翻译
切记遇到问题第一点是将错误译成中文,当然了如果你的英文水平不错,那么直接读英文就可以了。
>[info] 当我们用英语进行交流时,不需要在大脑中进行汉英、英汉转化时,就已经入门了。
英:
```
Access to XMLHttpRequest at 'http://localhost:8080/helloWorld' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
译
```
由源地址'http://localhost:4200'向'http://localhost:8080/helloWorld' 发起的XMLHttp请求被CORS策略阻止了:请求的资源上没有提供'Access-Control-Allow-Origin' 头信息
```
意思大体是说:我们当前访问的页面是'[http://localhost:4200](http://localhost:4200)',然后这个页面又向'[http://localhost:8080/helloWorld](http://localhost:8080/helloWorld)' 这个地址发起了访问(我们刚刚使用httpClient干的这件事情),由于CORS策咯是要求'[http://localhost:8080/helloWorld'在返回的数据上的](http://localhost:8080/helloWorld'%E5%9C%A8%E8%BF%94%E5%9B%9E%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%8A%E7%9A%84)`header`提供'Access-Control-Allow-Origin'信息的,由于它并没有提供所以浏览器阻止了这个操作。
那么如何查看返回数据的header信息呢?看这里:
![](https://img.kancloud.cn/2d/42/2d42872e0b3a3bb31e76a3b2712f53c4_988x445.png)
我们在`Response Heaser 响应信息`中找到了`Content-Length`、`Content-Type`以及`Date`,但并未找到`Access-Control-Allow-Origin`
既然大体的方向有了那么我们根据关键字查询下好了。
## 查询关键字
查询关键字有几种境界:第一种属于起步境界。既然是起步那么当然很简单很暴力了,我们把控制台打印的错误信息直接输入查询框即可:
![](https://img.kancloud.cn/92/85/928509544ac79d39fb01527a283d803f_1335x138.png)
很多时候这个方法还就真的很灵,适用的就是最好的,此境界在google的帮助下可以为我们解决大多数的问题。很不巧,这个问题恰巧属于运气不太好的。
> 请不要怀疑google的检索能力,如果一个关键字输入进去在结果的第1页中却没有找到自己想要的答案,那么请果断变更关键字进行下一次搜索。
当第一种直接搜索的方法解决不了问题时,那么就需要加入一些关键字了,比如我们的应用是在anuglar + spring boot下报的错,那么就可以试试:
angular + 错误信息的方法:
![](https://img.kancloud.cn/b3/14/b3141cd7124867b4f72000d2e959b849_1450x455.png)
一旦google出现了`Did you mean`字样,一般而言都是我们的表达习惯和与英文为母语的人的差异造成的。不要犹豫,点击`Did you mean`后面的链接跳转到正确的方法即可。此时,离我们成功解决问题就更近了一步。
如果前两种方法都解决不了问题,那么就需要我们静下心来进行基础的补缺及分析了。比如我们在前面其实已经分析过了:这个原因是由于后台没有在`header`中返回`Access-Control-Allow-Origin`造成的,加之我们后台使用的是`Spring Boot`,所以最终我们的关键为:`spring boot response Access-Control-Allow-Origin`
![](https://img.kancloud.cn/8b/6f/8b6f818b763326862f48ec63da9eca1d_1231x730.png)
点击[第一条](https://howtodoinjava.com/spring5/webmvc/spring-mvc-cors-configuration/),答案映入眼帘。这篇文章不止给出了N种解决的方法,而且还对CORS是什么进行简单的解释。
## 照抄代码
在此我们暂时使用`1.2. @CrossOrigin at Class/Controller Level` 方案,即在控制器上添加`@CorssOrigin`注解 。
使用`IDEA`打开后台项目:
HelloWorldApplication
~~~
package club.yunzhi.helloWorld;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;➊
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*", allowedHeaders = "*")➋
@SpringBootApplication
@RestController
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
@RequestMapping("helloWorld")
public String helloWorld() {
return "Hello Spring";
}
}
~~~
➊ 使用前先引入
➋ 对被注解的类进行CORS配置,允许请求的地址为\*(所有地址),允许请求的方法为\*(所有方法)
## 测试
重新启动后台后,发生请求并查看网络及控制台信息:
![](https://img.kancloud.cn/a9/19/a919fdf552266a62e088194a47f1a8eb_685x165.png)
Header中返回了Access-Control-Allow-Origin,这时候控制台不应该报错了吧。
![](https://img.kancloud.cn/8d/c5/8dc58ac8bd7dc5197088ed7c0c11b80f_1116x168.png)
的确异常没有了,现在迎来了新的错误:请求失败`HttpErrorResponse`。
# 排错2
如果你足够细心的话会发现只所以在控制台中发现的了打印的错误信息,其实是由于执行了我们前台输入的代码段,在控制台中我们可以点击最右侧的文件名来快速的定位到打印日志的代码:
![](https://img.kancloud.cn/17/8f/178f8e81866f33fb4e0ae3e0037e068b_1274x148.png)
点击后跳转到了Sources选项卡:
![](https://img.kancloud.cn/bf/fc/bffc20d80cd7fffbc0b92165b9e03d7f_271x85.png)
## 翻译
在控制台中展开HttpErrorResponse,让我们看看具体发生了什么错误:
![](https://img.kancloud.cn/43/3a/433ad4ef7235e946b9e5934e5c3d65e5_1323x357.png)
`SyntaxError: Unexpected token H in JSON at position 0 at JSON.parse`
`语法错误:在执行JSON.parse时于JSON(串)的位置0上遇到了非期待(不认识的,不知道怎么处理的)的标识H`
有点经验的程序员都知道:`SyntaxError`语法错误往往会发生在拼写错误上,可`JSON.parse`并不是我们维护的,又怎么会发生拼写错误呢?
## 搜索
既然没有头绪,那么碰碰运气好了:
![](https://img.kancloud.cn/14/9a/149a0e5c3c02a69afea71acedef50c5a_1177x384.png)
提出问题的场景和我们好像不一样,但如果细心看看答案的话,也是可以收获满满。
*****
`products`is an object. (creating from an object literal)
`products`是一个对象
`JSON.parse()`is used to convert a**string**containing JSON notation into a Javascript object.
`JSON.parse()`用来将一个用JSON字符串转换为Javascript对象的。
Your code turns the object into a string (by calling`.toString()`) in order to try to parse it as JSON text.为了执行`JSON.parse()`,你的代码为通过`products.toString()`来获取一个字符串,然后再尝试执行`JSON.parse()`
The default`.toString()`returns`"[object Object]"`, which is not valid JSON; hence the error.
但是,在执行`products.toString()`返回了如下字符串:`"[object Object]"`,这个字符串并不是一个有效的JSON字符串。因此错误便发生了。
*****
>[warning] 如果你还不清楚什么JSON字符串与JSON对象,那么简单了解下:[JSON字符串与JSON对象](https://www.jianshu.com/p/4b0bb59f585f)
于是,我们结合前面控制台的报错,大概明白什么原因了:原来Angular在获取后台返回的数据的时候,会尝试将返回的数据转换为JSON对象。这个转换过程是通过执行`JSON.parse`来完成的。当我们的后台返回`Hello Spring`时,就相当于执行了`JSON.parse('Hello Spring')`,而'Hello Spring'并不是一个合法的JSON字符串,所以错误发生了。
```
语法错误:在执行JSON.parse时于JSON(串)的位置0上遇到了非期待(不认识的,不知道怎么处理的)的标识H
```
上述错误发生时,JSON(串)的值是`Hello Spring`,在`Hello Spring`的位置0上的字符是`H`.
## 解决问题
清楚了问题产生的本源,解决起来就比较轻松了,既然只能返回JSON格式的字符串,我们修改一下返回数据便可以了:
~~~
@RequestMapping("helloWorld")
public String helloWorld() {
return "{\"message\": \"Hello Spring\"}";
}
~~~
>[success] `\"`为java中的转义符,代表输出`"`。
## 测试
重新启动后台,刷新网页并查看控制台:
![](https://img.kancloud.cn/24/e2/24e2a83e8f19ebd9d9ea7ade2dcc46f5_426x124.png)
# 主要数据流如下
![](https://img.kancloud.cn/70/94/7094eb3f520ef36c913a960beacc0bcf_800x600.gif)
# 本节小测
✓ 请尝试参考`HttpClient`写一个类`Yunzhi`,实现以下功能:
```js
constructor(private httpClient: HttpClient) {
const yunzhi = new Yunzhi();
// 当get中的参数值为a时,调用success方法,并将data数据设置为`hello success`
yunzhi.get('a')
.subscribe(success, error);
// 当get中的参数值为其它值时,调用error方法,并将data数据设置为`hello error`
yunzhi.get('b')
.subscribe(success, error);
}
/**
* 在控制台打印传入值
* @param data 任意数据
*/
function success(data) {
console.log('请求成功');
console.log(data);
}
/**
* 在控制台打印传入值
* @param data 任意数据
*/
function error(data) {
console.log('请求失败');
console.log(data);
}
class Yunzhi {
// 请补充功能代码
}
```
![](https://img.kancloud.cn/23/e7/23e76ba018024a8b3b97b04dcb892f7c_298x102.png)
## 上节答案:
✓ 在现实生活中:
租车公司有提供了汽车的能力,所以我们需要汽车时,需要找租车公司来帮忙,而不是找汽车来帮忙。(想直接找汽车来帮忙?那。。。你怎么联系汽车呢?)
同理,我把上面的租车公司换成`HttpClientModule`,把`汽车`替换为`HttpClient`后,便是答案了:
`HttpClientModule`有提供`HttpClient`的能力,所以我们需要`HttpClient`时,需要找`HttpClientModule`来帮忙,而不是找`HttpClient`来帮忙。
再看报错信息 ---- `No provider for HttpClient!`:
注意:不是`Not found for HttpClient!` 。不是找不到`HttpClient`,而是找不到提供`HttpClient`的人,而`HttpClientModule`正式提供`HttpClient`的人。
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用