FreeRTOS基礎(三):動態創建任務

? ? ?上一篇博客,我們講解了FreeRTOS中,我們講解了創建任務和刪除任務的API函數,那么這一講,我們從實戰出發,規范我們在FreeRTOS下的編碼風格,掌握動態創建任務的編碼風格,達到實戰應用!

目錄

一、任務函數

二、動態創建任務的基本步驟

2.1 使能FreeRTOS的API函數

2.2? 定義動態創建任務函數的入口參數

2.3 編寫任務函數

2.4 主函數進行調用

2.5 補充??

2.6 任務執行順序

四、動態創建任務的API函數解析(選學)

五、任務優先級

六、總結


一、任務函數

? ? ? ? ?不論是動態創建任務還是靜態創建任務,我們FreeRTOS都是在任務之間切換執行,那么任務函數就是我們單獨要實現的功能,根據功能的不同,把裸機系統分割為?個個獨立的無限循環且無法返回的函數。我們把這種函數稱之為任務。即:任務函數是沒有返回值,并且是死循環的!任務的形式:如下:

void task1(void *arg)
{//初始化代碼while(1) //?限循環且不能返回{具體實現的功能}//延時函數
}

1、為什么FreeRTOS的任務函數沒有返回值?(可以將任務理解為線程)

1. 持續運行的任務

? ? ? ?FreeRTOS 任務設計為長期運行,不像普通函數那樣有明確的結束點。在嵌入式系統中,任務(或者稱為線程)通常負責特定的功能,這些功能需要一直運行。例如,處理傳感器數據、管理通信協議或維護系統健康狀態等。這些功能需要持續監控和響應外部事件或內部條件,因此任務函數通常設計為死循環。

2. 任務調度

? ? ? ?FreeRTOS 是一個實時操作系統,負責在多個任務之間進行調度。任務函數進入死循環后,會周期性地調用 FreeRTOS 提供的 API 函數(如 vTaskDelayxQueueReceive),這些 API 會將任務置于阻塞狀態,直到特定條件滿足(延時時間到或者信號量接收到)。這種設計允許 RTOS 進行有效的任務切換,確保系統的實時性和多任務處理能力。

3. 沒有返回值

? ? ? 由于任務函數設計為長期運行,因此它們不需要返回值。任務的結束通常不是通過函數返回來實現的,而是通過其他機制,如任務刪除 (vTaskDelete)。任務函數的主要目的是在系統運行過程中持續執行特定操作,而不是像傳統函數那樣在執行完特定操作后返回。

4. 系統穩定性和資源管理

? ? ? ?任務函數設計為死循環還有助于系統的穩定性和資源管理。在 RTOS 中,任務的生命周期由系統管理,任務函數一旦啟動,便由調度器根據優先級和調度策略進行管理。死循環的設計簡化了任務的生命周期管理,避免了頻繁創建和銷毀任務帶來的資源開銷和復雜性。

2、為什么FreeRTOS任務函數的主體是一個死循環?

1、實時性:

? ? ? ?通過使用死循環,任務可以及時檢查事件狀態并作出相應的處理,以滿足實時性

2、持續性:

? ? ? ?將任務放在一個循環中,可以持續執行。如果任務函數沒有死循環,而是在任務完成后直接返回,那么任務將會自動退出。這可能導致任務被刪除并釋放資源,而無法再次調度執行

3、提高資源的利用率:

? ? ?只要任務不退出,就不需要重新獲取資源,提高效率。

二、動態創建任務的基本步驟

2.1 使能FreeRTOS的API函數

? ? ? 在使用FreeRTOS任務創建函數之前,我們需要在配置文件里(FreertosConfig.h)將宏configSUPPORT_DYNAMIC_ALLOCATION 配置為 1,此時便支持動態創建。利用Ctrl+F搜索即可。

2.2? 定義動態創建任務函數的入口參數

? ? ? ? 通過上一講我們知道動態創建任務的API函數如下:

其實,我們需要定義的入口參數就是這個API函數的參數,提前定義好,然后傳入參數,他就會自動的為我們創建好對應的任務,并且處于一種就緒態。? ?從上面我們可以看到:

1、任務函數指針:

? ? ? ?其實就是函數名,我們知道函數名就是函數的入口地址,就是一個函數指針

2、任務名字:

? ? ? ? 其實也就是函數名對應的字符串,要用雙引號括起來

3、任務堆棧大小:

? ? ? ? 動態創建任務,任務的任務控制塊以及任務的棧空間所需的內存,均由 FreeRTOS 自動從 FreeRTOS 管理的堆中分配,但是我們需要定義好任務棧的大小,使用宏:

#define     START_TASK_STACK_SIZE  128   //定義任務堆棧大小為128字(1字等于4字節)

4、傳遞給任務的參數:

? ? ? ?不需要傳參,我們直接給NULL即可;

5、任務優先級:

? ? ? ? 我們使用的是硬件的方式,因此,它要在0-31之間,使用宏定義即可:

#define     START_TASK_PRIO      1    //定義任務優先級,0-31根據任務需求

6、任務句柄:

? ? ? ? 這個參數是指向任務控制塊的指針,任務控制塊TCB其實就是描述任務屬性的一個結構體,一次他就是一個結構體指針,我們后續對任務的刪除等操作,都是通過該任務句柄進行操作,因此,我們需要提前定義好,然后傳入即可,使用宏即可:

TaskHandle_t   start_task_handler;    //定義任務句柄(結構體指針)

? ? ? 從上面我們可以知道:其實我們只需要提前利用宏定義好三個參數即可,其他的參數只要任務函數編寫好,便可以確定。示例如下:

/**********************START_TASK任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define        START_TASK_STACK_SIZE  128   //定義堆棧大小為128字(1字等于4字節)
#define        START_TASK_PRIO         1    //定義任務優先級,0-31根據任務需求
TaskHandle_t   start_task_handler;    //定義任務句柄(結構體指針)
void start_task(void* args);

注意:

  1. 為了編碼規范,我們使用的宏都是大寫,雖然較長,但是通俗易懂;
  2. 使用API函數進行任務創建,里面的參數需要進行強制轉換,以免報錯。
  3. 為了任務執行的順序是按照我們設定好的優先級執行的,我們可以在創建任務的任務中,使用臨界段保護,那么在這個任務體中,可以屏蔽中斷(中斷優先級在5-15之內)比如切換任務的PendSV,此時,我們創建任務的過程中,不會進行任務的調度,然后我們創建任務結束后,在打開臨界段保護,此時不會對所有中斷進行屏蔽,也就是任務切換PendSV(中斷)才會進行任務調度。如下代碼所示,在創建任務開始之前和創建任務之后加入,后面詳細講解。
  4. 動態創建任務函數,有返回值,我們可以在編程時,對返回值進行判斷,由此可以知道任務是否創建成功!
#include "stm32f4xx.h"                  // Device header
#include "stdio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"/**********************START_TASK任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/#define        START_TASK_STACK_SIZE  128   //定義堆棧大小為128字(1字等于4字節)
#define        START_TASK_PRIO         1    //定義任務優先級,0-31根據任務需求
TaskHandle_t   start_task_handler;    //定義任務句柄(結構體指針)
void start_task(void* args);/**********************TASK1任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define  TASK1_STACK_SIZE  128            //定義堆棧大小為128字(1字等于4字節)
#define  TASK1_PRIO         2             //定義任務優先級,0-31根據任務需求
TaskHandle_t   task1_handler;           //定義任務句柄(結構體指針)
void task1(void* args);/**********************TASK2任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define  TASK2_STACK_SIZE  128            //定義堆棧大小為128字(1字等于4字節)
#define  TASK2_PRIO         3             //定義任務優先級,0-31根據任務需求
TaskHandle_t   task2_handler;           //定義任務句柄(結構體指針)
void task2(void* args);/**********************TASK3任務配置******************************/
/***********包括任務堆棧大小、任務優先級、任務句柄、創建任務***********/
#define  TASK3_STACK_SIZE  128            //定義堆棧大小為128字(1字等于4字節)
#define  TASK3_PRIO         4            //定義任務優先級,0-31根據任務需求
TaskHandle_t   task3_handler;           //定義任務句柄(結構體指針)
void task3(void* args);
開始任務用來創建其他三個任務,只創建一次,不能是死循環,同時創建完3個任務后刪除開始任務本身
void start_task(void* args)
{taskENTER_CRITICAL();        /*進入臨界區*/BaseType_t xReturn;        //定義接收函數返回值的變量xTaskCreate( (TaskFunction_t)         task1,(char *)     "task1",  ( configSTACK_DEPTH_TYPE)   TASK1_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK1_PRIO ,(TaskHandle_t *)  &task1_handler );//任務1創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t)         task2,(char *)     "task2",  ( configSTACK_DEPTH_TYPE)   TASK2_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK2_PRIO ,(TaskHandle_t *)  &task2_handler );	//任務2創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}xTaskCreate( (TaskFunction_t)          task3,(char *)     "task3",  ( configSTACK_DEPTH_TYPE)   TASK3_STACK_SIZE,(void *)      NULL,(UBaseType_t) TASK3_PRIO ,(TaskHandle_t *)  &task3_handler );	//任務3創建結果的判斷if( xReturn == pdPASS){printf("LED_Task create SUCCESS\n");}else{printf("LED_Task create FALL\n");}vTaskDelete(NULL);    //刪除開始任務自身,傳參NULLtaskEXIT_CRITICAL();   /*退出臨界區*///臨界區內不會進行任務的調度切換,出了臨界區才會進行任務調度,搶占式						
}

2.3 編寫任務函數

? ? 對每個任務具體實現的功能進行函數的實現:需要注意,任務函數沒有返回值并且是死循環的!

/********其余三個任務的任務函數,無返回值且是死循環***********//***任務1:實現LED0每500ms翻轉一次*******/
void task1(void* args)
{while(1){printf("任務1正在運行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_9 );vTaskDelay(500);       //FreeRTOS自帶的延時函數,會進行任務切換調度}}/***任務2:實現LED1每500ms翻轉一次*******/
void task2(void* args)
{while(1){printf("任務2正在運行!\n");GPIO_ToggleBits(GPIOF,GPIO_Pin_10 );vTaskDelay(500);       //FreeRTOS自帶的延時函數,會進行任務切換調度}}/***任務3:判斷按鍵KEY0,按下KEY0,任務1刪除*******/
void task3(void* args)
{while(1){printf("任務3正在運行!\n");if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==0)  //表示按鍵按下{if(task1_handler!=NULL)  //防止重復刪除{printf("刪除任務1!\n");vTaskDelete(task1_handler);    //刪除任務1,傳任務1的句柄task1_handler=NULL;}}	  vTaskDelay(10);    //FreeRTOS自帶的延時函數,會進行任務切換調度}}

? ? ? 此外,我們再自定義一個入口函數,用來創建開始任務,然后將要創建的任務全部放在這個開始任務中,主函數只需調用這個入口函數,即可在這個開始任務中 , 創建其他的任務,這樣做,規范代碼,梳理代碼邏輯,清晰易懂任務的運行順序!如下所示:

//FreeRTO入口例程函數,無參數,無返回值,用來創建開始任務
void freertos_demo(void)
{xTaskCreate( (TaskFunction_t)     start_task,(char *)     "start_task",  ( configSTACK_DEPTH_TYPE)   START_TASK_STACK_SIZE,(void *)      NULL,(UBaseType_t) START_TASK_PRIO ,(TaskHandle_t *)  &start_task_handler );vTaskStartScheduler();  //開啟任務調度器}

2.4 主函數進行調用

? ? ? ? 在完成上述的編寫后,主函數內部只需要引入對應的頭文件,然后在函數內部調用相應的函數對使用到的外設進行初始化,然后調用入口函數即可進行按照我們設定的優先級進行任務的調度,如下所示:

#include "stm32f4xx.h"                  // Device header
#include "stdio.h"
#include "myled.h"
#include "mykey.h"
#include "myusart.h"#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"    //可以用來單獨存放任務函數的聲明以及配置相關的宏定義,然后直接引入頭文件使用extern TaskHandle_t Start_Handle;  
/*使用任務句柄可以對任務操作,如果沒有添加上面的單獨頭文件存放,
那么使用其他文件的全局變量利用extern關鍵字引入即可。*/int main(void)
{//1、外設初始化My_UsartInit();LED_Init();KEY_Init();//2、調用入口函數freertos_demo();}

2.5 補充??

? ? ? ?為進行模塊化的編程,我們可以將創建相應的頭文件可以用來單獨存放任務函數的聲明以及任務配置相關的宏定義,然后在主函數直接引入頭文件使用即可,這樣工程結構清晰易懂!

2.6 任務執行順序

? ? ? ? 編寫完程序后,一定要進行驗證,驗證程序是否按照我們設定的順序及進行執行,類似于操作系統的線程同步問題!

? ? ? ?首先主函數調用入口函數,在入口函數內部創建開始任務函數,該開始任務進入就緒狀態,啟用任務調度器,調度器啟動后,FreeRTOS 將接管系統控制,開始調度任務。此時CPU就會去執行開始任務,然后,在開始任務中創建三個任務,注意:由于使用了臨界保護:taskENTER_CRITICAL(); ? ? ? ?/*進入臨界區*/? 它會對5-15優先級的中斷進行屏蔽,即不會發生作用,其中PendSV是用來任務切換的內核中斷,它的優先級是13,因此,會被屏蔽,也就是說,我在創建三個任務的過程中,不會進行其他任務的切換,保證我的開始任務創建其他的三個任務不會被打斷!!!創建完三個任務后,它們都進入了就緒態,然后,再刪除這個開始任務(因為每個任務只需要創建一次,多次創建占用堆棧內存,造成棧溢出!)此時,我在關閉臨界區保護,taskEXIT_CRITICAL(); ? /*退出臨界區*/,也就是打開所有中斷,此時PendSV中斷就會被打開,按照任務的優先級進行搶占式調度,分別執行任務3、任務2、任務1,在三個任務執行的過程中,加入適當的延時,他就會進行任務的切換,去就緒列表尋找優先級最高的任務去運行!

四、動態創建任務的API函數解析(選學)

五、任務優先級

? ? ?在 FreeRTOS 中,任務的優先級決定了任務在系統中的調度順序和執行時機。設定任務優先級是 FreeRTOS 任務創建過程中一個重要的步驟。

1、優先級的范圍

FreeRTOS 任務優先級的范圍由 configMAX_PRIORITIES 宏定義。該宏在 FreeRTOSConfig.h 文件中定義。通常,優先級的范圍是從 0 到 configMAX_PRIORITIES - 1,優先級數值越大,優先級越高。

2、注意事項

  1. 優先級的相對性:任務的優先級是相對的,系統中最高優先級的任務將獲得最多的 CPU 時間。如果多個任務具有相同的優先級,調度器會按照時間片輪轉或其他調度策略在它們之間切換。

  2. 優先級反轉:在某些情況下,低優先級的任務可能會持有高優先級任務所需的資源,導致優先級反轉問題。FreeRTOS 提供了優先級繼承機制來解決這個問題。

  3. 優先級設定的策略:設定優先級時,需要考慮任務的重要性和時間敏感性。實時性要求高的任務應設定較高的優先級,而非實時任務可以設定較低的優先級。

  4. 避免過高優先級:設定任務優先級時要避免將所有任務都設為過高的優先級,這樣會導致系統缺乏靈活性,可能導致低優先級任務得不到執行。

六、總結

? ? ? ? ?通過以上的介紹,是不是覺得相比裸機開發確實提升了不少的難度,這就是實時性帶來的,萬事有利必有弊,多看幾遍,相信你對動態創建任務的過程會有清晰的認識,其實步驟也是非常簡單的,接下來去實踐吧!熟練后就不難了,萬事開頭難!

溫馨提示:?

? ? ? ?對于某個需要知道具體函數的實現的,我們可以雙擊函數然后直接跳轉到定義處,或者Ctrl+F 搜索,也可以去官網查看對應的使用實例:https://www.freertos.org/。

? ? ? 至此,動態創建任務就已經講解完畢!初次學習,循序漸進,一步步掌握即可!以上就是全部內容!請務必掌握,創作不易,歡迎大家點贊加關注評論,您的支持是我前進最大的動力!下期再見!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/20226.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/20226.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/20226.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

用貪心算法進行10進制整數轉化為2進制數

十進制整數轉二進制數用什么方法?網上一搜,大部分答案都是用短除法,也就是除2反向取余法。這種方法是最基本最常用的,但是計算步驟多,還容易出錯,那么還有沒有其他更好的方法嗎? 一、短除反向取…

AdroitFisherman模塊安裝日志(2024/5/31)

安裝指令 pip install AdroitFisherman-0.0.29.tar.gz -v 安裝條件 1:Microsoft Visual Studio Build Tools 2:python 3.10.x 顯示輸出 Using pip 24.0 from C:\Users\12952\AppData\Local\Programs\Python\Python310\lib\site-packages\pip (python 3.10) Processing c:\u…

matlab GUI界面設計

【實驗內容】 用MATLAB的GUI程序設計一個具備圖像邊緣檢測功能的用戶界面,該設計程序有以下基本功能: (1)圖像的讀取和保存。 (2)設計圖形用戶界面,讓用戶對圖像進行彩色圖像到灰度圖像的轉換…

3-哈希表-21-兩個數組的交集-LeetCode349

3-哈希表-21-兩個數組的交集-LeetCode349 參考:代碼隨想錄 LeetCode: 題目序號349 更多內容歡迎關注我(持續更新中,歡迎Star?) Github:CodeZeng1998/Java-Developer-Work-Note 技術公眾號:CodeZeng1998&…

2.1 OpenCV隨手簡記(二)

為后續項目學習做準備,我們需要了解LinuxOpenCV、Mediapipe、ROS、QT等知識。 一、圖像顯示與保存 1、基本原理 1.1 圖像像素存儲形式 首先得了解下圖像在計算機中存儲形式:(為了方便畫圖,每列像素值都寫一樣了)。對于只有黑白顏色的灰度…

[有監督學習]2.詳細圖解正則化

正則化 正則化是防止過擬合的一種方法,與線性回歸等算法配合使用。通過向損失函數增加懲罰項的方式對模型施加制約,有望提高模型的泛化能力。 概述 正則化是防止過擬合的方法,用于機器學習模型的訓練階段。過擬合是模型在驗證數據上產生的誤…

Java文件IO

White graces:個人主頁 🙉專欄推薦:Java入門知識🙉 🙉 內容推薦:JUC常見類🙉 🐹今日詩詞:東風吹柳日初長,雨馀芳草斜陽🐹 ??點贊 ??收藏??關注💬卑微小博主&…

Three.js 研究:4、創建設備底部旋轉的科技感圓環

1、實現效果 2、PNG轉SVG 2.1、原始物料 使用網站工具https://convertio.co/zh/png-svg/進行PNG轉SVG 3、導入SVG至Blender 4、制作旋轉動畫 4.1、給圓環著色 4.2、修改圓環中心位置 4.3、讓圓環旋轉起來 參考一下文章 Three.js 研究:1、如何讓物體動起來 Thre…

LeetCode # 1070. 產品銷售分析 III

1070. 產品銷售分析 III 題目 銷售表 Sales: ------------------ | Column Name | Type | ------------------ | sale_id | int | | product_id | int | | year | int | | quantity | int | | price | int | ------------------ (sale_id, year) 是這張表的主鍵&am…

“論SOA在企業集成架構設計中的應用”必過模板,突擊2024軟考高項論文

考題部分 企業應用集成(Enterprise Application Integration, EAI)是每個企業都必須要面對的實際問題。面向服務的企業應用集成是一種基于面向服務體系結構(Service-OrientedArchitecture,SOA)的新型企業應用集成技術,強調將企業和組織內部的資源和業務功…

VSCode界面Outline只顯示類名和函數名,隱藏變量名

參考鏈接 https://blog.csdn.net/Zjhao666/article/details/120523879https://blog.csdn.net/Williamcsj/article/details/122401996 VSCode中界面左下角的Outline能夠方便快速跳轉到文件的某個類或函數,但默認同時顯示變量,導致找某個函數時很不方便。…

mimkatz獲取windows10明文密碼

目錄 mimkatz獲取windows10明文密碼原理 lsass.exe進程的作用 mimikatz的工作機制 Windows 10的特殊情況 實驗 實驗環境 實驗工具 實驗步驟 首先根據版本選擇相應的mimikatz 使用管理員身份運行cmd 修改注冊表 ?編輯 重啟 重啟電腦后打開mimikatz 在cmd切換到mi…

Seq2Seq模型:詳述其發展歷程、深遠影響與結構深度剖析

Seq2Seq(Sequence-to-Sequence)模型是一種深度學習架構,專為處理從一個輸入序列到一個輸出序列的映射任務設計。這種模型最初應用于機器翻譯任務,但因其靈活性和有效性,現已被廣泛應用于自然語言處理(NLP&a…

醫院該如何應對網絡安全?

在線醫生咨詢受到很多人的關注,互聯網醫療行業的未來發展空間巨大,但隨著醫院信息化建設高速發展 醫院積累了大量的患者基本信息、化驗結果、電子處方、生產數據和運營信息等數據 這些數據涉及公民隱私、醫院運作和發展等多因素,醫療行業辦…

【QEMU中文文檔】1.關于QEMU

本文由 AI 翻譯(ChatGPT-4)完成,并由作者進行人工校對。如有任何問題或建議,歡迎聯系我。聯系方式:jelin-shoutlook.com。 QEMU 是一款通用的開源機器仿真器和虛擬化器。 QEMU 可以通過幾種不同的方式使用。最常見的用…

OrangePi AIpro--新手上路

目錄 一、SSH登錄二、安裝VNC Sevice(經測試Xrdp遠程桌面安裝不上)2.1安裝xface桌面2.2 配置vnc服務2.2.1 設置vnc server6-8位的密碼2.2.2 創建vnc文件夾,寫入xstartup文件2.2.3 給xstartup文件提高權限2.2.4 在安裝產生的vnc文件夾創建xsta…

C# 工廠模式學習

工廠模式(Factory Pattern)是一種創建型設計模式,它提供了一種創建對象的接口,而不是通過具體類來實例化對象。工廠模式可以將對象的創建過程封裝起來,使代碼更具有靈活性和可擴展性。 工廠模式有幾種常見的實現方式&…

Go 如何通過 Kafka 客戶端庫 生產與消費消息

文章目錄 0.前置說明1. confluent-kafka-go2. sarama3. segmentio/kafka-go4. franz-go選擇建議 1.啟動 kafka 集群2.安裝 confluent-kafka-go 庫3.創建生產者特殊文件說明如何查看.log文件內容 4.創建消費者 0.前置說明 Go 語言中有一些流行的 Kafka 客戶端庫。以下是幾個常用…

【Uniapp小程序】自定義導航欄uni-nav-bar滾動漸變色

效果圖 新建activityScrollTop.js作為mixins export default {data() {return {navBgColor: "rgba(0,0,0,0)", // 初始背景顏色為完全透明navTextColor: "rgba(0,0,0,1)", // 初始文字顏色};},onPageScroll(e) {// 設置背景const newAlpha Math.min((e.s…

踩坑:6年后為何不用GraphQL了?

GraphQL 是一項令人難以置信的技術,自從我在 2018 年首次開始將其投入生產以來,它就吸引了很多人的注意力。 在一大堆無類型的 JSON REST API 上構建了許多 React SPA 之后,我發現 GraphQL 是一股清新的空氣。 然而,隨著時間的推…