既然是常用的功能,那么spring必然已經(jīng)有了最佳實(shí)踐。在進(jìn)行最佳實(shí)踐前,我們來簡(jiǎn)單匯制下時(shí)序圖:

考慮到該功能實(shí)現(xiàn)的復(fù)雜性,我們?cè)诖耸褂妹艚蓍_發(fā)(agile development)的方法,先開發(fā)分頁功能,再開發(fā)綜合查詢功能。
# CurdRepository
無論經(jīng)過多少次轉(zhuǎn)發(fā),最終實(shí)現(xiàn)數(shù)據(jù)分頁查詢的必然是倉庫層。StudentRepository繼承了CurdRepository,進(jìn)行spring為其自動(dòng)實(shí)現(xiàn)了一些基本的增改查刪的功能。我們打開CurdRepository來簡(jiǎn)單瀏覽一下這個(gè)文件:
CurdRepository
```
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S var1); ?
<S extends T> Iterable<S> saveAll(Iterable<S> var1); ?
Optional<T> findById(ID var1); ?
boolean existsById(ID var1); ?
Iterable<T> findAll(); ?
Iterable<T> findAllById(Iterable<ID> var1); ?
long count(); ?
void deleteById(ID var1); ?
void delete(T var1); ?
void deleteAll(Iterable<? extends T> var1); ?
void deleteAll(); ?
}
```
* ? 新增/更新功能
* ? 查詢功能
* ? 刪除功能
通過查看我們發(fā)現(xiàn)其提供的查詢功能中并沒有找到我們需要的分頁功能。的確是這樣,在spring中CurdRepository只提供了基本的增改查刪功能,如果想實(shí)現(xiàn)更復(fù)雜的分頁功能,則需要繼承其它的接口。
# PagingAndSortingRepository
spring為我們提供了`org.springframework.data.repository.PagingAndSortingRepository;`來滿足對(duì)分頁功能的需求,要想使用此接口給我們帶來的功能,只需要繼承該接口即可.
repository/StudentRepository.java
```
package com.mengyunzhi.springBootStudy.repository;
import com.mengyunzhi.springBootStudy.entity.Student;
import org.springframework.data.repository.PagingAndSortingRepository; ①
/**
* 學(xué)生
*/
public interface StudentRepository extends PagingAndSortingRepository<Student, Long>② {
}
```
* ① 使用前先引入
* ② 和CrudRepository相同,繼承該接口時(shí),需要指定實(shí)體類型及實(shí)體的主健類型
此時(shí),我們應(yīng)該有個(gè)疑問:在歷史的代碼中,我們是通過間接調(diào)用CrudRepository的save方法來完成的數(shù)據(jù)新增功能。而當(dāng)前修改了繼承的接口,那么以前代碼中間接調(diào)用CrudRepository.save方法還可以正常工作嗎?為此,我們借助idea來看一下當(dāng)前接口的繼承關(guān)系:

依圖所示,StudentRepository繼承了PagingAndSortingRepository,PagingAndSortingRepository又繼承了CrudRepository。因而我們?cè)跉v史的代碼中書寫的學(xué)生保存的相關(guān)功能性代碼仍然可用。在調(diào)用studentRepository的save方法時(shí),它會(huì)按照繼承的原則:此類沒有則轉(zhuǎn)向父類、父類沒有則轉(zhuǎn)向父父類,依此累推,最終仍然會(huì)調(diào)用到CrudRepository的save方法。
PagingAndSortingRepository中有兩個(gè)方法:
```
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort var1); ?
Page<T> findAll(Pageable var1); ?
}
```
* ? 接收的參數(shù)類型為**排序**,返回值為**迭代器**,**迭代器**可以認(rèn)為是數(shù)組的一種,與數(shù)組不同的是:我們獲取數(shù)組中的子項(xiàng)時(shí),不能夠再使用索引的方法,而只能使用其它特定的方法。
* ? 接收的參數(shù)類型為\*\*(可)分頁\*\*,返回值為**含有總頁數(shù)及當(dāng)前頁數(shù)組的特定類型**。Page類型除包含總頁數(shù)、當(dāng)前頁數(shù)據(jù)外,還包含了第幾頁、每頁大小、總條數(shù)、排序規(guī)則、是否首頁、是否尾頁、是否還有下一頁、是否還有上一頁等其它的與分頁相關(guān)的信息。
# 獲取分頁數(shù)據(jù)
要想獲取分頁數(shù)據(jù),首先需要獲取一個(gè)實(shí)現(xiàn)了Pageable接口的對(duì)象,該對(duì)象可使用`Pageable pageable = PageRequest.of(page, size)`來初始化。比如我們想獲取每頁10條情況下,第1頁的數(shù)據(jù)則可以使用如下的方法:
repository/StudentRepositoryTest.java(請(qǐng)新建)
```
package com.mengyunzhi.springBootStudy.repository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page; ①
import org.springframework.data.domain.PageRequest; ②
import org.springframework.data.domain.Pageable; ③
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentRepositoryTest {
@Autowired
StudentRepository studentRepository;
@Test
public void page() {
Pageable pageable = PageRequest.of(0, 10); ?
Page<Student> studentPage = studentRepository.findAll(pageable); ?
return; ?
}
}
```
* ①②引入特定的類,由于有多個(gè)重名的類,所以在此處需要注意該類的位置。
* ? 初始化第0頁、每頁10條的分頁查詢條件
* ? 查詢分頁數(shù)據(jù)
* ? 加個(gè)冗余的return用于debug程序
接著我們?cè)诖颂幋騻€(gè)斷點(diǎn):

然后用debug模式啟動(dòng)該單元測(cè)試

并展開studentPage如下:

上圖所示,返回值Page中含有:
* 數(shù)據(jù)總條數(shù)0
* 當(dāng)前面數(shù)據(jù)content
* 分頁信息pageable
* 當(dāng)前為第0頁
* 每頁10條數(shù)據(jù)
將如上對(duì)象直接返回給前臺(tái),完全可以滿足我們的當(dāng)前需求。
## 數(shù)據(jù)測(cè)試
接下來,我們?cè)跍y(cè)試中加入測(cè)試數(shù)據(jù),再次debug看看實(shí)際的返回值
```
@Autowired
KlassRepository klassRepository;
@Test
public void page() {
Klass klass = new Klass();
klass.setName("testKlass");
this.klassRepository.save(klass);
for (int i = 0; i < 100; i++) {
Student student = new Student();
student.setName(RandomString.make(4));
student.setSno(RandomString.make(6));
student.setKlass(klass);
this.studentRepository.save(student);
}
Pageable pageable = PageRequest.of(2, 15);
Page<Student> studentPage = studentRepository.findAll(pageable);
return;
}
```
再次debug中斷查看:

查看content:

如此,我們便有了實(shí)現(xiàn)數(shù)據(jù)分頁功能的基礎(chǔ)。
# M層
當(dāng)我們第一次使用某個(gè)功能的時(shí)候(還處于解決技術(shù)障礙中),我們的首頁目標(biāo)是借助于debug來弄清楚該功能的具體使用方法,傳入值與返回的類型等,單元測(cè)試應(yīng)該放在后面進(jìn)行補(bǔ)充。TDD測(cè)試驅(qū)動(dòng)開發(fā)僅限于我們對(duì)某個(gè)功能的實(shí)現(xiàn)不存在技術(shù)上的障礙時(shí)。在此,我們先完成M層的功能部分,再對(duì)應(yīng)進(jìn)行測(cè)試代碼的編寫.
service/StudentService.java
```
/**
* 查詢分頁信息
*
* @param pageable 分頁條件
* @return 分頁數(shù)據(jù)
*/
Page<Student> findAll(Pageable pageable);
```
service/StudentServiceImpl.java
```
@Override
public Page<Student> findAll(Pageable pageable) {
return this.studentRepository.findAll(pageable);
}
```
## 單元測(cè)試
按前面的經(jīng)驗(yàn), 整理單元測(cè)試代碼如下:
service/StudentServiceImplTest.java
```
/**
* 分頁查詢
* 1. 模擬輸入、輸出、調(diào)用studentRepository
* 2. 調(diào)用測(cè)試方法
* 3. 斷言輸入與輸出與模擬值相符
*/
@Test
public void findAll() {
Pageable mockInPageable = PageRequest.of(1, 20); ①
List<Student> mockStudents = Arrays.asList(new Student()); ②
Page<Student> mockOutStudentPage = new PageImpl<Student>(
mockStudents,
PageRequest.of(1, 20),
21); ?
Mockito.when(this.studentRepository.findAll(Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage); ④
Page<Student> studentPage = this.studentService.findAll(mockInPageable); ⑤
Assertions.assertThat(studentPage).isEqualTo(mockOutStudentPage); ⑥
ArgumentCaptor<Pageable> pageableArgumentCaptor = ArgumentCaptor.forClass(Pageable.class);
Mockito.verify(this.studentRepository).findAll(pageableArgumentCaptor.capture());
Assertions.assertThat(pageableArgumentCaptor.getValue()).isEqualTo(mockInPageable); ⑦
```
* ① 模擬輸入
* ② 初始化返回分頁信息的本頁數(shù)據(jù)部分
* ③ 使用 本頁數(shù)據(jù)、 分頁情況、總條件來初始化模擬返回值Page<Student>
* ④ 模擬studentRepository.findAll方法的返回值
* ⑤ 調(diào)用被測(cè)試方法
* ⑥ 斷言返回值
* ⑦ 斷言傳入?yún)?shù)

# C層
C層的代碼也很簡(jiǎn)單:
controller/StudentController.java
```
@GetMapping
public Page<Student> findAll(@RequestParam int page, @RequestParam int size) {
return this.studentService.findAll(PageRequest.of(page, size));
}
```
## 單元測(cè)試一
為了更清楚的了解真實(shí)情況的返回值,我們暫且將單元測(cè)試中StudentService的注解由@MockBean改為@Autowired,然后模擬添加一些數(shù)據(jù),看看真實(shí)情況下會(huì)給我們返回什么樣的數(shù)據(jù)(注意:這違背了單元測(cè)試的原則。在單元測(cè)試中,我們的測(cè)試內(nèi)容應(yīng)該圍繞輸入與輸出展開。對(duì)于被測(cè)試方法在執(zhí)行期間調(diào)用其它的方法的,應(yīng)該使用MOCK來進(jìn)行模擬)。
contoroller/StudentControllerTest.java
```
@Autowired ?
@MockBean ?
private StudentService studentService;
@Autowired
private KlassRepository klassRepository; ①
@Autowired
private StudentRepository studentRepository; ①
@Test
public void findAll() throws Exception {
logger.info("準(zhǔn)備100條測(cè)試數(shù)據(jù)");
Klass klass = new Klass();
klass.setName("testKlass");
this.klassRepository.save(klass);
for (int i = 0; i < 100; i++) {
Student student = new Student();
student.setName(RandomString.make(4));
student.setSno(RandomString.make(6));
student.setKlass(klass);
this.studentRepository.save(student);
}
logger.info("每頁2條,請(qǐng)求第1頁數(shù)據(jù)");
String url = "/Student?page=49&size=2"; ②
this.mockMvc.perform(MockMvcRequestBuilders.get(url))
.andDo(MockMvcResultHandlers.print()) ③
.andExpect(MockMvcResultMatchers.status().isOk());
}
```
* ① 引入數(shù)據(jù)倉庫
* ② 將每頁大小、當(dāng)前頁兩個(gè)查詢參數(shù)直接拼接到URL中
* ③ 在控制臺(tái)中打印返回的結(jié)果
啟動(dòng)單元測(cè)試后在控制臺(tái)中得到如下返回信息:
```
Body = {"content":[{"id":99,"name":"FtJf","sno":"56IhJV","klass":{"id":1,"teacher":null,"name":"testKlass"}},{"id":100,"name":"WHpT","sno":"YVwSqA","klass":{"id":1,"teacher":null,"name":"testKlass"}}],"pageable":{"sort":{"sorted":false,"unsorted":true,"empty":true},"offset":98,"pageSize":2,"pageNumber":49,"paged":true,"unpaged":false},"totalPages":50,"totalElements":100,"last":true,"size":2,"number":49,"numberOfElements":2,"first":false,"sort":{"sorted":false,"unsorted":true,"empty":true},"empty":false}
```
對(duì)其進(jìn)行格式化后如下:
```
{ ①
"content": [ ②③
{ ④
"id": 99,
"name": "FtJf",
"sno": "56IhJV",
"klass":
{
"id": 1,
"teacher": null,
"name": "testKlass"
}
},
{ ④
"id": 100, ⑤
"name": "WHpT", ⑤
"sno": "YVwSqA", ⑤
"klass": ⑤
{
"id": 1, ⑥
"teacher": null, ⑥
"name": "testKlass" ⑥
}
}],
"pageable": ②⑦
{
"sort":
{
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 98,
"pageSize": 2,
"pageNumber": 49,
"paged": true,
"unpaged": false
},
"totalPages": 50, ②
"totalElements": 100, ②
"last": true, ②
"size": 2, ②
"number": 49, ②
"numberOfElements": 2, ②
"first": false, ②
"sort": ②⑧
{
"sorted": false,
"unsorted": true,
"empty": true
},
"empty": false ②
}
```
* ① 返回值為一個(gè)對(duì)象 Page
* ② 對(duì)象①的各個(gè)屬性
* ③ 當(dāng)前頁內(nèi)容 Array<Student>
* ④ 數(shù)組中有兩個(gè)對(duì)象 Student
* ⑤ Student對(duì)象④的屬性
* ⑥ Klass對(duì)象⑤的屬性
* ⑦ 分頁條件信息
* ⑧ 排序條件信息
如上所示,spring不僅僅返回了當(dāng)前頁的數(shù)據(jù)、分頁條件、總頁數(shù)、數(shù)據(jù)總數(shù)信息,還返回了是否尾頁、每頁大小、當(dāng)前頁碼(0基)、當(dāng)前頁數(shù)據(jù)條數(shù)、是否首頁、排序、當(dāng)前數(shù)據(jù)是否為空信息。這些數(shù)據(jù)為前臺(tái)提供了良好的支持。
## 單元測(cè)試二
讓我們恢復(fù)剛剛的測(cè)試,繼續(xù)使用模擬的服務(wù)層來完成C層的測(cè)試。
```
@Autowired ?
@MockBean ?
private StudentService studentService;
@Autowired ?
private KlassRepository klassRepository; ?
@Autowired ?
private StudentRepository studentRepository; ?
@Test
public void findAll() throws Exception {
logger.info("初始化模擬返回?cái)?shù)據(jù)");
List<Student> students = new ArrayList<>();
Klass klass = new Klass();
klass.setId(-2L);
for (long i = 0; i < 2; i++) {
Student student = new Student();
student.setId(-i - 1);
student.setSno(RandomString.make(6));
student.setName(RandomString.make(4));
student.setKlass(klass);
students.add(student);
}
logger.info("初始化分頁信息及設(shè)置模擬返回?cái)?shù)據(jù)");
Page<Student> mockOutStudentPage = new PageImpl<Student>(
students,
PageRequest.of(1, 2),
4
);
Mockito.when(this.studentService.findAll(Mockito.any(Pageable.class)))
.thenReturn(mockOutStudentPage);
logger.info("以'每頁2條,請(qǐng)求第1頁'為參數(shù)發(fā)起請(qǐng)求,斷言返回狀態(tài)碼為200,并接收響應(yīng)數(shù)據(jù)");
String url = "/Student";
MvcResult mvcResult = this.mockMvc.perform(
MockMvcRequestBuilders.get(url)
.param("page", "1")
.param("size", "2"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
logger.info("將返回值由string轉(zhuǎn)為json,并斷言接收到了分頁信息");
LinkedHashMap① returnJson = JsonPath.parse(mvcResult.getResponse().getContentAsString()).json();
Assertions.assertThat(returnJson.get("totalPages")).isEqualTo(2); // 總頁數(shù)
Assertions.assertThat(returnJson.get("totalElements")).isEqualTo(4); // 總條數(shù)
Assertions.assertThat(returnJson.get("size")).isEqualTo(2); // 每頁大小
Assertions.assertThat(returnJson.get("number")).isEqualTo(1); // 第幾頁(0基)
Assertions.assertThat(returnJson.get("numberOfElements")).isEqualTo(2); // 當(dāng)前頁條數(shù)
//todo: 斷言獲取到了content,類型為數(shù)組 ?
return;
}
```
通過??標(biāo)記可以看到,由于Mock的加入,在進(jìn)行C層功能代碼的測(cè)試時(shí),我們僅僅需要考慮C層直接調(diào)用的服務(wù)層StudentService.findAll的輸入輸出即可,而StudentService是否實(shí)現(xiàn)了其描述的功能以及如何實(shí)現(xiàn)的其描述的功能,我們完全不關(guān)心也不需要關(guān)心。
* ? 只所以在這里出現(xiàn)todo,是由于①LinkedHashMap這個(gè)容器可以裝入任意類型,所以我們無法通過returnJson.get("content")來獲取其content的數(shù)據(jù)類型。在第一次接觸時(shí),我們需要debug來幫我查看content的數(shù)據(jù)類型然后繼續(xù)完成后續(xù)的操作。
為此,我們?cè)诖颂幋騻€(gè)斷點(diǎn):

然后啟動(dòng)debug,并在debug控制臺(tái)中找到returnJson:

得到具體的類型后,我們繼續(xù)完成測(cè)試:
```
import net.minidev.json.JSONArray; ★
...
@Test
public void findAll() throws Exception {
...
Assertions.assertThat(returnJson.get("numberOfElements")).isEqualTo(2); // 當(dāng)前頁條數(shù)
logger.info("測(cè)試content");
JSONArray content = (JSONArray★) returnJson.get("content");
Assertions.assertThat(content.size()).isEqualTo(2); // 返回了2個(gè)學(xué)生
logger.info("測(cè)試返回的學(xué)生");
for (int① i = 0; i < 2; i++) {
LinkedHashMap studentHashMap = (LinkedHashMap) content.get(i); // 獲取第一個(gè)學(xué)生
Assertions.assertThat(studentHashMap.get("id")).isEqualTo(-i - 1);
Assertions.assertThat(studentHashMap.get("name").toString().length()).isEqualTo(4);
Assertions.assertThat(studentHashMap.get("sno").toString().length()).isEqualTo(6);
logger.info("測(cè)試返回學(xué)生所在的班級(jí)");
LinkedHashMap klassHashMap = (LinkedHashMap) studentHashMap.get("klass");
Assertions.assertThat(klassHashMap.get("id")).isEqualTo(-2);
Assertions.assertThat(klassHashMap.get("name")).isEqualTo("test klass name");
}
return;
}
```
* ★ 注意此處的類型為:net.minidev.json.JSONArray
* ① 此處是int,不是long。
>[success] 在C層的單元測(cè)試中,對(duì)每個(gè)前臺(tái)需要的測(cè)試都加入相應(yīng)的斷言是非常有必要的。在生產(chǎn)項(xiàng)目中如果未對(duì)C層的輸出字段進(jìn)行斷言,則必然發(fā)生在后臺(tái)的敏捷開發(fā)中造成前臺(tái)部分功能失效的問題。
# 總結(jié)
我們?cè)诒拘」?jié)中花費(fèi)了大量的精力來編寫單元測(cè)試。在編寫的過程中我們感受到:編寫單元測(cè)試的難度遠(yuǎn)遠(yuǎn)超出了編寫功能代碼的難度;編寫單元測(cè)試的時(shí)間遠(yuǎn)遠(yuǎn)的超出了編寫功能代碼的時(shí)間。而這,是非常有必要的。在軟件開發(fā)的所有的專業(yè)課中,軟件工程是在學(xué)習(xí)的時(shí)候最不容易引起重視但卻在實(shí)戰(zhàn)中起出保障軟件質(zhì)量關(guān)鍵一環(huán)的核心課程。如果你不希望自己以后編寫的軟件在每次更新后都會(huì)發(fā)生或多或少的非預(yù)期錯(cuò)誤,如果你不希望自己本已經(jīng)編寫好的功能在其它團(tuán)隊(duì)成員的協(xié)助開發(fā)下變得不可用,如果你想做一個(gè)對(duì)前臺(tái)負(fù)責(zé)的后臺(tái)開發(fā)工程師、如果你不想在新的版本上線后天天打噴嚏、如果你希望隨著需求的發(fā)展及新技術(shù)的普通而能夠放開手腳的重構(gòu)代碼、如果你的目標(biāo)是Engineer而不是Programmer,那么從現(xiàn)在起請(qǐng)注重**單元測(cè)試**!
# 參考文檔
| 名稱 | 鏈接 | 預(yù)計(jì)學(xué)習(xí)時(shí)長(zhǎng)(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.1) | - |
- 序言
- 第一章:Hello World
- 第一節(jié):Angular準(zhǔn)備工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二節(jié):Hello Angular
- 第三節(jié):Spring Boot準(zhǔn)備工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四節(jié):Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven國(guó)內(nèi)源配置
- 4 package與import
- 第五節(jié):Hello Spring Boot + Angular
- 1 依賴注入【前】
- 2 HttpClient獲取數(shù)據(jù)【前】
- 3 數(shù)據(jù)綁定【前】
- 4 回調(diào)函數(shù)【選學(xué)】
- 第二章 教師管理
- 第一節(jié) 數(shù)據(jù)庫初始化
- 第二節(jié) CRUD之R查數(shù)據(jù)
- 1 原型初始化【前】
- 2 連接數(shù)據(jù)庫【后】
- 3 使用JDBC讀取數(shù)據(jù)【后】
- 4 前后臺(tái)對(duì)接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節(jié) CRUD之C增數(shù)據(jù)
- 1 新建組件并映射路由【前】
- 2 模板驅(qū)動(dòng)表單【前】
- 3 httpClient post請(qǐng)求【前】
- 4 保存數(shù)據(jù)【后】
- 5 組件間調(diào)用【前】
- 第四節(jié) CRUD之U改數(shù)據(jù)
- 1 路由參數(shù)【前】
- 2 請(qǐng)求映射【后】
- 3 前后臺(tái)對(duì)接【前】
- 4 更新數(shù)據(jù)【前】
- 5 更新某個(gè)教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節(jié) CRUD之D刪數(shù)據(jù)
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個(gè)教師【后】
- 第六節(jié) 代碼重構(gòu)
- 1 文件夾化【前】
- 2 優(yōu)化交互體驗(yàn)【前】
- 3 相對(duì)與絕對(duì)地址【前】
- 第三章 班級(jí)管理
- 第一節(jié) JPA初始化數(shù)據(jù)表
- 第二節(jié) 班級(jí)列表
- 1 新建模塊【前】
- 2 初識(shí)單元測(cè)試【前】
- 3 初始化原型【前】
- 4 面向?qū)ο蟆厩啊?/a>
- 5 測(cè)試HTTP請(qǐng)求【前】
- 6 測(cè)試INPUT【前】
- 7 測(cè)試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺(tái)對(duì)接【前】
- 第三節(jié) 新增班級(jí)
- 1 初始化【前】
- 2 響應(yīng)式表單【前】
- 3 測(cè)試POST請(qǐng)求【前】
- 4 JPA插入數(shù)據(jù)【后】
- 5 單元測(cè)試【后】
- 6 惰性加載【前】
- 7 對(duì)接【前】
- 第四節(jié) 編輯班級(jí)
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務(wù)【前】
- 4 測(cè)試間諜spy【前】
- 5 使用JPA更新數(shù)據(jù)【后】
- 6 分層開發(fā)【后】
- 7 前后臺(tái)對(duì)接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節(jié) 選擇教師組件
- 1 初始化【前】
- 2 動(dòng)態(tài)數(shù)據(jù)綁定【前】
- 3 初識(shí)泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識(shí)單元測(cè)試【前】
- 7 其它問題
- 第六節(jié) 刪除班級(jí)
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺(tái)對(duì)接
- 第四章 學(xué)生管理
- 第一節(jié) 引入Bootstrap【前】
- 第二節(jié) NAV導(dǎo)航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節(jié) footer組件【前】
- 第四節(jié) 歡迎界面【前】
- 第五節(jié) 新增學(xué)生
- 1 初始化【前】
- 2 選擇班級(jí)組件【前】
- 3 復(fù)用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗(yàn)【后】
- 7 唯一性校驗(yàn)【后】
- 8 @PrePersist【后】
- 9 CM層開發(fā)【后】
- 10 集成測(cè)試
- 第六節(jié) 學(xué)生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識(shí)綜合查詢【后】
- 4 綜合查詢進(jìn)階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測(cè)試與分頁【前】
- 9 單選與多選【前】
- 10 集成測(cè)試
- 第七節(jié) 編輯學(xué)生
- 1 初始化【前】
- 2 嵌套組件測(cè)試【前】
- 3 功能開發(fā)【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測(cè)試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗(yàn)證【前】
- 第八節(jié) 刪除學(xué)生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發(fā)與測(cè)試【后】
- 4 集成測(cè)試
- 5 定制提示框【前】
- 6 引入圖標(biāo)庫【前】
- 第九節(jié) 集成測(cè)試
- 第五章 登錄與注銷
- 第一節(jié):普通登錄
- 1 原型【前】
- 2 功能設(shè)計(jì)【前】
- 3 功能設(shè)計(jì)【后】
- 4 應(yīng)用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態(tài)【前】
- 第二節(jié):你是誰
- 1 過濾器【后】
- 2 令牌機(jī)制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個(gè)人中心【前】
- 8 攔截器【后】
- 9 集成測(cè)試
- 10 單例模式
- 第六章 課程管理
- 第一節(jié) 新增課程
- 1 初始化【前】
- 2 嵌套組件測(cè)試【前】
- 3 async管道【前】
- 4 優(yōu)雅的測(cè)試【前】
- 5 功能開發(fā)【前】
- 6 實(shí)體監(jiān)聽器【后】
- 7 @ManyToMany【后】
- 8 集成測(cè)試【前】
- 9 異步驗(yàn)證器【前】
- 10 詳解CORS【前】
- 第二節(jié) 課程列表
- 第三節(jié) 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節(jié) 班級(jí)列表
- 第節(jié) 教師列表
- 第節(jié) 編輯課程
- TODO返回機(jī)制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節(jié) 刪除課程
- 第七章 權(quán)限管理
- 第一節(jié) AOP
- 總結(jié)
- 開發(fā)規(guī)范
- 備用