引言
上圖是筆者剛參加工作的時候使用的架構圖,當時前後端並未分離,所有代碼都剛在一個工程裡,如果後端代碼改了,前端代碼得跟著改,每次改一點,都需要觸動全身,非常痛苦;
前後端架構
後來隨著公司壯大之後,項目功能也越來越多,改動量也越來越大,實在受不了這種低效率的開發模式了,後來改造成前後端分離的架構,說到前後端分離,它主要分為半分離和完全分離兩種類型。
前後端半分離
上圖是前後端半分離的架構,為什麼說是半分離,因為不是所有頁面都是單頁面應用,在多頁面應用的情況下,前端因為沒有掌握controller層,前端需要跟後端討論,我們這個頁面是要同步輸出呢,還是異步json渲染呢?因此,在這一階段,只能算半分離。
前後端完全分離
上圖是完全分離的前後端架構,在這一時期,擴展了前端的範圍。認為controller層也屬於前端的一部分。在這一時期 前端:負責View和Controller層。 後端:只負責Model層,業務處理/數據等。 可是前端不懂後臺代碼呀?controller層如何實現呢? 這就是node.js的妙用了,node.js適合運用在高併發、I/O密集、少量業務邏輯的場景。最重要的一點是,前端不用再學一門其他的語言了,對前端來說,上手度大大提高。
restful 簡介
說到restful,很多人第一反應是“啊,你說的restful就是那個前後端分離的api嗎”,在RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與數據庫的表格名對應。一般來說,數據庫中的表都是同種記錄的"集合"(collection),所以API中的名詞也應該使用複數。
舉例來說,有一個API提供動物園(zoo)的信息,還包括各種動物和僱員的信息,則它的路徑應該設計成下面這樣。
1、https://api.example.com/v1/zoos
2、https://api.example.com/v1/animals
3、https://api.example.com/v1/employees
對於資源的具體操作類型,由HTTP動詞表示。 常用的HTTP動詞有下面五個(括號裡是對應的SQL命令)。
1、GET(SELECT):從服務器取出資源(一項或多項)。
2、POST(CREATE):在服務器新建一個資源。
3、PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
4、PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
5、DELETE(DELETE):從服務器刪除資源。
比如說我要獲取一筆orderid為5的訂單詳情,通常我們都會這樣設計;
GET /orders/5
創建一筆新訂單,我們一般會這樣設計:
POST /orders
restful 缺陷
前端程序員小紅希望在 某個restful api接口中新增一個返回值,後端程序員小明由於忙著談戀愛,沒有時間排期增加;這是restful 第一個缺點,前端請求某個接口,希望新增字段的時候,增加了額外的溝通成本;
過了幾天,前端程序員小紅髮現,現有接口無緣無故多返回了幾個她不需要的屬性,由於web,andorid,ios三個客戶端請求的都是同一個接口,因此小明把這三個客戶端所有的返回值都返回了。而這些額外的字段,小紅並不需要,而且也增加了小紅解析返回值的成本。這是restful的第二個缺點,接口的契約只能約定俗成,不能動態按需的請求字段。
graphql 簡介
GraphQL 是一個用於 API 的查詢語言,是一個使用基於類型系統來執行查詢的服務端運行時(類型系統由你的數據定義)。GraphQL 並沒有和任何特定數據庫或者存儲引擎綁定,而是依靠你現有的代碼和數據支撐。
這是官網的一段簡介,可能聽起來有點矇蔽;簡單的說就是之前restful設計的接口,都是由後端控制的,後端接口一旦確定了,就很難更改,因為是很多客戶端都使用這個接口,所以無法按照某個特定的客戶端去做定製,而且那樣成本也是特別高。但是使用graphql就不一樣了,數據的主動權掌握在客戶端這裡,客戶端想要什麼樣的數據,自己直接去請求就好了,無須服務端去做改動。
一個 GraphQL 服務是通過定義類型和類型上的字段來創建的,然後給每個類型上的每個字段提供解析函數。例如,一個 GraphQL 服務告訴我們當前登錄用戶是 me,這個用戶的名稱可能像這樣:
type Query {
me: User
}
type User {
id: ID
name: String
}
一併的還有每個類型上字段的解析函數:
function Query_me(request) {
return request.auth.user;
}
function User_name(user) {
return user.getName();
}
一旦一個 GraphQL 服務運行起來(通常在 web 服務的一個 URL 上),它就能接收 GraphQL 查詢,並驗證和執行。接收到的查詢首先會被檢查確保它只引用了已定義的類型和字段,然後運行指定的解析函數來生成結果。
例如這個查詢:
{
me {
name
}
}
會產生這樣的JSON結果:
{
"me": {
"name": "Luke Skywalker"
}
}
這個時候,前端小紅希望在要一個age屬性,那就可以這樣寫:
{
me {
name
age
}
}
服務端產生的json結果如下:
{
"me": {
"name": "Luke Skywalker",
"age":12
}
}
你看,想要什麼字段就在查詢語句裡增加字段即可,也不用和後端程序員溝通,而後端程序員也可以愉快的談戀愛了。
Java 操作 graphql
spring-boot 是基於 2.0.0版本,首先加入以下依賴:
<dependency> <groupid>com.graphql-java/<groupid> <artifactid>graphql-spring-boot-starter/<artifactid> <version>4.0.0/<version> /<dependency> <dependency> <groupid>com.graphql-java/<groupid> <artifactid>graphql-java-tools/<artifactid> <version>4.3.0/<version> /<dependency> 下面是graphql服務的入口定義:
type Query { bookById(id: ID): Book } type Book { id: ID name: String pageCount: Int author: Author } type Author { id: ID firstName: String lastName: String } graphql 入口定義了, 但這只是一個描述, 我們需要實現 query中的描述, 在實際開發中,數據的查詢應該從db中獲取,這裡為了方便直接在程序中寫死了。
@Component
public class GraphQLDataFetchers {
private static List
定義GraphQL:
@Component
public class GraphQLProvider {
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
private GraphQL graphQL;
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))
.build();
}
@Bean
public GraphQL graphQL() {
return graphQL;
}
}
定義Controller:
@RestController
@RequestMapping("/test")
public class DemoController {
@Autowired
private GraphQL graphQL;
@GetMapping
public Object find(@RequestParam("query") String query){
ExecutionResult result = graphQL.execute(query);
return result.getData();
}
}
打開postman,使用get請求,請求如下:
http://localhost:8080/test?query={
\tbookById(id:"book-1") {
\t id
\t}
}
返回結果如下:
{
"bookById": {
"id": "book-1"
}
}
這個時候,我需要多返回一個name,請求如下:
http://localhost:8080/test?query={
\tbookById(id:"book-1") {
\t id
\t\tname
\t}
}
返回結果如下:
{
"bookById": {
"id": "book-1",
"name": "Harry Potter and the Philosopher's Stone"
}
}
示例很簡單,但graphql確實很好地解決了不同接口對查詢字段差異性的要求,而不會產生數據冗餘,更多功能還待研究。
當然你會說使用graphql會不會產生額外的工作量? 當然會增加額外的工作量,前期你需要熟悉graphql的語法,後端需要大量的工作;但是這些額外的工作只是在前期,後期的好處就會體現出來了。
總結
本文介紹了傳統的web開發架構的弊端,以及隨著業務的增長,如何改造成前後端分離的架構,進而引申出restful 架構,隨著restful 架構的缺陷,才有後來的代替者graphql。
閱讀更多 科技伍小黑 的文章