0%

restful-react 搭配 abp.io 自動產生前端 api script

前言:

最近在找 abp.io 的 service proxy 替代方式,因為原本只支援 angular,想說要找看看有沒有同樣利用 open api 產生前端 script 給 react 的方法,所以才有了這篇文

主文:

看了 2019 react conf 剛好看到這個影片

IMAGE ALT TEXT HERE

裡面講到了在前端透過 typescript 讓演講者團隊的產品更加穩固,不在有 js 的一堆老毛病,並且透過 ts 的 type definition,讓整個程式需要重構時更加方便

但是,在網路層(前端呼叫 api 的部分),還是會經常發生問題,一般 api 都是使用 rest api,但 rest api 沒有一個正式的回傳格式定義,假設一個使用者去取得一個不屬於他的資料,應該是要回傳 401 因為沒有經過驗證,或是 403 因為此項操作被 forbidden,或甚至 404 因為使用者根本不該知道此項資源存在,開發者要知道這些只能去翻看 api 文件(或者用猜的),並且 call api 回傳的資料不看 api 文件也不知道裡面有什麼,甚至有 api 更新了但文件沒更新,導致開發者完全摸不著頭緒

所以得出了結論 rest api suck

接著又講到了 graphql,graphql 很好的解決了上面那些問題,回傳的資料內容是有詳細定義的,但是畢竟 graphql 只能在 web 使用,如果想要讓 api 共用其他 client side,就可能要把 api 分兩套來寫。

所以,有沒有什麼辦法是可以結合 graphql 的優點,但卻是使用 rest api 呢?

他提到了 open api

open api 是 rest api 的一個標準化定義格式,透過 json 或是 yaml 定義 server side 有哪些 api,不同 api 的 http method,以及回傳的格式

演講者就想說可以透過這個東西來結合 react,於是就誕生了 restful-react

下面介紹一下 restful-react 的使用方式

  1. 首先先在 react 專案中輸入以下內容,安裝 restful-react

    1
    npm install restful-react
  2. 建立一個 provider 在你想要可以使用 api 的層級,這邊就以整個 app 來示範

    1
    2
    3
    <RestfulProvider>
    <App/>
    </RestfulProvider>
  3. 指定一個 base url 給 provider,這個就是告訴 restful-react call api 要打哪個網址下的 api

    1
    2
    3
    <RestfulProvider base="[your api host domain]">
    <App/>
    </RestfulProvider>

好,那接下來就是重頭戲,我們要怎麼透過 open api 的 schema 來自動產生前端的 api script 呢?

首先我們要先知道在 server 端,swagger/open api 所建立出來的 schema 在哪裏,我這邊的範例後端是使用 abp.io,那他會預設將 open api schema 放在 [domain]/swagger/v1/swagger.json 中

知道了 schema 放在哪後,就可以透過 restful-react 提供的 sdk 產生出 api script 檔

在專案根目錄中輸入:

1
npx restful-react import --url [your swagger schema url] --output [out put path]

神奇的事情就發生了….失敗了XD

_2021-07-07_10.48.42

錯誤訊息顯示:

1
Error: Every path must have a operationId - No operationId set for get /api/abp/api-definition

原來,預設 abp.io 所產生出來的 open api schema 中沒有預設放入 operationId,這個 operationId 是 restful-react 產生 api script 時,要拿來當作 hook 的名稱的,沒有加入的話就會出錯

那就來加入一下,在 stack overflow 上找到 這篇 ,手動加入 operationId 的方式如下

1
2
3
4
services.AddSwaggerGen(c =>
{
c.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.HttpMethod}");
});

stack overflow 上面的方法是將 operationId 設定成 controller name + http method,不過在 abp.io 中使用這樣的命名方式會導致 operationId 重複,詳細原因我在這邊先不說明,下面提供使用 abp.io 設定 operationId 的方式

1
2
3
4
services.AddSwaggerGen(c =>
{
c.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.GetMethodInfo().Name}");
});

abp.io 這邊是透過 controller name + method name,那產出來的 operationId 會像這樣

1
"operationId": "SourceConnection_GetListAsync"

基本上就是 abp.io 透過 crud appservice 產生出來的 method name

好,那經過以上設定後,再重新執行一次

1
npx restful-react import --url [your swagger schema url] --output [out put path]

這次就產生成功了,我們來看一下產生出來的 api script 長怎樣

dto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
... 略
export interface KnowledgeGraphLoaderWebAPIDatabasesColumnPropertyDto {
columnName?: string;
columnDataType?: string;
propertyName?: string;
propertyDataType?: string;
isPrimaryKey?: boolean;
}

export interface KnowledgeGraphLoaderWebAPIDatabasesCreateUpdateImportProjectDto {
sourceConnectionId: string;
graphDbId: string;
metaData: string;
name: string;
importStatus: string;
}

export interface KnowledgeGraphLoaderWebAPIDatabasesCreateUpdateSourceConnectionDto {
name: string;
host: string;
port: number;
databaseType: KnowledgeGraphLoaderWebAPIDatabasesDatabaseType;
databaseName: string;
databaseSchema?: string;
url: string;
username?: string;
password?: string;
}
... 略

api:

1
2
3
4
5
6
7
8
9
... 略
export const SourceConnectionGetListAsync = (props: SourceConnectionGetListAsyncProps) => (
<Get<VoloAbpApplicationDtosPagedResultDto1KnowledgeGraphLoaderWebAPIDatabasesSourceConnectionDtoKnowledgeGraphLoaderWebAPIApplicationContractsVersion1000CultureNeutralPublicKeyTokenNull, VoloAbpHttpRemoteServiceErrorResponse, SourceConnectionGetListAsyncQueryParams, void>
path={`/api/app/source-connection`}

{...props}
/>
);
... 略

hook:

1
2
3
4
5
... 略
export type UseSourceConnectionGetListAsyncProps = Omit<UseGetProps<VoloAbpApplicationDtosPagedResultDto1KnowledgeGraphLoaderWebAPIDatabasesSourceConnectionDtoKnowledgeGraphLoaderWebAPIApplicationContractsVersion1000CultureNeutralPublicKeyTokenNull, VoloAbpHttpRemoteServiceErrorResponse, SourceConnectionGetListAsyncQueryParams, void>, "path">;

export const useSourceConnectionGetListAsync = (props: UseSourceConnectionGetListAsyncProps) => useGet<VoloAbpApplicationDtosPagedResultDto1KnowledgeGraphLoaderWebAPIDatabasesSourceConnectionDtoKnowledgeGraphLoaderWebAPIApplicationContractsVersion1000CultureNeutralPublicKeyTokenNull, VoloAbpHttpRemoteServiceErrorResponse, SourceConnectionGetListAsyncQueryParams, void>(`/api/app/source-connection`, props);
... 略

那我們來看看如何使用:

  1. 首先,在想要呼叫 api 的 component 中引入想要的 hook

    1
    import { useSourceConnectionGetListAsync } from '../../../api'
  2. 使用這個 hook 的方式有點類似 react-query,會有 loading、error、data

    1
    const { error, loading, data } = useSourceConnectionGetListAsync({});
  3. 接著就可以使用這些資料來顯示了

    1
    2
    3
    4
    5
    6
    7
    return (
    <div>
    {error && console.log(error) && <div>error</div>}
    {loading && <div>loading</div>}
    {data && <div>{JSON.stringify(data)}</div>}
    </div>
    )

結語:

原本是要研究 abp.io 的 service proxy 功能,一樣是透過 open api schema 產生前端 api script,但沒想到只支援 angular,後來跑去找 open-api 官方產 typescript 的 openapi-generator 產出來的 script 也不盡如人意

繞了一圈後在下班時看了一下 react conf 的錄影,竟然就讓我找到這麼好用的 libary,真的是萬里尋他千百度,驀然回首,那人卻在燈火闌珊處 XDDD

參考連結:

https://github.com/contiamo/restful-react#code-generation-from-openapi–swagger-specs

https://stackoverflow.com/questions/52262826/asp-net-core-swashbuckle-set-operationid

https://www.youtube.com/watch?v=cdsnzfJUqm0