🔍 搜尋結果:error

🔍 搜尋結果:error

極為簡單的 Python 專案結構與導入

喜歡這些文章嗎?買書吧! [***Jason C. McDonald 的《Dead Simple Python》可從 No Starch Press 取得。](https://nostarch.com/dead-simple-python) --- 教程最糟糕的部分始終是它們的簡單性,不是嗎?您很少會發現一個包含多個文件的文件,更罕見的是包含多個目錄的文件。 我發現**建立 Python 專案**是語言教學中最常被忽略的部分之一。更糟的是,許多開發人員都會犯錯,在一堆常見錯誤中跌跌撞撞,直到他們得到至少「有效」的東西。 好訊息是:您不必成為他們中的一員! 在《Dead Simple Python》系列的本期中,我們將探索「import」語句、模組、包,以及如何將所有內容組合在一起而不費力氣。我們甚至會涉及 VCS、PEP 和 Python 之禪。係好安全帶! # 設定儲存庫 在我們深入研究實際的專案結構之前,讓我們先討論一下它如何適合我們的版本控制系統 [VCS]…從您*需要* VCS 的事實開始!有幾個原因是... * 追蹤您所做的每一個更改, * 弄清楚你什麼時候弄壞了東西, * 能夠查看舊版的程式碼, * 備份您的程式碼,以及 * 與他人合作。 您有很多選擇。 **Git** 是最明顯的,尤其是當您不知道還可以使用什麼時。您可以在 GitHub、GitLab、Bitbucket 或 Gitote 等上免費託管 Git 儲存庫。如果您想要 Git 以外的東西,還有許多其他選擇,包括 Mercurial、Bazaar、Subversion(儘管如果您使用最後一個,您可能會被同行視為恐龍。) 我將悄悄假設您在本指南的其餘部分中使用 Git,因為這是我專門使用的。 建立儲存庫並將「本機副本」複製到電腦後,您就可以開始設定專案了。您至少需要建立以下內容: - `README.md`:您的專案及其目標的描述。 - `LICENSE.md`:您的專案的許可證(如果它是開源的)。 (有關選擇一個的更多訊息,請參閱 [opensource.org](https://opensource.org/)。) - `.gitignore`:一個特殊文件,告訴 Git 要忽略哪些文件和目錄。 (如果您使用其他 VCS,則該檔案具有不同的名稱。請尋找。) - 包含您的專案名稱的目錄。 沒錯...**我們的Python 程式碼檔案實際上屬於一個單獨的子目錄!** 這非常重要,因為我們的儲存庫的根目錄將變得非常混亂,其中包含建置檔案、打包腳本、虛擬環境以及各種方式其他實際上不屬於原始碼的內容。 僅為了舉例,我們將虛構的專案稱為「awesomething」。 # PEP 8 和命名 Python 風格主要由一組稱為 **Python 增強提案**(縮寫為 **PEP**)的文件管轄。當然,並非所有 PEP 都被實際採納——這就是它們被稱為「提案」的原因——但有些是被採納的。您可以在Python官方網站上瀏覽主PEP索引。此索引的正式名稱為 [PEP 0](https://www.python.org/dev/peps/)。 現在,我們主要關注 [**PEP 8**](https://www.python.org/dev/peps/pep-0008/),它最初由 Python 語言建立者 Guido van Rossum 於2001 年。該文件正式概述了所有Python 開發人員應普遍遵循的程式設計風格。把它放在枕頭下!學習它,遵循它,鼓勵其他人也這樣做。 (附註:PEP 8 指出樣式規則總是有例外。它是*指南*,而不是*命令*。) 現在,我們主要關注標題為 [“包和模組名稱”](https://www.python.org/dev/peps/pep-0008/#package-and-module-names)的部分。。 > 模組應該有簡短的、全小寫的名稱。如果可以提高可讀性,可以在模組名稱中使用下劃線。 Python 套件也應該有短的、全小寫的名稱,儘管不鼓勵使用底線。 我們稍後會了解*模組*和*包*到底是什麼,但現在,請了解**模組由檔案名稱命名**,而**套件由其目錄名稱命名**。 換句話說,**檔案名稱應全部小寫,如果可以提高可讀性,則使用下劃線。**同樣,**目錄名稱應全部小寫,如果可以避免,則不使用下劃線**。換句話說... + 執行此動作:`awesomething/data/load_settings.py` + 不是這個:`awesomething/Data/LoadSettings.py` 我知道,我知道,這是一種冗長的表達方式,但至少我在你的步驟中加入了一點 PEP。 (*你好?這個東西開著嗎?*) # 套件和模組 這會讓人感覺虎頭蛇尾,但這裡是那些承諾的定義: **任何Python(`.py`)檔案都是一個*模組*,目錄中的一堆模組是一個*套件*。** 嗯……差不多了。要讓目錄成為包,您還必須做另一件事,那就是將名為「__init__.py」的檔案貼到其中。實際上,您不必將任何內容*放入*該文件中。它必須在那裡。 您也可以使用`__init__.py` 做其他很酷的事情,但這超出了本指南的範圍,因此[請閱讀文件以了解更多資訊](https://docs.python.org/3/tutorial/modules.html#packages)。 如果你*確實*忘記了套件中的`__init__.py`,它會做一些比失敗更奇怪的事情,因為這使它成為一個**隱式命名空間包**。您可以使用這種特殊類型的套件做一些有趣的事情,但我不會在這裡討論。像往常一樣,您可以透過閱讀文件來了解更多:[PEP 420:隱式命名空間包](https://www.python.org/dev/peps/pep-0420/)。 所以,如果我們看看我們的專案結構,`awesomething` 實際上是一個包,它可以包含其他包。因此,我們可以將「awesomething」稱為我們的*頂級包*,以及其*子包*下的所有包。一旦我們開始進口東西,這將非常重要。 讓我們看一下我的現實專案“遺漏”的快照,以了解我們如何建置東西... ``` omission-git ├── LICENSE.md ├── omission │ ├── app.py │   ├── common │   │   ├── classproperty.py │   │   ├── constants.py │   │   ├── game_enums.py │   │   └── __init__.py │   ├── data │   │   ├── data_loader.py │   │   ├── game_round_settings.py │   │   ├── __init__.py │   │   ├── scoreboard.py │   │   └── settings.py │   ├── game │   │   ├── content_loader.py │   │   ├── game_item.py │   │   ├── game_round.py │   │   ├── __init__.py │   │   └── timer.py │   ├── __init__.py │   ├── __main__.py │   ├── resources │   └── tests │   ├── __init__.py │   ├── test_game_item.py │   ├── test_game_round_settings.py │   ├── test_scoreboard.py │   ├── test_settings.py │   ├── test_test.py │   └── test_timer.py ├── pylintrc ├── README.md └── .gitignore ``` (如果您想知道的話,我使用 UNIX 程式“tree”來製作上面的小圖。) 您會看到我有一個名為“omission”的頂級包,它有四個子包:“common”、“data”、“game”和“tests”。我還有“resources”目錄,但只包含遊戲音訊、圖像等(為簡潔起見,此處省略)。 `resources` 不是一個包,因為它不包含 `__init__.py`。 我的頂層包中還有另一個特殊檔案:`__main__.py`。這是當我們直接透過「python -m omission」執行頂級套件時執行的檔案。我們稍後會討論「__main__.py」中的內容。 # 導入如何進行 如果您以前編寫過任何有意義的 Python 程式碼,那麼您幾乎肯定熟悉「import」語句。例如... ``` import re ``` 知道當我們導入模組時,我們實際上是在執行它是有幫助的。這意味著模組中的任何“import”語句也正在執行。 例如,[`re.py`](https://github.com/python/cpython/blob/3.7/Lib/re.py#L122) 有幾個自己的 import 語句,當我們說 `導入重新`。這並不意味著它們可用於我們從*導入“re”的文件,但這確實意味著這些文件必須存在。如果(由於某種不太可能的原因)“enum.py”在您的環境中被刪除,並且您執行了“import re”,它將失敗並出現錯誤... > 回溯(最近一次呼叫最後一次): > 檔案“weird.py”,第 1 行,位於 <module> 中 > 進口再 > 檔案“re.py”,第 122 行,位於 <module> 中 > 導入枚舉 > ModuleNotFoundError:沒有名為「enum」的模組 當然,讀到這裡,你可能會有點困惑。有人問我為什麼找不到外部模組(在本例中為“re”)。其他人想知道為什麼要導入內部模組(此處為“enum”),因為他們沒有直接在程式碼中請求它。答案很簡單:我們導入了 `re`,然後導入了 `enum`。 當然,上面的場景是虛構的:「import enum」和「import re」在正常情況下永遠不會失敗,因為這兩個模組都是Python核心庫的一部分。這只是一個愚蠢的例子。 ;) # 匯入註意事項 實際上有多種導入方式,但其中大多數應該很少使用(如果有的話)。 對於下面的所有範例,我們假設有一個名為「smart_door.py」的檔案: ``` # smart_door.py def close(): print("Ahhhhhhhhhhhh.") def open(): print("Thank you for making a simple door very happy.") ``` 例如,我們將在 Python 互動式 shell 中執行本節中的其餘程式碼,執行位置與「smart_door.py」相同。 如果我們想執行`open()`函數,我們必須先導入模組`smart_door`。最簡單的方法是...... ``` import smart_door smart_door.open() smart_door.close() ``` 我們實際上會說“smart_door”是“open()”和“close()”的**命名空間**。 Python 開發人員非常喜歡命名空間,因為它們讓函數和其他內容的來源一目了然。 (順便說一句,不要將 *命名空間* 與 *隱式命名空間包* 混淆。它們是兩個不同的東西。) **Python 之禪**,也稱為 [PEP 20](https://www.python.org/dev/peps/pep-0020/),定義了 Python 語言背後的哲學。最後一行有一個聲明解決了這個問題: > 命名空間是一個非常棒的想法——讓我們做更多這樣的事情! 然而,在某種程度上,命名空間可能會變得很痛苦,尤其是對於嵌套包來說。 `foo.bar.baz.whatever.doThing()` 太醜了。值得慶幸的是,我們確實有辦法避免*每次*呼叫函數時都必須使用命名空間。 如果我們希望能夠使用 `open()` 函數,而不必總是在其前面加上模組名稱,我們可以這樣做... ``` from smart_door import open open() ``` 但請注意,「close()」和「smart_door.close()」在最後一個場景中都不起作用,因為我們沒有直接匯入該函數。要使用它,我們必須將程式碼更改為這樣... ``` from smart_door import open, close open() close() ``` 在之前可怕的嵌套包噩夢中,我們現在可以說“from foo.bar.baz.whatever import doThing”,然後直接使用“doThing()”。或者,如果我們想要一點命名空間,我們可以說“from foo.bar.baz importwhatever”,然後說“whatever.doThing()”。 “導入”系統非常靈活。 但不久之後,您可能會發現自己說“但是我的模組中有數百個函數,我想全部使用它們!”這是許多開發人員偏離軌道的地方,這樣做... ``` from smart_door import * ``` **這非常非常糟糕!** 簡而言之,它直接導入模組中的所有內容,這是一個問題。想像一下下面的程式碼... ``` from smart_door import * from gzip import * open() ``` 你認為會發生什麼事?答案是,「gzip.open()」將是被呼叫的函數,因為這是在我們的程式碼中導入並定義的「open()」的最後一個版本。 `smart_door.open()` 已被 **shadowed** - 我們不能稱之為 `open()`,這意味著我們實際上根本無法呼叫它。 當然,由於我們通常不知道,或者至少不記得每個導入的模組中的*每個*函數、類別和變數,所以我們很容易陷入一堆混亂。 *Python 之禪* 也解決了這個情況... > 顯式優於隱式。 您永遠不必“猜測”函數或變數來自何處。文件中的某個位置應該有程式碼“明確”告訴我們它來自哪裡。前兩個場景證明了這一點。 我還應該提到,早期的 `foo.bar.baz.whatever.doThing()` 場景是 Python 開發人員不喜歡看到的。也來自 *Python 之禪*... > 扁平比嵌套更好。 一些包的嵌套是可以的,但是當你的專案開始看起來像一套精緻的俄羅斯娃娃時,你就做錯了。將模組組織到包中,但保持相當簡單。 # 在您的專案中匯入 我們之前建立的專案文件結構即將「非常方便」。回想一下我的「遺漏」專案... ``` omission-git ├── LICENSE.md ├── omission │ ├── app.py │ ├── common │ │ ├── classproperty.py │ │ ├── constants.py │ │ ├── game_enums.py │ │ └── __init__.py │ ├── data │ │ ├── data_loader.py │ │ ├── game_round_settings.py │ │ ├── __init__.py │ │ ├── scoreboard.py │ │ └── settings.py │ ├── game │ │ ├── content_loader.py │ │ ├── game_item.py │ │ ├── game_round.py │ │ ├── __init__.py │ │ └── timer.py │ ├── __init__.py │ ├── __main__.py │ ├── resources │ └── tests │ ├── __init__.py │ ├── test_game_item.py │ ├── test_game_round_settings.py │ ├── test_scoreboard.py │ ├── test_settings.py │ ├── test_test.py │ └── test_timer.py ├── pylintrc ├── README.md └── .gitignore ``` 在我的“game_round_settings”模組中,由“omission/data/game_round_settings.py”定義,我想使用“GameMode”類別。類別在「omission/common/game_enums.py」中定義。我怎樣才能到達它? 因為我將“omission”定義為包,並將模組組織到子包中,所以實際上非常簡單。在“game_round_settings.py”中,我說...... ``` from omission.common.game_enums import GameMode ``` 這稱為**絕對導入**。它從頂級包“omission”開始,然後進入“common”包,在其中查找“game_enums.py”。 一些開發人員向我提供更像「from common.game_enums import GameMode」的導入語句,並想知道為什麼它不起作用。簡而言之,「data」套件(「game_round_settings.py」所在的位置)不知道其兄弟包。 然而,它確實知道它的父母。正因為如此,Python 有一種叫做「相對導入」的東西,它可以讓我們做同樣的事情,就像這樣... ``` from ..common.game_enums import GameMode ``` `..` 表示“此套件的直接父包”,在本例中為“omission”。因此,導入後退一級,進入“common”,並找到“game_enums.py”。 關於是否使用絕對導入或相對導入有許多爭論。就我個人而言,我更喜歡盡可能使用絕對導入,因為它使程式碼更具可讀性。不過,您可以自己做決定。唯一重要的部分是結果是「顯而易見的」——任何東西的來源都不應該是神秘的。 (繼續閱讀:[Real Python - Python 中的絕對導入與相對導入](https://realpython.com/absolute-vs-relative-python-imports/) 這裡還隱藏著另一個陷阱!在 `omission/data/settings.py` 中,我有這一行: ``` from omission.data.game_round_settings import GameRoundSettings ``` 當然,由於這兩個模組都在同一個包中,我們應該可以直接說“from game_round_settings import GameRoundSettings”,對嗎? *錯誤!* 它實際上無法找到“game_round_settings.py”。這是因為我們正在執行頂級包“omission”,這意味著**搜尋路徑**(Python 查找模組的位置以及順序)的工作方式不同。 但是,我們可以使用相對導入來代替: ``` from .game_round_settings import GameRoundSettings ``` 在這種情況下,單一“.”表示“這個包”。 如果您熟悉典型的 UNIX 檔案系統,這應該開始有意義。 `..` 表示“後一級”,“.` 表示“目前位置”。當然,Python 更進一步:`...` 表示“後兩級”,`....` 表示“後三級”,依此類推。 但是,請記住,這些「等級」不僅僅是簡單的目錄。他們是包裹。如果在一個不是包的普通目錄中有兩個不同的包,則不能使用相對導入從一個包跳到另一個包。為此,您必須使用 Python 搜尋路徑,但這超出了本指南的範圍。 (請參閱本文末尾的文件。) # `__main__.py` 還記得我提到在我們的頂級包中建立一個 `__main__.py` 嗎?這是一個特殊的文件,當我們直接使用 Python 執行套件時會執行該文件。我的“omission”包可以使用“python -m omission”從我的存儲庫的根目錄執行。 這是該文件的內容: ``` from omission import app if __name__ == '__main__': app.run() ``` 是的,實際上就是這樣!我正在從頂級包“omission”導入我的模組“app”。 請記住,我也可以說「來自…」。改為導入應用程式。或者,如果我只想說“run()”而不是“app.run()”,我可以執行“from omission.app import run”或“from .app import run”。最後,只要程式碼可讀,我如何進行導入並沒有太大的技術差異。 (附註:我們可以爭論為我的主要`run()` 函數設定一個單獨的`app.py` 對我來說是否合乎邏輯,但我有我的理由......而且它們超出了本指南的範圍。 ) 首先讓大多數人感到困惑的部分是整個「if __name__ == '__main__'」語句。 Python 沒有太多**樣板** - 必須非常普遍地使用且幾乎不需要修改的程式碼 - 但這是那些罕見的位元之一。 `__name__` 是每個 Python 模組的特殊字串屬性。如果我將“print(__name__)”行貼在“omission/data/settings.py”的頂部,當該模組被導入(並因此執行)時,我們會看到“omission.data.settings”被打印出去。 當模組直接透過「python -m some_module」運作時,模組會被指派一個特殊值「__name__」:「__main__」。 因此,「if __name__ == '__main__':」實際上是在檢查該模組是否以 *main* 模組執行。如果是,它將在條件下執行程式碼。 您可以透過另一種方式看到這一點。如果我將以下內容加入到“app.py”的底部... ``` if __name__ == '__main__': run() ``` ……然後我可以直接透過 `python -m omission.app` 執行該模組,結果與 `python -m omission` 相同。現在`__main__.py`被完全忽略,`omission/app.py`的`__name__`是`"__main__.py"`。 同時,如果我只是執行“python -m omission”,“app.py”中的特殊程式碼將被忽略,因為它的“__name__”現在又是“omission.app”。 看看效果如何? # 總結 我們來複習。 * 每個專案都應該使用 VCS,例如 Git。有很多選項可供選擇。 * 每個 Python 程式碼檔案 (`.py`) 都是一個 **模組**。 * 將您的模組組織到**包**中。每個包必須包含一個特殊的「__init__.py」檔案。 * 您的專案通常應由一個頂級包組成,通常包含子包。該頂級包通常共享您的專案的名稱,並作為專案存儲庫根目錄中的目錄存在。 * **永遠不要**在導入語句中使用「*」。在考慮可能的例外之前,Python 之禪指出「特殊情況並沒有特殊到足以違反規則」。 * 使用絕對或相對導入來引用專案中的其他模組。 * 可執行專案的頂層套件中應該有一個`__main__.py`。然後,您可以使用「python -m myproject」直接執行該套件。 當然,我們可以在建立 Python 專案時使用許多更高級的概念和技巧,但我們不會在這裡討論。我強烈建議閱讀文件: + [Python 參考:導入系統](https://docs.python.org/3/reference/import.html) + [Python 教學:模組](https://docs.python.org/3/tutorial/modules.html) + [PEP 8:Python 風格指南](https://www.python.org/dev/peps/pep-0008/) + [PEP 20:Python 之禪](https://www.python.org/dev/peps/pep-0020/) + [PEP 240:隱式命名空間套件](https://www.python.org/dev/peps/pep-0420/) --- *感謝 `grym`、`deniska` (Freenode IRC `#python`)、@cbrintnall 和 @rhymes (Dev) 提出的修改建議。* --- 原文出處:https://dev.to/codemouse92/dead-simple-python-project-structure-and-imports-38c6

网页文件加载失败如何重试

> 本文主要讲解脚本文件加载失败时的处理,对于其他类型的文件加载错误时,解决方向大致一样 ## 背景 在我们开发网站应用时,我们可能会遇到脚本加载失败的情况,导致脚本加载失败的原因有很多,比如用户的网络问题、终端设备问题、用户浏览器版本等诸多因素。 ## 解决方案 在 JavaScript 中,我们可以创建一个监听来监听脚本加载失败的情况,然后针对加载失败的脚本进行重新加载。 重新加载的方案,一般是通过更换域名来解决。我们给每个脚本添加一个映射关系表,用来在加载失败时匹配新的域名进行重试。 具体的解决方案,下面我一步一步讲解,另外希望大家可以仔细阅读注释中的内容 ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>脚本加载失败如何重试</title> <script> window.addEventListener( "error", // 监听全局错误 function (e) { console.log(e); }, true // 由于脚本加载失败不会冒泡,所以我们要在捕获阶段进行监听 ); </script> </head> <body> <script src="https://www.zowlsat.com/api/1.js"></script> <script src="https://www.qqqqqqq.com/api/2.js"></script> <script src="https://www.zowlsat.com/api/3.js"></script> </body> </html> ``` 此时我们可以在浏览器控制台看到以下效果 ![脚本加载失败如何重试](https://www.zowlsat.com/images/article/1.png) 但是这个监听方法会监听到很多其他的错误,我们只需要监听脚本加载失败的错误,所以我们要通过这个监听事件的参数 e 来判断了 ![脚本加载失败如何重试](https://www.zowlsat.com/images/article/2.png) 根据上图我们可以发现,普通错误的类型是 ErrorEvent,而脚本加载失败的类型是 Event,并且他的 target 会指向 script 标签,所以我们根据这个区别过滤掉其他的错误,这样剩下的情况才是我们需要处理的。 ```js window.addEventListener( "error", function (e) { if (e.target.tagName !== "SCRIPT" || e instanceof ErrorEvent) return; console.log(e); }, true ); ``` 接下来就是如何来实现重新加载,我们先给需要重新加载的域名建立一个映射关系,用于替换映射关系表中的域名。然后就是挨个匹配,当还是加载失败时继续匹配下一个,直到成功为止。 ```js const domainList = ["www.aaaaa.com", "www.bbbbb.com", "www.zowlsat.com"]; const retry = {}; window.addEventListener( "error", function (e) { if (e.target.tagName !== "SCRIPT" || e instanceof ErrorEvent) return; // 创建一个URL对象 const url = new URL(e.target.src); // 获取文件路径 const key = url.pathname; // 假如映射表中没有这个文件路径,那么就初始化一个映射键 if (!(key in retry)) { retry[key] = 0; } // 假如匹配完整个映射表都没重新加载成功,则放弃 const index = retry[key]; if (index >= domainList.length) { return; } // 获取新的完整路径 const domain = domainList[index]; // 替换域名 url.host = domain; // 创建新的script标签 const script = document.createElement("script"); script.src = url.toString(); // 将新的script标签追加到加载失败的script标签之前 document.body.insertBefore(script, e.target); retry[key]++; }, true // 由于脚本加载失败不会冒泡,所以我们要在捕获阶段进行监听 ); ``` 到此为止,我们功能已经基本实现,效果如下图 ![脚本加载失败如何重试](https://www.zowlsat.com/images/article/3.png) 但是有一个很关键的问题,就是假如我 2.js 这个文件中的内容,在 3.js 中要使用,那这样的话,2.js 就必须加载到 3.js 之前,否则就会报错。此时,我们就需要在 2.js 加载失败时,阻塞浏览器的解析,知道重新加载完成或者放弃重新加载时,再继续渲染之后的内容。 那这样的话我们该怎么做呢?🤔 其实很简单,在我们入门 js 时就学到过一个知识点,就是使用`document.write` `document.write`这个方法在解析期间使用的话,会阻塞浏览器的解析,而我们现在就是需要阻塞浏览器解析,那此时我们只需要将创建 script 标签的方法更换为`document.write`方法即可。 修改之后的代码如下: ```js const domainList = ["www.aaaaa.com", "www.bbbbb.com", "www.zowlsat.com"]; const retry = {}; window.addEventListener( "error", function (e) { if (e.target.tagName !== "SCRIPT" || e instanceof ErrorEvent) return; const url = new URL(e.target.src); const key = url.pathname; if (!(key in retry)) { retry[key] = 0; } const index = retry[key]; if (index >= domainList.length) { return; } const domain = domainList[index]; url.host = domain; // 此处加上转译是因为防止编译器识别script标签为结束标签报错 document.write(`\<script src="${url.toString()}">\<\/script>`); // const script = document.createElement("script"); // script.src = url.toString(); // document.body.insertBefore(script, e.target); retry[key]++; }, true ); ``` 现在我们再打开控制台查看,现在js文件按它原来的顺序执行了,这样既不会改变原有的代码逻辑,又可以在可控范围内进行重新加载。 效果如下图: ![脚本加载失败如何重试](https://www.zowlsat.com/images/article/4.png) 以上是简单实现了一个js文件重新加载错误的方案,其实这个方案也可以运用到其他很多类型的文件,不限于js文件。 然后我们还需要更加细化这个方法的话,我们可能还需要考虑到这个script标签是否带有`async`、`defer`等属性,还有诸多需要考虑的点,但是沿着这个方向解决的话,大体是没有问题的。

寫給年輕單身男性軟體工程師:一些感情&自我成長建議

最近我留意到一件事,在我們工程師閒聊群組 https://line.me/ti/g2/nipkjq2WoZPKX5dTn9tE9266aEOt6EOICFGa1g 常常會出現感情方面的困擾&討論,令我非常吃驚,因為這是討論 coding 的群組 但是這個話題出現的頻率相當高,我發現這似乎是「年輕單身男性軟體工程師」相當關心的一個話題 站長畢竟年紀稍大一些,比群組內很多人多活了幾年,是有一些建議,雖然跟 coding 無關,但還是跟各位開發者簡單分享 ## 姿態永遠不要放太低 這點就跟業務在做銷售時一樣,你心裡再怎麼想成交,也絕對不能用超低姿態、苦苦哀求拜託對方成交 過低的姿態會釋放「很缺」的訊號,任何人都會心想「這產品很沒價值,所以業務才姿態這麼低,千萬不能買」 同理,「年輕單身男性軟體工程師」似乎在感情上有成為「舔狗」的傾向,請避免這樣做,絕對只有壞處、沒有好處 如果你真的很想疼女生、找一個女生對她好,我建議你多帶你媽出門走走,週五下班幫你媽多買一份點心、宵夜、禮物,有空多陪你媽聊天即可,就不用擔心都沒有女生陪你聊天 ## 注意力放在自己身上 下一個問題是,姿態很低,背後是源自於缺乏自信 這點我們先談一件事,在網路創業圈,有一句話叫做「Attention Is The New Currency」,注意力就是網路世界的現金 你把注意力放在社群網站,這些網站企業就更有錢;你把注意力放在 IG 網美,這些 IG 網美就更有錢 你把注意力放在哪,會決定誰將過得越來越好 所以,建議把注意力放在自己身上,你就會越來越優質 簡單建議,分三個層面 **知識層面** 工程師應該很擅長吸收新知,這點不難。學有趣知識、學習投資、專精工作、更有智慧、賺更多,都不是壞事 **肉體層面** 叫大家都進健身房,很難、又花錢,不切實際。所以我建議各位路過公園就去拉一下單槓,一週兩次,一次20分鐘就可,免費 上網找一些分難度的教學即可,例如這個 https://www.youtube.com/watch?v=kxdNy86hG5I 你就拉個三個月,工程師打電腦的駝背、姿態、儀態,都會改善,然後你會變強壯 **精神層面** 現代人普遍忽略這點,不過,如果你掌握「發呆」的技術、藝術,也就是所謂的「冥想」、「靜坐」 其實在冷靜程度、自信程度、鎮定程度,會有顯著變化,這比較宗教、靈性,需要一些年紀體會 隨便找一個適合你的靈性練習、或宗教,都可以。如果沒頭緒,我建議逛一逛這個印度阿北的頻道 https://www.youtube.com/@sadhgurutraditionalchinese ## 面對挫折感 人生不如意,十之八九,不要太玻璃心 被拒絕了、丟臉了、意想不到情況發生了,就認了,慢慢消化 回家休息,繼續做你該做的事情就好 千萬不要糾纏、不要挽回、不要當恐怖情人 瀟灑一點,繼續做上面三個層面談到的事情就是了 ## trial and error,你就多練習 這跟工程師學新語言、新套件的情況一樣 出錯很正常,多練習就是,你想在什麼方面進步呢?那就 trial and error,你就多練習 想改善穿搭,多逛街、消費就是。簡單從黑灰白開始配色就是,跟前端 css 色彩配色技巧一樣 想改善談吐,在一些小聚、適合的大眾場合、甚至咖啡廳酒吧,你就去搭訕兩句,尬聊兩句 大多數情況下就是被拒絕、很丟臉,別擔心,不要搞到「讓對方感到害怕」的程度即可,丟人現眼 OK 的,被婉拒就禮貌離開即可 我留意到近幾年有一些昂貴的課程在賺這種錢,其實錢省下來買電玩比較爽,不要被商人隨意利用你的焦慮感 反正就是練習交新朋友、networking 而已,臉皮厚一點,越練習臉皮越厚,然後目的性不需要總是那麼強 熟能生巧,上手了社交技巧,在職場也很有用 ## 關於打電動&看動漫 爽爽打電動,爽爽看動漫,一點問題都沒有,都是很健康的興趣 不要越打越沒自信、越看越空虛就好,要找到真正喜歡的電玩、動漫並且投入 有人傳訊息你就不要理,專心打電動&看動漫,有空再回就好,享受你的興趣就對了 呼應到前面幾點,這種人,會讓很多人覺得「好像是一個很忙的人」,然後,不知怎的,就是比較有魅力 (雖然根本不知道你只是看動畫懶得回而已) ## 結論 這些也是我比較年輕時候,身為「年輕單身男性軟體工程師」的一些觀察&經驗談 簡單分享,希望對大家有幫助

2024 年開源 Discord 計劃

大家好! **祝大家新年快樂** 🥳 我的將會充滿壓力和樂趣。希望你的也是! ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/8f714431-4254-4287-8634-c27c6308c1c2/ezgif.com-resize__43_.gif?t=1703587844) [Discord](https://discord.com?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=the-discord-plan-for-open-source-in-2024) 是一個聊天訊息平台,您可以執行它與開發人員進行互動為您的產品。它可用於行銷、支援或為您的產品建立強大的社群。 在 Novu 期間,我們總是嘗試讓 Discord 伺服器 **「活躍」。** 這是我和很多創辦人都經歷過的事情。 他們希望他們的 Discord 不僅僅是一個 **「支援」** 頻道。 大多數創辦人的目標是讓 Discord 頻道活躍起來,只是透過人們之間的互動,**但是你如何做到這一點?** 以下是您可以在 Gitroom 和 HackSquad Discord 伺服器中嘗試的對我來說效果很好的操作清單。 --- 1\.讓人們回頭購買更多 ---------------------------------- 我想從最簡單的例子開始 - [**Midjourney**](https://www.midjourney.com/?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=the-discord-plan-for-open-source-2024)。 他們的主要產品位於 Discord 上;如果你想產生影像,你必須使用 Discord 機器人來完成。每當人們開始創作圖片時,他們就會接觸到其他人並與他們互動。這是一個天才之舉。 在[Gitroom](https://discord.gitroom.com?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=the-discord-plan-for-open-source-in-2024) 中,我實現了一個機制,供人們啟動他們的內容。由於大多數人每週都會發佈內容 - 他們必須**每週**回來,同時與其他人互動。 這就是為什麼您通常每週一都會在 Gitroom 中看到大量活動。 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c84b9613-4209-49c4-a02e-345d1377aefa/image.png?t=1703588707) --- 2\.製造“非產品”Discord -------------------------------- 當您的 Discord 伺服器類似於「Novu」時,Discord 的訪客會有「這是 Novu 產品。讓我們來談談這個吧,」雖然大多數產品都是這樣做的,但你不必這樣做。 我們的 Discord 伺服器可以是「通知基礎架構」。現在,我們擴大了受眾範圍,為他們提供了一個了解有關通知的所有資訊的地方。 HackSquad 非常活躍,因為它是一個基於開發人員的伺服器,人們可以在其中談論任何事情。 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/00ff23cd-c6d1-4f34-b773-3fb6c2fb2aa1/image.png?t=1703588739) --- 3\.有意圖地加入 -------------------- 最近,我發現在我的通話期間透過我們的 Discord 伺服器進行推廣的人比自己加入的人要活躍得多。 我認為這是建立一個活躍社區的主要秘密 - 雖然你不能與每個人交談,但只有少數人已經相當不錯了,因為他們會很活躍並讓其他人參與其中。 **您可以執行的其他選項是:** * 在 Discord **「入門」** 嚮導中提供更多訊息,以便人們可以了解透過活躍可以得到什麼。 * 在 Discord 連結旁邊顯示一些內容或 YouTube 影片,以便人們可以了解伺服器的價值。 * 標記參加不同活動的人並徵求他們的意見。 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/74fbf3ff-2c78-4257-8af0-d02e6b2208e2/image.png?t=1703588859) 到目前為止,您喜歡這篇文章嗎? 請務必註冊新聞通訊以閱讀下一篇新聞通訊 訂閱 \* 加入您的郵箱,獲得前1000星的影片將發送至您的郵箱 或邀請你的朋友學習【如何取得 GitHub stars】(https://howtogetgithubstars.com) --- 4\.保持 ---------- 讓人們加入您的 Discord 是「**獲取**」過程的一部分,但讓他們留下來是「**保留**」過程的一部分。 在 HackSquad 中,讓人們回到 Discord 會讓他們更加活躍。 **例如:** * 我不會製作網路研討會的 YouTube 直播或 Zoom 直播,而是製作 Discord 直播,然後透過電子報將人們帶回 Discord。 * 我舉辦的可重複活動超出了「如何使用 Novu」的範圍。我實際上會做更多通用的東西來吸引更多的人。 * 我舉辦 Discord 贈品活動,讓開發者回歸。 * 我經常在社群媒體上寫關於成為 Discord 的一部分的好處。 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/e23e84a2-b0a0-473d-af8e-b41a0c820e16/image.png?t=1703588895) --- 5\.指定主持人 ---------------------- 我用 Gitroom 做的第一件事就是在 Discord 上找到第一批活躍且有才華的人,並讓他們成為 mod。這是我讓社區活力提升十倍的最佳措施之一。 一旦社區發展一點,我打算帶來更多的模組。 在 HackSquad 中,我在 1 個月內任命了 10 位版主 - **史上最佳決策。** 我很高興有像[Nathan這樣有才華的人](https://www.linkedin.com/in/nathan-tarbert/?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=the-discord-plan-for-open-source-in-2024) 和[Saurabh](https://www.linkedin.com/in/srbhr/?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=the-discord-plan-for-open-source-in-2024),他們幫助我發展社區並將其整合在一起。 當您給予模組**“力量”**時,它們會自動變得更加活躍 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/056b4451-19ac-4304-9bfa-59a82fa8e1f4/image.png?t=1703588936) --- 6\.建立入職管道 ------------------------------------------- 當有人加入你的 Discord 時,那將是他們最活躍、最積極的時候。 **你應該利用這個時間!** 在 Discord 上問他們一些問題,並讓他們參與一些對話並給予一些意見。 另外,**DM 他們**。雖然這與活躍在 Discord 上無關,但這是了解社群成員並從他們那裡提取一些資訊的絕佳時機: * 為什麼加入? * 你在建造什麼? * 誰告訴你我們的事了? * 為什麼想從這個社區獲得? 如果可以的話,甚至嘗試打一個 10 分鐘的電話。 事情是這樣的:你不是活躍的 **24/7**,但如果你有來自世界各地的模組 - 他們可以幫助你! 只要確保不是所有的模組同時向同一個人發送私訊即可 - 這不太好 🙂 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/cc0d1401-d2ba-4943-bfda-39bac4f1be5f/image.png?t=1703588982) 最後一件事🤣 製造很多錯誤 - 這會讓人們陷入**(當然,我在開玩笑。)** --- ## 我邀請您註冊我的電子報。 若符合以下條件,本通訊對您有好處: - 您正在考慮開源您的產品(或建立新產品)。 - 您正在考慮開啟一個副產品並將其開源(以反映您的主要產品)。 - 您從事科技業,希望在沒有明星/沒有 GitHub 趨勢的情況下實現成長。 這是一份 100% 免費的時事通訊(並且永遠如此)。請隨時註冊: [https://gitroom.com](https://gitroom.com/) ![技術](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86ywkzncq6erg44d6xy9.gif) --- 原文出處:https://dev.to/github20k/the-discord-plan-for-open-source-in-2024-2596

🌌 31 個開源庫 + Good First Issues(開始你的旅程)⛰️

為優秀的開源庫做出貢獻是建立您的作品集並加入令人驚嘆的社群的最佳方式。 我編譯了 31 個開源程式庫和一些好的第一期,以幫助推動您的旅程。 不要忘記加星號並支持這些🌟 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/637xnt75fuwgfeaasdke.gif) --- #AI最愛🦾: ### 1. [CopilotKit](https://github.com/CopilotKit/CopilotKit) - 應用內 AI 聊天機器人與 AI 文字區域 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ox3mv8nmqzot6m4kvkdh.png) 開源平台,用於使用兩個 React 元件將關鍵 AI 功能整合到 React 應用程式中。 CopilotPortal:應用程式內人工智慧聊天機器人,可以「查看」當前應用程式狀態並採取行動。 CopilotTextarea:AI 驅動的 <textarea /'> 替換。具有自動完成、插入和生成功能。 ###[好第一期:](https://github.com/CopilotKit/CopilotKit/issues/62) ``` Gracefully fail if CopilotProvider is omitted The bug: Virtually every CopilotKit functionality depends on a CopilotContext provided by the CopilotProvider. e.g. CopilotTextarea autocompletions, chatbot, etc. However when a CopilotProvider does not wrap the component, functionality fails silently. To Reproduce 1. Omit <CopilotProvider>...</CopilotProvider> 2. trigger useMakeCopilotReadable, useMakeCopilotActionable, CopilotTextarea, CopilotSidebarUIProvider 3. See how functionality does not work, but no error is emitted Expected behavior An error is emitted, with clear description of the likely core issue and how to resolve it (namely, wrap the app in a CopilotProvider). Point to docs. ``` {% cta https://github.com/CopilotKit/CopilotKit %} Star CopilotKit ⭐️ {% endcta %} --- ###2.[PortKeyAI](https://github.com/Portkey-AI/gateway){% embed https://github.com/Portkey-AI/gateway no-readme %} ###3.[Pezzo.ai](https://github.com/pezzolabs/pezzo){% 嵌入 https://github.com/pezzolabs/pezzo no-readme %} ###4.[OpenVoice](https://github.com/myshell-ai/OpenVoice){% 嵌入 https://github.com/myshell-ai/OpenVoice no-readme %} ###5.[LLMCourse](https://github.com/mlabonne/llm-course){% 嵌入 https://github.com/mlabonne/llm-course no-readme %} --- &nbsp; #雲端和資料庫☁️ ### 6. [Winglang](https://github.com/winglang/wing) - 雲端導向的程式語言 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gvfykepsj1tszs8260wj.png) Wing 是一種用於雲端應用程式的程式語言。 它結合了雲端基礎設施和應用程式的程式碼,使雲端服務開發變得更加容易。 Wing 獨特的執行模型和測試模擬器有助於高效建置和部署雲端應用程式。 ###[第一期好:](https://github.com/winglang/wing/issues/4998) ``` Support Array.sort() method Feature Spec: let arr: MutArray<num>=[2, 1, 3, 9, 6, 4]; arr.sort(); log("${arr}"); // it should print sorted array in ascending order, eg: [1, 2, 3, 4, 6, 9] Component: Wing SDK Community Notes: If you are interested to work on this issue, please leave a comment. If this issue is labeled needs-discussion, it means the spec has not been finalized yet. Please reach out on the #dev channel in the Wing Slack. ``` {% cta https://github.com/winglang/wing %} 星翼朗 ⭐️ {% endcta %} --- ### 7. [StackQL](https://github.com/stackql/stackql) - 以 SQL 為基礎的雲端資源管理 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdtf51ekap09idn80xnh.png) StackQL 提供了一個獨特的 SQL 為基礎的框架來管理和查詢跨不同提供者(例如 Google、AWS、Azure 等)的雲端資源和 API。 它允許使用類似 SQL 的命令來配置和操作雲端服務,從而簡化了雲端操作。 這使得 StackQL 成為雲端資源管理和互動的多功能工具,特別是對於熟悉 SQL 的人來說。 ###[好第一期:](https://github.com/stackql/stackql/issues/280) ``` Add unit testing to package writer Add unit testing for internal/stackql/writer . Description: add implementation for testing sql_writer.go modify sql_writer.go by adding function for dependency injection add implementation for testing generic.go modify generic.go by adding variable for patching GetDB function modify entryutil.go to adjust sql_writer.go ``` {% cta https://github.com/stackql/stackql/ %} Star StackSQL ⭐️ {% endcta %} --- ###8.[Appwrite](https://github.com/appwrite/appwrite){% 嵌入 https://github.com/appwrite/appwrite no-readme %} ###9.[Supabase](https://github.com/supabase/supabase){% 嵌入 https://github.com/supabase/supabase no-readme %} ###10.[SuperDuperDB](https://github.com/SuperDuperDB/superduperdb){% 嵌入 https://github.com/SuperDuperDB/superduperdb no-readme %} --- &nbsp; #開發實用程式🛠️ ### 11. [Firecamp](https://github.com/firecamp-dev/firecamp) - 多協定 API 協作工具 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adt6n8uv5dseylmemng0.png) Firecamp 是一款多功能 API 開發工具,支援 Rest、GraphQL 和 WebSockets 等協定。 它簡化了 API 的設計、測試和記錄,並增強了 API 專案的團隊協作。 ###[好第一期:](https://github.com/firecamp-dev/firecamp/issues/137) ``` Help out with Manual Testing of Firecamp Responsibilities: Executing test cases and reporting results Logging bugs and issues in the Github issue tracker Providing feedback on usability and the testing process Suggesting improvements to tests and expanding test coverage. Benefits Benefits for your testing profile and career: Experience testing a real-world open source application Each release will include your name with bugs fixes. Exposure to different types of testing such as UI, API, integration, etc Opportunity to have your contributions and feedback incorporated into the product Collaborating with an open source community Having your testing work visible to potential employers Firecamp Swags (T-shirts and stickers) Community shoutout and promotion ``` {% cta https://github.com/firecamp-dev/firecamp %} 星際火營 ⭐️ {% endcta %} --- ###12.[Odigos](https://github.com/keyval-dev/odigos){% 嵌入 https://github.com/keyval-dev/odigos no-readme %} ###13.[Digger](https://github.com/diggerhq/digger){% 嵌入 https://github.com/diggerhq/digger no-readme %} ###14.[鏡像](https://github.com/metalbear-co/mirrord){% 嵌入 https://github.com/metalbear-co/mirrord no-readme %} --- &nbsp; #後端⚙️ ### 15. [Cerbos](https://github.com/cerbos/cerbos) - 可擴充、與語言無關的授權 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cljttnnxua54lyg4w65x.png) Cerbos 提供獨特、可擴展的解決方案,用於在應用程式中實施特定於上下文的使用者權限。 其靈活的、與語言無關的方法可以輕鬆整合和管理複雜的授權結構。 與眾不同的是,Cerbos 簡化了存取控制策略的開發,使其更能適應各種應用需求。 ###[第一期好:](https://github.com/cerbos/cerbos/issues/1920) ``` Produce output when the rule condition is not satisfied Currently the output block is only evaluated if the rule is actually activated (action, roles and conditions are satisfied). In certain situations, it's desirable to produce output when the rule is nearly activated (action and roles match but the condition is not satisfied). In order to maintain backward compatibility, reduce noise, and to keep policy execution as fast as possible (outputs incur a tiny overhead), the proposal is to let users add an optional when section to the output block to opt into this behaviour. - actions: ['view'] effect: EFFECT_ALLOW roles: ['user'] condition: match: expr: timestamp(R.expiry_date) > now() output: expr: > format("%d hours until expiry", (timestamp(R.expiry_date) - now()).getHours()) when: cond_fail: > format("expired on %s", R.expiry_date) When evaluating the above rule, if the action, roles and condition match, output will be the result of evaluating output.expr If the condition is not satisfied, output will be the result of evaluating output. when.cond_fail if it exists. Otherwise no output will be produced. ``` {% cta https://github.com/cerbos/cerbos %} 明星 Cerbos ⭐️ {% endcta %} --- ###16.[Novu](https://github.com/novuhq/novu){% 嵌入 https://github.com/novuhq/novu no-readme %} ###17.[Trigger.dev](https://github.com/triggerdotdev/trigger.dev){% 嵌入 https://github.com/triggerdotdev/trigger.dev no-readme %} ###18.[SuperTokens](https://github.com/supertokens/supertokens-core){% 嵌入 https://github.com/supertokens/supertokens-core no-readme %} ###19.[Wazuh](https://github.com/wazuh/wazuh){% 嵌入 https://github.com/wazuh/wazuh no-readme %} --- &nbsp; #UI/UX🦋: ### 20. [Flowbite](https://github.com/themesberg/flowbite) - 頂級 CSS 元件庫 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98dwqyrhf1pbiqkpko8g.png) 最好、最受尊敬的 UI 元件庫之一。 基於實用優先的 CSS 框架。 易於使用,充滿重要的支援和模板。 {% cta https://github.com/themesberg/flowbite %} 明星 Flowbite ⭐️ {% endcta %} &nbsp; ###21.[MaterialUI](https://github.com/mui/material-ui) - 使用 Google 的 Material Design 實現的基礎 React 元件 {% 嵌入 https://github.com/mui/material-ui no-readme %} &nbsp; ###22。 [SwiperUI](https://github.com/nolimits4web/swiper) - 用於實現行動滑動 UI 的受人尊敬的庫 {% 嵌入 https://github.com/nolimits4web/swiper no-readme %} &nbsp; ###23.[ReactSpring](https://github.com/pmndrs/react-spring) - 在 React 中實現具有真實物理效果的動畫 {% 嵌入 https://github.com/pmndrs/react-spring no-readme %} --- &nbsp; #雜項🎨 ### 24. [SwirlSearch](https://github.com/swirlai/swirl-search) - 多源人工智慧資料搜尋器 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thplxod3d4vh1qq5hhpa.jpeg) Swirl 是一款由人工智慧驅動的搜尋工具,可同時查詢多個資料來源,包括資料庫和公用資料服務。 它使用人工智慧對結果進行排名並產生見解,從而可以跨不同的資料儲存庫進行全面搜尋。 Swirl 一次簡化了對各種來源的資料的搜尋和分析,使其成為資料驅動洞察的獨特工具。 ###[第一期好:](https://github.com/swirlai/swirl-search/issues/789) ``` Add a Connector: Yahoo search It would help to search anything with Swirl on Yahoo effectively. Locate and read a bit in their search API first. You might just need to make a new SearchProvider configration vs. a new Connector. Their docs should help guide you a bit in which way you might need to go. ``` {% cta https://github.com/swirlai/swirl-search/ %} Star SwirlSearch ⭐️ {% endcta %} --- ### 25. [Wasp](https://github.com/wasp-lang/wasp) - 使用 React 和 Node.js 開發全端 Web 應用程式 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/54jp6j6r8ils6we97i0f.png) 使用 React 和 Node.js 進行快速全端 Web 應用程式開發。 Wasp 提供了一種建立現代 Web 應用程式的簡化方法,將前端的 React 和後端的 Node.js 結合在一個緊密結合的框架中。 ###[好第一期:](https://github.com/wasp-lang/wasp/issues/874) ``` Add images (or link to the example app) of auth UI helpers Wasp provides At this point in docs (also in the tutorial if we're using it), it would be nice to add an image of UI helpers for Auth (login/signup form, Google/GitHub button, ...) so developers can immediately see what they are getting and how nice it looks. ``` {% cta https://github.com/wasp-lang/wasp %} 星黃蜂 ⭐️ {% endcta %} ###26.[Logstash](https://github.com/elastic/logstash) {% 嵌入 https://github.com/elastic/logstash 無自述文件 %} ###27.[Snapify](https://github.com/MarconLP/snapify) {% 嵌入 https://github.com/MarconLP/snapify 無自述文件 %} --- &nbsp; #為了好玩🎭 ###28.[Twitter 的演算法](https://github.com/twitter/the-algorithm){% embed https://github.com/twitter/the-algorithm no-readme %} ###29.[十億行挑戰](https://github.com/gunnarmorling/1brc){% embed https://github.com/gunnarmorling/1brc no-readme %} ###30.【秘密知識之書】(https://github.com/trimstray/the-book-of-secret-knowledge){% embed https://github.com/trimstray/the-book-of -秘密知識無自述文件%} ###31.[GenAI 初學者](https://github.com/microsoft/generative-ai-for-beginners){% 嵌入 https://github.com/microsoft/generative-ai-for-beginners no -自述文件%} --- 原文出處:https://dev.to/copilotkit/31-open-source-libraries-to-kickstart-your-journey-4hhd

关于 JS 的一些高级用法

在学习 JavaScript中,变量、函数、类、循环、异步这些都是基础知识。这些基础知识是我们使用 JavaScript 的基础。但是,在日常的业务开发中,我们需要一些更高级的技巧来更好地解决问题。 > 通过本文你将了解到 JS 的高级知识点以及实际应用技巧,如高级数据结构和算法、函数式编程、异步编程和面向对象编程。我们会利用代码实例来让大家更好地理解这些知识点。同时,我们也会提供一些实战案例的示范和使用技巧,让你更好地将这些技术应用到实际业务中。 ## 高级数据结构和算法 ### Map 和 Set 数据结构 在 JavaScript 中,Map 数据结构通常用于存储键值对,它可以使用任意类型作为键和值。Set 数据结构用于存储唯一值的集合。 ```js // 创建Map对象 const map = new Map(); // 设置键值对 map.set("name", "Tom"); map.set("age", 20); // 获取键值对 console.log(map.get("name")); // 'Tom' console.log(map.get("age")); // 20 // 创建Set对象 const set = new Set(); // 添加元素 set.add(10); set.add(20); set.add(30); // 删除元素 set.delete(20); // 判断元素是否存在 console.log(set.has(10)); // true console.log(set.has(20)); // false ``` ### 堆、栈和队列 堆和栈是常用的内存分配方式。栈是一种后进先出的数据结构,堆是一种动态分配的内存结构。队列是一种先进先出的数据结构,它通常用于缓存和并发编程中。 ```js // 使用数组模拟堆 const arr = [1, 2, 3, 4]; arr.push(5); // 入堆 console.log(arr.pop()); // 出堆 // 使用数组模拟栈 const stack = [1, 2, 3, 4]; stack.push(5); // 入栈 console.log(stack.pop()); // 出栈 // 使用数组模拟队列 const queue = [1, 2, 3, 4]; queue.push(5); // 入队 console.log(queue.shift()); // 出队 ``` ### 深度优先搜索和广度优先搜索 深度优先搜索(DFS)和广度优先搜索(BFS)是常用的遍历算法。DFS 通常用于解决深度问题,BFS 适用于宽度问题。 ```js // 深度优先遍历 function dfs(node) { if (node == null) return; console.log(node.value); dfs(node.left); dfs(node.right); } // 广度优先遍历 function bfs(node) { const queue = [node]; while (queue.length) { const curr = queue.shift(); console.log(curr.value); if (curr.left) queue.push(curr.left); if (curr.right) queue.push(curr.right); } } ``` ### 常用算法 常用的算法有排序、搜索、查找等。 ```js // 排序算法:快速排序使用分治思想,通过把数组分成较小的块来排序。 function quickSort(arr) { if (arr.length < 2) { return arr; } let pivot = arr[0]; let left = []; let right = []; for (let i = 1; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } // 查找算法: function binarySearch(arr, target) { let left = 0; let right = arr.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); if (arr[mid] === target) { return mid; } else if (arr[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; } ``` ## 函数式编程 ### 高阶函数和柯里化 高阶函数和柯里化是函数式编程中的常见概念,它们可以让我们创建更加抽象、灵活的函数。 ```js // 高阶函数 function higherOrderFunction(func) { return function (num) { return func(num); }; } function double(num) { return num * 2; } const doubleFunc = higherOrderFunction(double); console.log(doubleFunc(10)); // 20 // 柯里化 function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function (...args2) { return curried.apply(this, [...args, ...args2]); }; } }; } function sum(a, b, c) { return a + b + c; } const curriedSum = curry(sum); console.log(curriedSum(1)(2)(3)); // 6 ``` ### 闭包和作用域 闭包和作用域是 JavaScript 中比较常见的概念。闭包可以让我们维护函数内的状态,作用域则决定了变量的可见范围。 ```js // 闭包 function closure() { let i = 0; return function () { return ++i; }; } const func = closure(); console.log(func()); // 1 console.log(func()); // 2 // 作用域 let a = 10; function foo() { let a = 20; console.log(a); // 20 } foo(); console.log(a); // 10 ``` ### 函数式编程中的常见模式 函数式编程中有很多常见的模式,如 map、filter、reduce 等。 ```js // map const arr = [1, 2, 3]; const mapArr = arr.map((item) => item * 2); console.log(mapArr); // [2, 4, 6] // filter const filterArr = arr.filter((item) => item > 1); console.log(filterArr); // [2, 3] // reduce const reduceArr = arr.reduce((sum, curr) => sum + curr, 0); console.log(reduceArr); // 6 异步编程 Promise和async/await Promise和async/await是常见的异步编程方式,它们可以让我们更好地处理异步编程中的问题。 // Promise function promise() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('done'); }, 1000); }); } promise().then((result) => console.log(result)); // 'done' // async/await async function asyncFunc() { const result = await promise(); console.log(result); } asyncFunc(); // 'done' ``` ### 事件循环和 EventEmitter 事件循环和 EventEmitter 用于处理异步事件,它们可以让我们更好地处理事件流。 ```js // 事件循环 console.log("start"); setTimeout(() => { console.log("setTimeout"); }, 0); Promise.resolve().then(() => console.log("promise")); console.log("end"); // EventEmitter const { EventEmitter } = require("events"); const emitter = new EventEmitter(); emitter.on("doSomething", (arg1, arg2) => { console.log(`${arg1} ${arg2}`); }); emitter.emit("doSomething", "Hello", "World"); // 'Hello World' ``` ### Web Worker Web Worker 可以让我们将长时间运行的任务移出主线程,以避免阻塞 UI。 ```js // 主线程 const worker = new Worker("worker.js"); worker.onmessage = (event) => { console.log(event.data); }; worker.postMessage("start"); // worker.js self.onmessage = (event) => { const result = longCalculation(event.data); self.postMessage(result); }; ``` ## 面向对象编程 ### 类和继承 JavaScript 中的类和继承与其他面向对象编程语言类似。 ```js // 类 class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Cat extends Animal { constructor(name, breed) { super(name); this.breed = breed; } speak() { console.log(`${this.name} meows.`); } get description() { return `${this.name} is a ${this.breed} cat.`; } set nickname(nick) { this.name = nick; } } const cat = new Cat("Fluffy", "Persian"); cat.speak(); // 'Fluffy meows.' console.log(cat.description); // 'Fluffy is a Persian cat.' cat.nickname = "Fuffy"; console.log(cat.name); // 'Fuffy' ``` ### Encapsulation、Inheritance、Polymorphism(封装、继承、多态) 封装、继承、多态是面向对象编程中的重要概念。 ```js // 封装 class Person { constructor(name) { this._name = name; } get name() { return this._name.toUpperCase(); } set name(newName) { this._name = newName; } } const person = new Person("John"); console.log(person.name); // 'JOHN' person.name = "Lisa"; console.log(person.name); // 'LISA' // 继承 class Shape { constructor(color) { this.color = color; } draw() { console.log("Drawing a shape..."); } } class Circle extends Shape { constructor(color, radius) { super(color); this.radius = radius; } draw() { console.log(`Drawing a ${this.color} circle with radius ${this.radius}.`); } } const circle = new Circle("red", 10); circle.draw(); // 'Drawing a red circle with radius 10.' // 多态 function drawShape(shape) { shape.draw(); } drawShape(new Shape("blue")); // 'Drawing a shape...' drawShape(new Circle("green", 20)); // 'Drawing a green circle with radius 20.' ``` ## 总结和实战 在本文中,我们介绍了一些 JavaScript 的高级知识点,如高级数据结构和算法、函数式编程、异步编程和面向对象编程。我们还提供了一些代码示例和实战案例,让大家更好地理解和掌握这些技术。 ### 通过 Promise.all 实现并发请求 ```js function fetchData(urls) { const promises = urls.map((url) => fetch(url)); return Promise.all(promises).then((responses) => Promise.all( responses.map((response) => { if (!response.ok) throw new Error(response.statusText); return response.json(); }) ) ); } ``` ### 使用 async/await 实现异步调用 ```js async function getData(url) { const response = await fetch(url); if (!response.ok) throw new Error(response.statusText); const data = await response.json(); return data; } ``` ### 在面向对象编程中使用工厂模式 ```js class Product { constructor(name, price) { this.name = name; this.price = price; } } class ProductFactory { createProduct(name, price) { return new Product(name, price); } } const productFactory = new ProductFactory(); const product = productFactory.createProduct("Apple", 1); console.log(product.name); // 'Apple' console.log(product.price); // 1 ``` 本文结束,感谢阅读

📚 前 1% 的 React 開發者使用的 8 個儲存庫 🏆

你好👋 今天,讓我們來看看**前 1% 的開發人員使用**的 8 個 React 儲存庫(以及那些您可能從未聽說過的儲存庫)。 準備好? ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5yehweju0i54ov2n6bwt.gif) --- # 我們如何找到前 1% 的開發人員使用的儲存庫? 🔦 我們如何找到最好的開發人員使用的東西背後的故事植根於大量的資料探勘和一些重要的建模。 現在,在 Quine,我們根據開發人員的**[DevRank](https://docs.quine.sh/for-developers/devrank)** 對開發人員進行排名。 簡單來說,DevRank 使用 [Google 的 PageRank 演算法](https://en.wikipedia.org/wiki/PageRank) 根據開發人員對開源儲存庫的貢獻來衡量開發人員在開源領域的重要性。 為了建立此列表,我們查看了前 1% 已加星號的儲存庫。 🌟 然後,我們計算了前 1% 的開發者會為回購加註星標的可能性,與後 50% 的開發者不支持的可能性進行比較。 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/miugcnqpataix1fsq6hb.png) 最後,經過一番挑選,我們找到了以下 8 個儲存庫。 :向下點: 當您想要建立很酷的網頁應用程式時,這些儲存庫將特別有用。** 如果您有興趣建立小型應用程式,並且喜歡應用人工智慧方面,我們建議您查看 Creator Quests,這是一項**開源挑戰,獎勵開發人員使用 ChatGPT、Claude、Gemini 建立酷炫的 GenerativeAI 應用程式**和更多。 :upside_down_face: 💰 最新的 Creator Quest 挑戰您使用生成式 AI 建立開發人員工具。要參與,只需註冊 [Quine](https://quine.sh/?utm_source=devto&utm_campaign=best_react_repos) 並前往 _Quests_。 **目前獎金池為$2028**,並且隨著更多參與者的加入,獎金池將會增加!點擊下面的圖片並嘗試! ⬇️ [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/akiuhk62zctvf3b9gilx.png)](https://quine.sh/?utm_source=devto&utm_campaign=best_react_repos) --- # jsxstyle/jsxstyle **不再有 JS 到 CSS 的跳躍** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h75mskqja5bcwst05e93.png)](https://github.com/jsxstyle/jsxstyle) **為什麼要關心?** 在 Web 開發中,使用 React 或 Preact,您必須設定元件的樣式(如按鈕、選單等)。傳統上,這是使用單獨的 CSS 檔案或複雜的樣式系統來完成的,這可能非常耗時且管理起來很麻煩。 jsxstyle 可讓您直接在 JavaScript 程式碼中以及元件中定義樣式,從而簡化了此過程。換句話說,這意味著您不再需要在 JS 和 CSS 檔案之間跳躍。 **設定**:`npm install jsxstyle` **範例用例**:您的程式碼可以如下所示。 👇 ``` <Row padding={15}> <Block backgroundColor="#EEE" boxShadow="inset 0 0 0 1px rgba(0,0,0,0.15)" borderRadius={5} height={64} width={64} marginRight={15} backgroundSize="contain" backgroundImage="url(http://graph.facebook.com/justinbieber/picture?type=large)" /> <Col fontFamily="sans-serif" fontSize={16} lineHeight="24px"> <Block fontWeight={600}>Justin Bieber</Block> <Block fontStyle="italic">Canadian</Block> </Col> </Row> ``` [https://github.com/jsxstyle/jsxstyle](https://github.com/jsxstyle/jsxstyle) --- # 💨 alangpierce/蔗糖酶 **Babel 的超快替代品** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rk9ceq6mlw8ya0f2icb8.png)](https://github.com/alangpierce/sucrase) **為什麼你應該關心?** Babel 是 Web 開發中廣泛使用的工具,可將現代 JavaScript 程式碼轉換為舊瀏覽器可以理解的格式。 Sucrase 是 Babel 更快的替代品。 **設定**: ``` yarn add --dev sucrase # Or npm install --save-dev sucrase node -r sucrase/register main.ts ``` **用例範例**:Sucrase 可以直接從 JS 呼叫: ``` import {transform} from "sucrase"; const compiledCode = transform(code, {transforms: ["typescript", "imports"]}).code; ``` [https://github.com/alangpierce/sucrase](https://github.com/alangpierce/sucrase) --- # 🎨 woorm/折射鏡 **我為您的網頁程式碼著色,讓您的生活更輕鬆** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzwpgi5t47o93kvcbtdq.png)](https://github.com/wooorm/refractor) **為什麼你應該關心?** Refractor 很重要,因為它允許您加入突出顯示,從而增強專案的可讀性;尤其是當您將程式碼片段新增至 Web 應用程式時。它允許您用 270 多種程式語言表達程式碼,並且在傳統的基於 HTML 的突出顯示不理想的領域(例如 CLI 表單)特別有用。 **設定**:`npm install refractor` **用例範例**: ``` import {refractor} from 'refractor' const tree = refractor.highlight('"use strict";', 'js') console.log(tree) ``` **產量**: ``` { type: 'root', children: [ { type: 'element', tagName: 'span', properties: {className: ['token', 'string']}, children: [{type: 'text', value: '"use strict"'}] }, { type: 'element', tagName: 'span', properties: {className: ['token', 'punctuation']}, children: [{type: 'text', value: ';'}] } ] } ``` [https://github.com/wooorm/refractor](https://github.com/wooorm/refractor) --- # 🐦 react-static-tweets **您在網站上加入推文的最佳選擇。** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1lvul78znx84ph479fa1.png)](https://github.com/transitive-bullshit/react-static-tweets) **為什麼你應該關心?** 將推文加入到您的網站是您在許多登陸頁面上看到的一項很酷的功能。 React Static Tweets 很重要,因為它提供了一種在 Web 專案中嵌入推文的高效方法,與 Twitter 的標準嵌入方法相比,提供更快的載入時間和更好的效能。 **設定**: ``` npm install react-static-tweets static-tweets date-fns # or yarn add react-static-tweets static-tweets date-fns ``` **用例範例:** ``` import React from 'react' import { fetchTweetAst } from 'static-tweets' import { Tweet } from 'react-static-tweets' const tweetId = '1358199505280262150' export const getStaticProps = async () => { try { const tweetAst = await fetchTweetAst(tweetId) return { props: { tweetAst }, revalidate: 10 } } catch (err) { console.error('error fetching tweet', err) throw err } } export default function Example({ tweetAst }) { return <Tweet ast={tweetAst} /> } ``` [https://github.com/transitive-bullshit/react-static-tweets](https://github.com/transitive-bullshit/react-static-tweets) --- # 🖨️ preactjs/preact-render-to-string **以 HTML 形式呈現您的元件** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7djwj6w7nqwfnifc43c.png)](https://github.com/preactjs/preact-render-to-string) **為什麼要關心?** 「preact-render-to-string」是一個工具,可以幫助網站更快地載入並在搜尋引擎中更好地顯示。使用 Preact 等 JS 框架建立的網站需要一段時間才能顯示內容,因為瀏覽器必須先執行 JavaScript。此儲存庫透過將元件轉換為現成的 HTML 來完成伺服器端的繁重工作。因此,當有人造訪該網站時,即使網路速度很慢,他們也會立即看到內容。 **設定**:`npm install preact-render-to-string` **用例範例:** ``` import { render } from 'preact-render-to-string'; import { h, Component } from 'preact'; /** @jsx h */ // Classical components work class Fox extends Component { render({ name }) { return <span class="fox">{name}</span>; } } // ... and so do pure functional components: const Box = ({ type, children }) => ( <div class={`box box-${type}`}>{children}</div> ); let html = render( <Box type="open"> <Fox name="Finn" /> </Box> ); console.log(html); // <div class="box box-open"><span class="fox">Finn</span></div> ``` [https://github.com/preactjs/preact-render-to-string](https://github.com/preactjs/preact-render-to-string) --- # 🏆 自行車刮鬍/曲柄 **唯一的 JavaScript 框架** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8hp1ex1qh1sv6isksaq.png)](https://github.com/bikeshaving/crank) **為什麼要關心?** 在像 React 這樣的傳統 Web 框架中,Web 元件配置一次,僅在明確指定時才更改。它們看起來像是需要手動更新的靜態影像。 Crank.js 透過允許小部件更新自身以回應新資料來改變這一點,類似於用新新聞刷新的新聞收報機。這對於管理即時資料(例如即時體育賽事比分或產品更新)的 Web 應用程式尤其有用。 這個倉庫需要更多的人遷移到這裡才能獲得關注,但它仍然是一個非常酷的倉庫,值得關注。 👀 **設定**:`$ npm i @b9g/crank` **用例範例**: ``` import {renderer} from "@b9g/crank/dom"; function Greeting({name = "World"}) { return ( <div>Hello {name}</div> ); } renderer.render(<Greeting />, document.body); ``` [https://github.com/bikeshaving/crank](https://github.com/bikeshaving/crank) --- # 🎯 evoluhq/evolu **我是一個本地第一的人** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k15m25pi0w9pk0g54zrn.png)](https://github.com/evoluhq/evolu) **為什麼要關心?** Web 應用程式通常依賴在伺服器上儲存使用者資料,這需要持續的網路連接,並引起對隱私和資料安全的擔憂。這種基於伺服器的方法也意味著如果伺服器發生故障或公司停止運營,效能會降低,並且可能會遺失資料。 Evolu 引入了「本地優先」方法,其中資料直接儲存在使用者的裝置上。這意味著您的應用程式可以離線工作,更快地存取資料,並提供增強的隱私和安全性。如果您正在建立離線 Chrome/瀏覽器應用程式,這將非常有用。 **設定**:` npm install @evolu/react` 要開始使用它,您可以在[此處](https://www.evolu.dev/docs/quickstart)找到這個很棒的指南。 [https://github.com/evoluhq/evolu](https://github.com/evoluhq/evolu) --- # 📸 笑話社群/快照差異 **我比較你們的元件並突出顯示差異** [![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hy76comkwkqkt0d5qn8z.png)](https://github.com/jest-community/snapshot-diff) **為什麼要關心?** 在測試 React 元件或其他 JavaScript 值時,開發人員通常會比較整個狀態或輸出。這意味著處理大量資料,查找特定變更就像大海撈針一樣。 Snapshot-diff 是重點比較工具,可讓您取得元件的兩種不同狀態(或任兩個 JavaScript 值)並直接比較它們,僅將差異突出顯示。 這對於測試 React 元件特別有幫助,因為它可以準確指出兩種狀態之間發生的變化,從而更容易辨識和理解程式碼變更的影響。 **設定**:`yarn add --dev snapshot-diff` **範例用例:** 預設笑話匹配器 ``` const snapshotDiff = require('snapshot-diff'); test('snapshot difference between 2 strings', () => { expect(snapshotDiff(a, b)).toMatchSnapshot(); }); const React = require('react'); const Component = require('./Component'); test('snapshot difference between 2 React components state', () => { expect( snapshotDiff(<Component test="say" />, <Component test="my name" />) ).toMatchSnapshot(); }); ``` [https://github.com/jest-community/snapshot-diff](https://github.com/jest-community/snapshot-diff) --- **我希望這些發現對您有價值,並將有助於建立更強大的 React 工具包!⚒️** 如果您今天想利用這些工具來獲得獎勵,我們剛剛發起了一項使用生成式人工智慧建立開發人員工具的挑戰。 如果對此有興趣,請登入 [Quine](https://quine.sh/?utm_source=devto&utm_campaign=best_react_repos) 並發現任務! 💰 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5drisgbolxfnzfvtwzw.gif) 最後,請**考慮透過加星號來支持這些專案。 ⭐️** PS:我們與他們沒有任何關係。我們只是認為偉大的專案值得高度認可。 下週見, 您的開發夥伴💚 巴普 --- 原文出處:https://dev.to/quine/8-repos-used-by-the-top-1-of-react-devs-2758

🎉像專業人士一樣監控您的 Javascript 應用程式🧙‍♂️💫

## **簡介** 在本教程中,您將學習如何使用**現代工具**和**最佳實踐**來監控您的Javascript應用程式。 探索分散式追蹤的力量,並了解如何無縫整合和利用 Odigos 和 Jaeger 等工具來增強您的監控能力。 **您將學到什麼:✨** - 如何在 Javascript 中建立微服務🐜。 - 為微服務設定 Docker 容器📦。 - 配置 Kubernetes ☸️ 以管理微服務。 - 整合追蹤後端以可視化追蹤🔍。 您準備好成為監控 JS 應用程式的**專家**了嗎? 😍 說**是的,先生!**。 我聽不到你說話。大聲點說。 🙉 ![大聲點 GIF](https://media.giphy.com/media/8m5dizh7ghyEPIWIx1/giphy.gif) *** ## **讓我們設定一下 🦄** > 🚨 在部落格的這一部分中,我們將建立一個虛擬的 JavaScript 微服務應用程式並將其部署在本地 Kubernetes 上。如果您已經有一個並且正在跟進,請隨意跳過這一部分。 為您的應用程式建立初始資料夾結構,如下所示。 👇🏻 ``` mkdir microservices-demo cd microservices-demo mkdir src cd src ``` ### **設定伺服器** 🖥️ > 👀 出於演示目的,我將建立兩個相互通信的微服務,最終我們可以使用它來視覺化分散式追蹤。 - **建置與 Dockerize 微服務 1** 在「/src」資料夾中,建立一個新資料夾「/microservice-1」。在資料夾內初始化 **NodeJS** 專案並安裝所需的依賴項。 ``` mkdir microservice-1 cd microservice-1 npm init -y npm install --save express node-fetch ``` 建立一個新檔案“index.js”並新增以下程式碼: ``` // 👇🏻/src/microservice-1/index.js const express = require("express"); const fetch = require("node-fetch") const app = express(); const PORT = 3001; app.use(express.json()); app.get("/", async (req, res) => { try { const response = await fetch("http://microservice2:8081/api/data"); const data = await response.json(); res.json({ data: "Microservice 2 data received in Microservice 1", microservice2Data: data, }); } catch (error) { console.error(error.message); res.status(500).json({ error: "Internal Server Error" }); } }); app.listen(PORT, () => { console.log(`Microservice 1 listening on port ${PORT}`); }); ``` 伺服器正在偵聽連接埠“3001”,並且在對“/”發出請求時,我們從“microservice2”請求資料並將回應作為 JSON 物件返回。 📦 現在,是時候對這個微服務進行 docker 化了。在“/microservice-1”資料夾中建立一個新的“Dockerfile”並新增以下程式碼: ``` // 👇🏻/src/microservice-1/Dockerfile FROM node:18 # Use /usr/src/app as the working directory WORKDIR /usr/src/app # Copy package files and install production dependencies COPY --chown=node:node package*.json /usr/src/app RUN npm install --production # Copy the rest of the files COPY --chown=node:node . /usr/src/app/ # Switch to the user node with limited permissions USER node # Expose the application port EXPOSE 3001 # Set the default command to run the application CMD ["node", "index.js"] ``` 將我們不想推送到容器的文件加入到“.dockerignore”總是很好。使用我們不想推送的檔案的名稱來建立一個“.dockerignore”檔案。 ``` // 👇🏻/src/microservice-1/.dockerignore node_modules Dockerfile ``` 最後,透過執行以下命令來建構 🏗️ docker 映像: ``` docker build -t microservice1-image:latest . ``` 現在,這就是我們第一個微服務的完整設定。 ✨ - **建置與 Dockerize 微服務 2** 我們將有一個類似於“microservice1”的設置,只是在這裡和那裡進行了一些更改。 在「/src」資料夾中,建立一個新資料夾「/microservice-2」。在該資料夾內,初始化 **NodeJS** 專案並安裝所需的依賴項。 ``` mkdir microservice-2 cd microservice-2 npm init -y npm install --save express node-fetch ``` 建立一個新檔案“index.js”並新增以下程式碼: ``` // 👇🏻/src/microservice-2/index.js const express = require("express"); const fetch = require("node-fetch") const app = express(); const PORT = 3002; app.use(express.json()); app.get("/api/data", async (req, res) => { const url = "https://jsonplaceholder.typicode.com/users"; try { const response = await fetch(url); const data = await response.json(); res.json(data); } catch (error) { console.error(error.message); res.status(500).json({ error: "Internal Server Error" }); } }); app.listen(PORT, () => { console.log(`Microservice 2 listening on port ${PORT}`); }); ``` 伺服器正在偵聽連接埠 3002,根據對“/api/data”的“GET 請求”,我們從“jsonplaceholder”獲取資料並將回應作為 JSON 物件傳回。 📦 現在,是時候對這個微服務進行 docker 化了。複製並貼上「microservice1」的整個「Dockerfile」內容,然後將連接埠從 3001 變更為 3002。 另外,新增一個「.dockerignore」檔案並包含我們在建立「microservice1」時新增的相同檔案。 最後,透過執行以下命令來建構 🏗️ Docker 映像: ``` docker build -t microservice2-image:latest . ``` 現在,這也是我們第二個微服務的完整設定。 ✨ - **設定 Kubernetes** > 確保已安裝 **[Minikube](https://github.com/kubernetes/minikube)** 透過執行以下命令建立新的本機 Kubernetes 叢集。我們在設定 Odigos 和 Jaeger 時將需要它。 **啟動 Minikube:🚀** ``` minikube start ``` 現在我們已經準備好並 Docker 化了兩個微服務,是時候設定 Kubernetes 來管理這些服務了。 在專案的根目錄下,建立一個新資料夾「/k8s/manifests」。在此資料夾中,我們將為兩個微服務新增部署和服務配置。 - **部署設定📜**:用於在 Kubernetes 叢集上實際部署容器。 - **服務配置📄**:將 Pod 暴露給叢集內部和叢集外部。 首先,我們為「microservice1」建立清單。建立一個新檔案「microservice1-deployment-service.yaml」並新增以下內容: ``` // 👇🏻/k8s/manifests/microservice1-deployment-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: microservice1 spec: selector: matchLabels: app: microservice1 template: metadata: labels: app: microservice1 spec: containers: - name: microservice1 image: microservice1-image # Make sure to set it to Never, or else it will pull from the docker hub and fail. imagePullPolicy: Never resources: limits: memory: "200Mi" cpu: "500m" ports: - containerPort: 3001 --- apiVersion: v1 kind: Service metadata: name: microservice1 labels: app: microservice1 spec: type: NodePort selector: app: microservice1 ports: - port: 8080 targetPort: 3001 nodePort: 30001 ``` 此配置部署了一個名為「microservice1」的微服務,其資源限制為 **200MB 記憶體** 🗃️ 和 **0.5 個 CPU 核心**。它透過部署在連接埠 3001 上公開微服務,並透過服務在 **NodePort** 30001 上公開微服務。 > 🤔 還記得我們用名稱「microservice1-image」建構的「Dockerfile」嗎?我們使用相同的映像來建立容器。 可透過集群內的連接埠 8080 存取它。我們假設「microservice1-image」透過「imagePullPolicy: Never」在本地可用。如果沒有到位,它將嘗試從 Docker Hub 🐋 中提取映像並失敗。 現在,讓我們為「microservice2」建立清單。建立一個名為「microservice2-deployment-service.yaml」的新檔案並新增以下內容: ``` // 👇🏻/k8s/manifests/microservice1-deployment-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: microservice2 spec: selector: matchLabels: app: microservice2 template: metadata: labels: app: microservice2 spec: containers: - name: microservice2 image: microservice2-image # Make sure to set it to Never, or else it will pull from the docker hub and fail. imagePullPolicy: Never resources: limits: memory: "200Mi" cpu: "500m" ports: - containerPort: 3002 --- apiVersion: v1 kind: Service metadata: name: microservice2 labels: app: microservice2 spec: type: NodePort selector: app: microservice2 ports: - port: 8081 targetPort: 3002 nodePort: 30002 ``` 它與“microservice1”的清單類似,只有一些更改。 👀 此配置部署一個名為「microservice2」的微服務,並透過部署在連接埠 3002 上將其內部公開,並透過服務在 **NodePort** 30002 上將其外部公開。 可透過叢集內的連接埠 8081 進行存取,假設「microservice2-image」可透過「imagePullPolicy: Never」在本地使用。 全部完成後,請確保套用這些設定並使用這些服務啟動 Kubernetes 叢集。將目錄更改為`/manifests`並執行以下命令:👇🏻 ``` kubectl apply -f microservice1-deployment-service.yaml kubectl apply -f microservice2-deployment-service.yaml ``` 執行以下命令檢查我們的兩個部署是否正在**執行**:👇🏻 ``` kubectl get pods ``` ![Kubernetes Pod](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ywsvodcqqbx1wv0kede1.png) 最後,我們的應用程式已準備就緒,並使用必要的部署配置部署在 Kubernetes 上。 🎉 *** ## **安裝 Odigos 😍** > 💡 [**Odigos**](https://odigos.io/) 是一個開源可觀察性控制平面,使組織能夠建立和維護其可觀察性管道。 ![Odigos - 監控工具](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c6i7wth5l3ey9frk0cx.jpg) > ℹ️ 如果您在 Mac 上執行,請執行以下命令在本地安裝 Odigos。 ``` brew install keyval-dev/homebrew-odigos-cli/odigos ``` > ℹ️ 如果您使用的是 Linux 計算機,請考慮透過執行以下命令從 GitHub 版本安裝它。確保根據您的 Linux 發行版更改該檔案。 > ℹ️ 如果 Odigos 二進位檔案不可執行,請在執行安裝指令之前執行此指令 `chmod +x odigos` 使其可執行。 ``` curl -LJO https://github.com/keyval-dev/odigos/releases/download/v1.0.9/cli_1.0.9_linux_amd64.tar.gz tar -xvzf cli_1.0.9_linux_amd64.tar.gz ./odigos install ``` ![Odigos 安裝](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/145z2j9fusgnbp41whcw.png) > 如果您需要有關其安裝的更多簡短說明,請按照此[**連結**](https://docs.odigos.io/installation)操作。 現在,Odigos 已準備好執行 🎉。我們可以執行它的 UI,配置追蹤後端,並相應地發送追蹤。 *** ## **將 Odigos 連接到追蹤後端 💫** > 💡 [**Jaeger**](https://github.com/jaegertracing/jaeger) 是一個開源的端對端分散式追蹤系統。 ![Odigos - 分散式追蹤平台](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b9bytdpf4wv1ncb0z52p.jpg) ### **設定 Jaeger!** ✨ 在本教程中,我們將使用 **Jaeger** 🕵️‍♂️,這是一個流行的開源平台,用於查看微服務應用程式中的分散式追蹤。我們將用它來查看 Odigos 生成的痕跡。 > 有關 Jaeger 安裝說明,請點選此 [**link**](https://www.jaegertracing.io/download/)。 👀 若要在 Kubernetes 叢集上部署 Jaeger,請執行下列命令:👇🏻 ``` kubectl create ns tracing kubectl apply -f https://raw.githubusercontent.com/keyval-dev/opentelemetry-go-instrumentation/master/docs/getting-started/jaeger.yaml -n tracing ``` 在這裡,我們建立一個「tracing」命名空間,並在該命名空間中為 Jaeger 應用部署配置📃。 此命令設定自託管 Jaeger 實例及其服務。 👀 執行以下命令來取得正在執行的 pod 的狀態:👇🏻 ``` kubectl get pods -A -w ``` 等待所有三個 Pod 都 **正在執行**,然後再繼續。 ![Kubernetes Pod](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n41rxtp8gcbe4cwsl6xx.png) 現在,要在本地查看 Jaeger Interface 💻,我們需要進行連接埠轉送。將流量從本機電腦上的連接埠 16686 轉送至 Kubernetes 叢集中選定 pod 上的連接埠 16686。 ``` kubectl port-forward -n tracing svc/jaeger 16686:16686 ``` 此命令在本機電腦和 Jaeger pod 之間建立一條隧道,公開 Jaeger UI,以便您可以與其互動。 最後,在瀏覽器上開啟「 http://localhost:16686 」並查看 Jaeger 實例正在執行。 ![Jaeger UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gr6bcqph7nyxa7v0u01t.png) ### **設定 Odigos 與 Jaeger 一起工作!** 🌟 > ℹ️ 對於 Linux 用戶,請前往從 GitHub 版本下載 Odigos 二進位檔案的資料夾,然後執行以下命令來啟動 Odigos UI。 ``` ./odigos ui ``` > ℹ️ 對於 Mac 用戶,只需執行: ``` odigos ui ``` 造訪“ http://localhost:3000 ”,您將看到 Odigos 介面,您將在“default”命名空間中看到您的部署。 ![Odigos 登陸頁](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/14yqd2x41i9gqvwxdtsu.png) 選擇這兩個選項並點擊“下一步”。在下一頁上,選擇 Jaeger 作為後端,並在出現提示時加入以下詳細資訊: - **目的地名稱🛣️**:提供您想要的任何名稱,例如說**快速追蹤**。 - **端點🎯**:為端點加上`jaeger.tracing:4317`。 就是這樣 - Odigos 已準備好向我們的 Jaeger 後端發送痕跡。就是這麼簡單。 🤯 ![具有兩個微服務的 Odigos UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqmo7div92zngnkdwwyu.png) *** ## **查看分散式追蹤 🧐** 設定 Odigos 後,在 Jaeger 主頁「 http://localhost:16686 」上,您將已經看到列出的兩個微服務。 ![Jaeger UI 列出了兩個微服務](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwb0qjdmxi4ydcvwjgr1.png) Odigos 已經開始向 Jaeger 發送我們的應用程式痕跡。 😉 請記住,這是我們的微服務應用程式。由於以「microservice1」為起點,因此再向「microservice1」發出一些請求,隨後它將向「microservice2」請求資料並傳回。最終,Jaeger 將開始填滿這些痕跡。 ![Jaeger 分散式追蹤](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4kwzh854bsh5wga1or3.png) 點擊任一請求,您應該能夠觀察請求如何流經您的應用程式以及完成每個請求所需的時間。 這一切都是在沒有更改一行程式碼的情況下完成的。 🤯 一切都感謝 **Odigos**! 🤩 ![令人震驚的 GIF](https://media.giphy.com/media/l0NwHXQy3kUSfFF60/giphy.gif) 想像一下,這只是一個很小的虛擬應用程式,但對於一個執行著大量微服務並相互交互的更大的應用程式來說,分散式追蹤將非常強大! 💪 透過分散式跟踪,您可以輕鬆辨識應用程式中的瓶頸,並確定哪個服務導致問題或花費更長的時間。 🕒 *** ## **讓我們總結一下! 🥱** 到目前為止,您已經學習如何使用 **Odigos** 作為應用程式和追蹤後端 **Jaeger** 之間的 **中間件**,透過分散式追蹤來密切監控 👀 Javascript 應用程式。 👏 如果您已經做到了這一步,請拍拍自己的背。 🥳你值得擁有! 😉 本教學的源程式碼可在此處取得: https://github.com/keyval-dev/blog/tree/main/odigos-monitor-JS-like-a-pro > 如果您對本文有任何疑問或建議,請在下面的評論部分分享。 👇🏻 那麼,這就是本文的內容。感謝您的閱讀! 🎉🫡 --- 原文出處:https://dev.to/odigos/monitor-your-javascript-application-like-a-pro-581p

AI 程式碼產生與手動編碼 - 202X 的程式設計將會是什麼樣子 🤖 🤔

我們正在開發一個 [React 和 Node.js 的全端 Web 框架](https://github.com/wasp-lang/wasp),它使用簡單的設定語言來擺脫樣板檔案。很多次,我們被問到,*「為什麼你要費心去建立一個新的 Web 應用程式開發框架?無論如何,ChatGPT / LLM X 不是很快就會為開發人員生成所有程式碼嗎?」*。 這是我們對當前情勢的看法,也是我們相信未來的情況。 ## 為什麼我們需要(AI)程式碼產生? 為了讓開發速度更快,我們首先提出了 IDE 自動補全 - 如果您正在使用 React 並開始輸入 `use`,IDE 將自動向 `useState()` 或 `useEffect()` 提供補全。除了節省擊鍵次數之外,也許更有價值的是能夠查看目前範圍內有哪些方法/屬性可供我們使用。 IDE 對專案結構和程式碼層次結構的感知也使重構變得更加容易。 **雖然這已經很棒了,但是我們如何將其提升到一個新的水平?** 傳統的 IDE 支援是基於人類編寫的規則,例如,如果我們想讓 IDE 能夠為我們實現常用功能(例如, *使用API Y* 取得X,或*實現快速排序*),其中的數量太多,無法手動進行分類和維護。 如果有一種方法可以讓電腦分析我們迄今為止編寫的所有程式碼,並自行學習如何自動完成我們的程式碼以及如何對待人類,而不是我們做所有艱苦的工作... [除了美味又濕潤的蛋糕](https://www.youtube.com/watch?v=Y6ljFaKRTrI),我們其實已經做到了!由於機器學習的最新進展,IDE 現在可以做一些非常酷的事情,例如根據函數的名稱和頂部的簡短註釋來建議函數的完整實現: ![GPT 函數實作範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9p0mivtdpmjcskgt7qnl.gif) 這真是太神奇了!上面的範例由 [Github Copilot](https://copilot.github.com/) 提供支援 - 它本質上是一個在大量公開可用程式碼上訓練的神經網路。我不會深入了解其幕後工作原理的技術細節,但有很多精彩的文章和影片涵蓋了背後的科學知識。 **看到這一點,問題出現了 - 這對程式設計的未來意味著什麼?** 這只是 IDE 自動補全功能還是其他什麼?如果我們只需在註釋中輸入我們想要的內容就可以了,我們還需要繼續手動編寫程式碼嗎? ## 支持我們! 🙏⭐️ ![star_us](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3a8gkl9fcs0a8rl4zsq.gif) 如果您想表達對我們正在做的事情的支持,請考慮[在 Github 上給我們一顆星](https://github.com/wasp-lang/wasp)!我們在 Wasp 所做的一切都是開源的,您的支持激勵我們並幫助我們不斷簡化 Web 應用程式開發並減少樣板程式碼。 ![丟一顆星](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lz3ok1dpfkscsoo0n2om.gif) ## 大問題:程式碼產生後由誰維護? 在思考 ML 程式碼產生如何影響整個開發過程時,有一點需要考慮,但在查看所有令人印象深刻的範例時,通常不會立即想到這一點。 問題是 - **生成程式碼後會發生什麼?誰負責,將來誰來維護和重構?** ![一直是](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzh9yp2ejwhu4zpio0e.png) 儘管 ML 程式碼產生有助於編寫特定功能的初始程式碼,但它不能做更多的事情 - 如果將來要維護和更改該程式碼(如果有人使用該產品,那麼開發人員仍然會這樣做)需要完全擁有並理解它。你可以再次使用人工智慧來幫助你,但最終,你是負責的人。 想像一下,我們擁有的只是一種彙編語言,但程式碼生成非常適合它,你可以說“實現一個對陣列進行升序排序的函數”,它將完美地生成所需的程式碼。一旦您需要將排序更改為降序,您是否仍想在將來返回該功能? 或者,更貼近我們的日常生活,如果產生的 React 程式碼使用舊的類別語法,或者函數式元件和鉤子,對你來說是否都是一樣的? **換句話說,這意味著GPT 和其他LLM 不會降低程式碼複雜性,也不會降低建置功能所需的知識量**,它們只是幫助更快地編寫初始程式碼並使知識/範例更接近程式碼(其中真的很有幫助)。 **如果開發人員盲目接受生成的程式碼,他們只是在創造技術債並推動其向前發展**。 ## 認識大 A - 抽象 👆 如果 ChatGPT 和這群人無法解決我們學習如何編碼和詳細理解(例如透過 JWT 進行會話管理)工作原理的所有麻煩,還有什麼可以呢? 抽象化——這就是程式設計師幾十年來透過建立庫、框架和語言來處理程式碼重複和降低複雜性的方式。這就是我們從普通 JS 和直接 DOM 操作到 jQuery,最後到 React 和 Vue 等 UI 函式庫的方式。 引入抽像不可避免地意味著放棄一定的功能和靈活性(例如,在 Python 中對數字求和時,您無法準確指定將使用哪些 CPU 寄存器),但重點是,如果如果做得好,在大多數情況下你不需要也不想要這樣的權力。 ![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/75uhgmdu7fq5wcwhe6tw.jpg) **不對一段程式碼負責的唯一方法是它從一開始就不存在。** 因為一旦螢幕上的像素改變顏色,你就必須擔心,這就是為什麼所有框架、語言等的主要好處是_更少的程式碼==更少的決策==更少的責任_。 擁有更少程式碼的唯一方法是做出更少的決定,並向計算機提供更少的關於如何完成某項任務的細節- 理想情況下,我們只需要說明我們想要什麼,我們甚至不會關心它是如何完成的,只要它在我們擁有的時間/記憶體/成本邊界內(所以我們可能也需要說明這些)。 讓我們來看看網路應用程式世界中非常常見(也是每個人最喜歡的)功能 - 身份驗證(yaay ☠️ 🔫)!它的典型程式碼如下所示: ``` import jwt from 'jsonwebtoken' import SecurePassword from 'secure-password' import util from 'util' import prisma from '../dbClient.js' import { handleRejection } from '../utils.js' import config from '../config.js' const jwtSign = util.promisify(jwt.sign) const jwtVerify = util.promisify(jwt.verify) const JWT_SECRET = config.auth.jwtSecret export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options) export const verify = (token) => jwtVerify(token, JWT_SECRET) const auth = handleRejection(async (req, res, next) => { const authHeader = req.get('Authorization') if (!authHeader) { return next() } if (authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7, authHeader.length) let userIdFromToken try { userIdFromToken = (await verify(token)).id } catch (error) { if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) { return res.status(401).send() } else { throw error } } const user = await prisma.user.findUnique({ where: { id: userIdFromToken } }) if (!user) { return res.status(401).send() } const { password, ...userView } = user req.user = userView } else { return res.status(401).send() } next() }) const SP = new SecurePassword() export const hashPassword = async (password) => { const hashedPwdBuffer = await SP.hash(Buffer.from(password)) return hashedPwdBuffer.toString("base64") } export const verifyPassword = async (hashedPassword, password) => { try { return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) } catch (error) { console.error(error) return false } } ``` 這只是後端程式碼的一部分(僅適用於用戶名和密碼方法)!正如您所看到的,我們在這裡有很大的靈活性,可以執行/指定以下操作: - 選擇身份驗證的實作方法(例如會話或基於 JWT) - 選擇我們想要用於令牌(如果使用 JWT)和密碼管理的確切 npm 套件 - 解析 auth 標頭並指定每個值(授權、承載等)如何回應 - 為每個可能的結果選擇回傳程式碼(例如 401、403) - 選擇密碼的解碼/編碼方式 (base64) 一方面,在我們的程式碼中擁有這種程度的控制和靈活性確實很酷,但另一方面,需要做出很多決定(==錯誤),特別是對於像身份驗證這樣常見的事情! 如果後來有人問“_那麼你到底為什麼選擇secure-password npm 包,或者為什麼到底是base64 編碼?_”,我們可能應該用其他東西來回答,而不是“_好吧,2012 年有一篇看起來相當合法的帖子,它有近 50 票贊成。嗯,不過現在找不到了。另外,它的名字裡有‘安全’,聽起來不錯,對吧?_” 另一件要記住的事情是,我們還應該追蹤事情如何隨著時間的推移而變化,並確保幾年後,我們仍然使用最佳實踐,並且軟體包定期更新。 如果我們嘗試應用上面的原則(更少的程式碼,更少的詳細說明,說明我們想要什麼**而不是需要做什麼**),身份驗證的程式碼可能如下所示: ``` auth: { userEntity: User, externalAuthEntity: SocialLogin, methods: { usernameAndPassword: {}, google: {} }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/dashboard" } ``` 基於此,計算機/編譯器可以處理上面提到的所有內容,然後根據抽象級別,提供某種接口(例如表單元件或函數)來“掛鉤”我們自己的接口,例如React/Node.js 程式碼(順便說一句,這就是它實際上[在 Wasp 中工作](https://wasp-lang.dev/docs/auth/overview) 的方式)。 我們不需要關心底層使用了什麼確切的套件或加密方法 - 這是我們信任抽象層的作者和維護者的責任,就像我們相信 Python 最了解如何將兩個數字相加一樣裝配水平,並與該領域的最新進展保持同步。當我們依賴內建資料結構或依靠垃圾收集器來很好地管理程式記憶體時,也會發生同樣的情況。 ## 但是我產生的漂亮程式碼😿💻!那麼會發生什麼事呢? 別擔心,一切都還在這裡,您可以產生您想要的所有程式碼!這裡要理解的要點是,人工智慧程式碼生成和框架/語言開發是相互補充而不是替代,並且將繼續存在,這最終對開發人員社群來說是一個巨大的勝利——它們將繼續讓我們的生活更輕鬆,讓我們能夠做更多有趣的事情(而不是第 n 次實作 auth 或 CRUD API)! 我將這裡的演變視為一個循環(或實際上是螺旋式上升,但這超出了我的繪圖能力): 1. **語言/框架:存在**,是主流,很多人使用它 2. **模式開始出現**(例如實作身份驗證,或進行 API 呼叫)→ AI 學習它們,透過自動完成提供 3. **其中一些模式成熟**並變得穩定→抽象的候選者 4. **新的、更抽象的語言/框架**出現 5. **返回步驟 1。** ![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9na8wwmaqfabhx1dkuaf.png) ## 結論 這意味著我們雙贏——當語言成為主流時,我們可以從人工智慧驅動的程式碼產生中受益,幫助我們更快地編寫程式碼。另一方面,當我們不想重複/處理的程式碼模式出現並變得穩定時,我們就得到了一種全新的語言或框架,它允許我們編寫更少的程式碼並關心更少的實作細節! ![嘶嘶聲停止](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fistx8x0w8ee62nr1kl5.gif) 感謝您的閱讀,並希望您發現這篇文章內容豐富!我很想聽聽您是否同意(或不同意)這一點,以及您如何看待人工智慧工具驅動的程式設計的未來。 --- 原文出處:https://dev.to/wasp/ai-code-generation-vs-coding-by-hand-what-programming-is-going-to-look-like-in-202x-1idh

Supabase Auth:身分連結、Hooks 和 HaveIBeenPwned 集成

我們很高興地宣布 Supabase Auth 的四項新功能: 1. 身份連結 2. 會話控制 3. 密碼外洩保護 4. 帶有 Postgres 函數的 Auth Hooks {% 嵌入 https://youtu.be/LF8GABnAFyE %} ## 身份連結 當使用者登入時,系統會使用身份驗證方法和登入提供者建立身分。從歷史上看,如果身分與使用者共享相同的經過驗證的電子郵件,[Supabase Auth](https://supabase.com/docs/guides/auth) 會自動將身分連結到使用者。這可以方便地刪除重複的用戶帳戶。然而,一些開發人員還需要靈活地連結不共享相同電子郵件的帳戶。 今天,我們推出身份連結,開發人員可以使用它手動連結兩個單獨的身份。我們為開發人員新增了兩個新端點來管理身分連結流程: 使用者登入後,使用「linkIdentity()」[連結 OAuth 身分:](https://supabase.com/docs/reference/javascript/auth-linkidentity) ``` const { data, error } = await supabase.auth.linkIdentity({ provider: 'google', }) ``` 使用 `unlinkIdentity()` 來[取消連結身分](https://supabase.com/docs/reference/javascript/auth-unlinkidentity): ``` // retrieve all identities linked to a user const { data: { identities }, } = await supabase.auth.getUserIdentities() // find the google identity linked to the user const googleIdentity = identities.find(({ provider }) => provider === 'google') // unlink the google identity from the user const { data, error } = await supabase.auth.unlinkIdentity(googleIdentity) ``` 目前,這些方法支援連結 OAuth 身分。要將電子郵件或電話身分連結到用戶,您可以使用 [updateUser()](https://supabase.com/blog/supabase-auth-identity-linking-hooks#:~:text=can%20use% 20the -,updateUser(),-method.)方法。 預設情況下禁用手動連結。您可以在[儀表板驗證設定](https://supabase.com/dashboard/project/_/settings/auth) 中為您的專案啟用它。 ![如何啟用手動連結](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kmsrmiw0ue3q5rshji7v.jpg) > 有關更多訊息,請參閱[身份連結文件](https://supabase.com/docs/guides/auth/auth-identity-linking)。 ## 會話控制 Supabase Auth 從使用者登入應用程式那一刻起管理整個會話生命週期。這涉及以下步驟: 1. 為使用者建立會話。 2. 刷新會話以使其保持活動狀態。 3. 過期或登出時撤銷會話。 對於想要更好地控制使用者會話的開發人員,我們公開了 3 個新設定: - **時間盒使用者會話:** 強制使用者在一段時間間隔後再次登入。 - **不活動逾時:** 如果使用者在一段時間內不活動,則強制使用者重新登入。 - **每個使用者單一會話:** 將使用者限制為單一會話。保留最近的活動會話,並終止所有其他會話。 這些會話控制設定在專業版及以上版本中可用。 ![如何強制每個使用者單一會話](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aov0nw5xch0m4hw97vsg.jpg) > 有關更多訊息,請參閱[會話管理文件](https://supabase.com/docs/guides/auth/sessions)。 ## 密碼外洩保護 由於常見的使用者行為(例如選擇可猜測的密碼或在不同平台上重複使用密碼),密碼本質上可能是不安全的。 儘管 OAuth 和 magiclinks 更安全,但我們認識到密碼將繼續存在。我們希望讓用戶不易陷入潛在的陷阱。為了實現這一目標,我們在 Supabase Auth 中整合了 [HaveIBeenPwned.org](https://haveibeenpwned.com/) _Pwned Passwords API_,以防止使用者使用洩漏的密碼。 > **去圖書館** ℹ️ 我們開源了一個 Go 函式庫,用於與我們在身分驗證伺服器中使用的 [HaveIBeenPwned.org](http://haveibeenpwned.org/) Pwned 密碼 API 互動。查看 [存儲庫](https://github.com/supabase/hibp) 並隨時貢獻! 作為附加步驟,我們新增了為您的使用者指定密碼要求的功能。這可以透過[儀表板:](https://supabase.com/dashboard/project/_/settings/auth) 中專案的身份驗證設定進行配置 ![新增密碼要求](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o9ax3tudp7v8tba67hd.jpg) > 請參閱[密碼文件](https://supabase.com/docs/guides/auth/passwords) 以了解更多資訊。 ## 驗證掛鉤 我們收到了大量回饋,詢問如何自訂 Auth,例如: - 將自訂聲明新增至存取權杖 JWT - 多次嘗試 MFA 驗證失敗後註銷用戶 - 對密碼驗證嘗試套用自訂規則 我們的目標是保持簡單、無縫的 Supabase Auth 體驗。對於大多數開發人員來說,它應該可以輕鬆工作,而無需自訂。但是,認識到應用程式的多樣性,您現在可以透過 Auth Hook 擴展標準 Auth 功能。 Auth Hooks 只是 Postgres 函數,它們在 Auth 生命週期的關鍵點同步執行,以更改操作的結果。 例如,要使用 Auth Hooks 自訂 JWT 聲明,您可以建立一個 Postgres 函數,該函數接受第一個參數中的 JWT 聲明並傳回您希望 Supabase Auth 使用的 JWT。 假設您正在建立一個遊戲化應用程式,並且希望將用戶的層級作為自訂聲明附加到 JWT: ``` create function custom_access_token_hook(event jsonb) returns jsonb language plpgsql as $$ declare user_level jsonb; begin -- fetch the current user's level select to_jsonb(level) into user_level from profiles where user_id = event->>'user_id'::uuid; -- change the event.claims.level return jsonb_set( event, '{claims,level}', user_level); end; $$ ``` 在資料庫中建立函數後,您只需使用 Supabase Auth 註冊它: ![Auth Hooks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/unkgs56o62l8c4kfjzjg.jpg) 目前,您可以為流程中的以下點註冊 Auth Hook: - **自訂存取權杖:** 每次產生新的 JWT 時都會呼叫。 - **MFA 驗證嘗試:** 每次驗證 MFA 因素時都會呼叫,從而可以更好地控制檢測和阻止嘗試。 - **密碼驗證嘗試:** 每次使用密碼登入使用者時都會呼叫,從而可以更好地控制使用者帳戶的安全性。 如果編寫 PL/pgSQL 函數不是您的強項,您始終可以使用 [pg_net](https://supabase.com/docs/guides/database/extensions/pg_net) 向後端 API 發送請求,或使用[plv8]( https://supabase.com/docs/guides/database/extensions/plv8) 透過用JavaScript 編寫函數來更輕鬆地操作JSON。 Auth Hooks 今天可供自架,並將於下個月推出到該平台。如果您需要盡快存取,請透過[支援](https://supabase.help/)與我們聯繫! 那不是全部! Postgres 函數並不是寫鉤子的唯一方法。 Supabase 是 [Standard Webhooks](https://www.standardwebhooks.com/) 的創始貢獻者,這是一組關於輕鬆、安全、可靠地發送和接收 Webhook 的開源工具和指南。當然,Auth Hooks 將在 2024 年第一季支援 Webhooks。 ## 還有一件事… 如果您從一開始就關注我們(https://supabase.com/blog/supabase-auth),您就會知道Supabase Auth 是透過分叉[Netlify 的GoTrue 伺服器](https://github.com)開始的/netlify/gotrue)。從那時起,發生了很多變化,我們已經偏離了上游儲存庫。在這個階段,將專案重新命名為其他名稱是有意義的(提示鼓聲)-Auth。 這僅僅意味著儲存庫將從使用“gotrue”重新命名為“auth”。但別擔心! Docker 映像和庫(如“@supabase/gotrue-js”)將繼續發布,只要當前 v2 版本受支持,您就可以互換使用“@supabase/auth-js”。所有類別和方法都保持不變。這裡沒有重大變化! ## 結論 感謝您閱讀到最後!我們希望您喜歡第 X 週發布的 Supabase Auth 更新:身分連結、會話控制、洩露密碼保護和帶有 Postgres 功能的 Auth Hooks。 我們期待看到您使用這些新功能建立的內容,當然還有您的回饋意見,以使它們變得更好。 ## 更多發布第 X 週 - [第 1 天 - Supabase Studio 更新:AI 助理與使用者模擬](https://supabase.com/blog/studio-introducing-assistant) - [第 2 天 - Edge Functions:節點和本機 npm 相容性 ](https://supabase.com/blog/edge-functions-node-npm) -[第 3 天 - 介紹 Supabase Branching,這是一個針對每個拉取請求的 Postgres 資料庫](https://supabase.com/blog/supabase-branching) - [Postgres語言伺服器:實作解析器](https://supabase.com/blog/postgres-language-server-implementing-parser) - [Supabase 專輯](https://www.youtube.com/watch?v=r1POD-IdG-I) - [Supabase 啟動週 X 黑客松](https://supabase.com/blog/supabase-hackathon-lwx) - [啟動週 X 社群聚會](https://supabase.com/blog/community-meetups-lwx) --- 原文出處:https://dev.to/supabase/supabase-auth-identity-linking-hooks-and-haveibeenpwned-integration-19e1

Edge Functions:Node 和本機 npm 相容性

我們很高興地宣布,[Edge Functions](https://supabase.com/docs/guides/functions) 現在原生支援 npm 模組和 Node 內建 API。您可以將數百萬個流行、常用的 npm 模組直接匯入 Edge Functions 中。 `從 'npm:drizzle-orm/node-postgres' 導入 { drizzle }` ## 將現有 Node 應用程式遷移到 Edge Functions 您可以透過最少的變更將現有的 Node 應用程式遷移到 Supabase Edge Functions。 我們建立了一個示範來展示如何遷移使用 Express、Node Postgres 和 Drizzle 的 Node 應用程式。有關在 Edge Functions 中使用 npm 模組和 Node 內建程式的更多訊息,請參閱[管理依賴項指南](https://supabase.com/docs/guides/functions/import-maps)。 {% 嵌入 https://youtu.be/eCbiywoDORw %} **npm 模組的底層運作原理** 我們執行一個開源 Deno 伺服器來託管 Edge Functions,稱為 [Supabase Edge Runtime](https://supabase.com/blog/edge-runtime-self-hosted-deno-functions)。此自訂版本可協助我們保持 Edge Functions 以相同的方式運作,無論部署在何處 - 在我們的託管平台上、在本地開發中還是在您的自託管環境中。 加入 npm 支援時最大的挑戰是找到適用於所有環境的方法。我們希望保持工作流程接近 Deno CLI 體驗。應該可以直接在原始程式碼中導入 npm 模組,而無需額外的建置步驟。 部署函數時,我們將其模組圖序列化為單一檔案格式([eszip](https://github.com/denoland/eszip))。在託管環境中,所有模組引用都會從 eszip 中載入。這可以防止獲取模組時出現任何額外的延遲以及模組依賴關係之間的潛在衝突。 我們也在本機和自架環境中使用了 eszip 模組載入器,因此我們只需要為所有環境實作一種模組載入策略。作為本地開發的另一個好處,此方法避免了與使用者係統中安裝的 npm 模組的潛在衝突,因為 Edge Function 的 npm 模組是獨立於 eszip 中的。 [重構模組載入器](https://github.com/supabase/edge-runtime/pull/223)修正了一些其他錯誤,例如[邊緣函數錯誤](https://github.com/supabase/cli /issues/1584#issuecomment-1848799355) 當專案中已存在`deno.lock` 檔案時。 ## 您要求的其他一些東西... **區域呼叫** 現在,您可以選擇在執行邊緣函數時指定區域(也許我們將來應該更改名稱)。通常,邊緣函數在最靠近呼叫函數的使用者的區域中執行。但是,有時您希望在靠近 Postgres 資料庫或其他第 3 方 API 的地方執行它,以獲得最佳效能。 功能仍然部署到所有區域。但是,在呼叫過程中,您可以提供“x-region”標頭以將執行限制在特定區域。 **捲曲** ``` # https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions curl --request POST 'https://<project_ref>.supabase.co/functions/v1/hello-world' \ --header 'Authorization: Bearer ANON_KEY' \ --header 'Content-Type: application/json' \ --header 'x-region: eu-west-3' \ --data '{ "name":"Functions" }' ``` **JavaScript** ``` // https://supabase.com/docs/reference/javascript/installing import { createClient } from '@supabase/supabase-js' // Create a single supabase client for interacting with your database const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key') // https://supabase.com/docs/reference/javascript/functions-invoke const { data, error } = await supabase.functions.invoke('hello-world', { body: { name: 'Functions' }, headers: { 'x-region': 'eu-west-3' }, }) ``` > ℹ️查看[區域呼叫指南](https://supabase.com/docs/guides/functions/regional-inspiration)以了解更多詳情。 **更好的指標** 我們在 [Supabase 儀表板](https://supabase.com/dashboard/project/_/functions) 的 Edge Functions 部分中加入了更多指標:它現在顯示 CPU 時間和使用的記憶體。我們也按 HTTP 狀態碼細分了呼叫。 這些變更可協助您發現邊緣功能的任何問題並採取行動。 > ℹ️ 請參閱 Edge Functions 的[日誌記錄和指標指南](https://supabase.com/docs/guides/functions/debugging) 以了解更多資訊。 ![使用視覺化範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ki4pk0w0ykpa9i2c47q.jpg) **使用 Sentry 追蹤錯誤** 我們 Sentry 的朋友最近發布了官方的 [Sentry SDK for Deno](https://deno.land/x/[email protected])。有了這個,現在可以輕鬆追蹤 Sentry 邊緣函數中的錯誤和異常。 以下是一個簡單的範例,說明如何處理函數中的異常並將其傳送到 Sentry。 ``` import * as Sentry from 'https://deno.land/x/sentry/index.mjs' Sentry.init({ dsn: _DSN_, integrations: [], // Performance Monitoring tracesSampleRate: 1.0, // Set sampling rate for profiling - this is relative to tracesSampleRate profilesSampleRate: 1.0, }) // Set region and execution_id as custom tags Sentry.setTag('region', Deno.env.get('SB_REGION')) Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID')) Deno.serve(async (req) => { try { const { name } = await req.json() const data = { message: `Hello ${name}!`, } return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }) } catch (e) { Sentry.captureException(e) return new Response(JSON.stringify({ msg: 'error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }) } }) ``` ## 下一步是什麼 NPM 支援是 Edge Functions 最受歡迎的功能之一。如果您之前因缺乏支援而無法使用 Edge Functions,我們希望此更新能夠吸引您[再試一次](https://supabase.com/dashboard/project/_/functions)。如果您遇到任何問題,我們只需[一個支援請求](https://supabase.help/)。 對於現有的 Edge Functions 用戶來說,區域呼叫、更好的指標和錯誤處理只是接下來會發生的事情的一瞥。我們繼續迭代平台穩定性並對邊緣功能可以使用的資源設定自訂限制。請留意新的一年的另一篇文章。 ## 更多發布第 X 週 - [第 1 天 - Supabase Studio 更新:AI 助理與使用者模擬](https://supabase.com/blog/studio-introducing-assistant) - [pg_graphql:現在支援 Postgres 函式](https://supabase.com/blog/pg-graphql-postgres-functions) - [Postgres語言伺服器:實作解析器](https://supabase.com/blog/postgres-language-server-implementing-parser) - [Supabase 設計如何運作](https://supabase.com/blog/how-design-works-at-supabase) - [Supabase 專輯](https://www.youtube.com/watch?v=r1POD-IdG-I) - [Supabase 啟動週 X 黑客松](https://supabase.com/blog/supabase-hackathon-lwx) - [啟動週 X 社群聚會](https://supabase.com/blog/community-meetups-lwx) --- 原文出處:https://dev.to/supabase/edge-functions-node-and-native-npm-compatibility-77f

🧙‍♂️ 使用 ChatGPT 助理產生部落格 🪄 ✨

# 長話短說;博士 我們都已經看到了 ChatGPT 的功能(這對任何人來說都不陌生)。 很多文章都是使用 ChatGPT 一遍又一遍地寫的。 **實際上**,DEV 上的文章有一半是用 ChatGPT 寫的。 你可以使用一些[AI內容偵測器](https://copyleaks.com/ai-content- detector)來檢視。 問題是,ChatGPT 永遠不會產生一些非凡的內容,除了它內部已經有(經過訓練/微調)的內容。 但有一種方法可以超越目前使用 RAG(OpenAI 助理)訓練的內容。 [上一篇](https://dev.to/triggerdotdev/train-chatgpt-on-your-documentation-1a9g),我們討論了在您的文件上「訓練」ChatGPT;今天,讓我們看看如何從中製作出很多內容。我們將: - 使用 Docusaurus 建立新的部落格系統。 - 詢問 ChatGPT,為我們寫一篇與文件相關的部落格文章。 ![部落格](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ms26qb0uahpi898s0qun.gif) --- ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業! &nbsp; [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 {% cta https://github.com/triggerdotdev/trigger.dev %} 為 Trigger.dev 儲存庫加註星標 ⭐️ {% endcta %} --- ## 上次回顧 ⏰ - 我們建立了一個作業來取得文件 XML 並提取所有 URL。 - 我們抓取了每個網站的 URL 並提取了標題和內容。 - 我們將所有內容儲存到文件中並將其發送給 ChatGPT 助手。 - 我們建立了一個 ChatBot 畫面來詢問 ChatGPT 有關文件的資訊。 您可以在此處找到上一個[教學]的完整原始程式碼(https://github.com/triggerdotdev/blog/tree/main/openai-assistant)。 --- ![工具](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4adju83b5s1k0qozh3x.png) ## 稍作修改⚙️ 上次,我們建立了一個文件助理。我們寫: ``` You are a documentation assistant, loaded with documentation from ' + payload.url + ', return everything in an MD format. ``` 讓我們將其更改為部落格作者,請轉到“jobs/process.documentation.ts”第 92 行,並將其替換為以下內容: ``` You are a content writer assistant. You have been loaded with documentation from ${payload.url}, you write blog posts based on the documentation and return everything in the following MD format: --- slug: [post-slug] title: [post-title] --- [post-content] ``` 使用“slug”和“title”非常重要,因為這是 Docusaurus 的格式 - 我們的部落格系統可以接受(當然,我們也以 MD 格式發送所有輸出) --- ![Docusaurus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gu8wlh7qk8e3rh6mz35v.png) ## 多庫龍🦖 您可以使用多種類型的部落格系統! 對於我們的用例,我們將使用 Docusaurus,它可以讀取基於 MD 的格式(我們從 ChatGPT 請求的輸出)。 **我們可以透過執行來安裝 Docusaurus:** ``` npx create-docusaurus@latest blog classic --typescript ``` 接下來,我們可以進入已建立的目錄並執行以下命令: ``` npm run start ``` 這將啟動 Docusaurus。你可以關註一下。還有一個名為“blog”的附加目錄,其中包含所有部落格文章;這是我們保存 ChatGPT 產生的部落格文章的地方。 ![範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pgo25rlkw85nfvbh0y4s.png) --- ![部落格](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3oxjtli1dn9i9klnj5t.png) ## 產生部落格 📨 我們需要創造一個就業機會 - 取得部落格標題 - 使用 ChatGPT 產生完整的部落格文章 - 將其保存到我們部落格上的 MD 文件中 我們可以輕鬆地使用 ChatGPT 來實現這一點! 前往“jobs”資料夾並新增一個名為“process.blog.ts”的新檔案。新增以下程式碼: ``` import { eventTrigger } from "@trigger.dev/sdk"; import { client } from "@openai-assistant/trigger"; import {object, string} from "zod"; import {openai} from "@openai-assistant/helper/open.ai"; import {writeFileSync} from "fs"; import slugify from "slugify"; client.defineJob({ // This is the unique identifier for your Job, it must be unique across all Jobs in your project. id: "process-blog", name: "Process Blog", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.blog.event", schema: object({ title: string(), aId: string(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { const {title, aId} = payload; const thread = await io.openai.beta.threads.create('create-thread'); await io.openai.beta.threads.messages.create('create-message', thread.id, { content: ` title: ${title} `, role: 'user', }); const run = await io.openai.beta.threads.runs.createAndWaitForCompletion('run-thread', thread.id, { model: 'gpt-4-1106-preview', assistant_id: payload.aId, }); if (run.status !== "completed") { console.log('not completed'); throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`); } const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id, { query: { limit: "1" } }); return io.runTask('save-blog', async () => { const content = messages[0].content[0]; if (content.type === 'text') { const fileName = slugify(title, {lower: true, strict: true, trim: true}); writeFileSync(`./blog/blog/${fileName}.md`, content.text.value) return {fileName}; } }); }, }); ``` - 我們加入了一些必要的變數: - `title` 部落格文章標題 - `aId` 上一篇文章中新增的助手 ID。 - 我們為助手建立了一個新線程(`io.openai.beta.threads.create`) - 我們無法在沒有任何線程的情況下質疑它。與之前的教程不同,在這裡,我們對每個請求建立一個新線程。我們不需要對話中最後一條訊息的上下文。 - 然後,我們使用部落格標題為線程(`io.openai.beta.threads.messages.create`)新增訊息。我們不需要提供額外的說明 - 我們已經在第一部分完成了該部分😀 - 我們執行 `io.openai.beta.threads.runs.createAndWaitForCompletion` 來啟動進程 - 通常,您需要某種每分鐘執行一次的遞歸來檢查作業是否完成,但是 [Trigger.dev]( http://Trigger .dev)已經加入了一種執行進程並同時等待它的方法🥳 - 我們在查詢正文中執行帶有“limit: 1”的“io.openai.beta.threads.messages.list”,以從對話中獲取第一則訊息(在ChatGPT 結果中,第一則訊息是最後一條訊息) 。 - 然後,我們使用「writeFileSync」從 ChatGPT 取得的值來儲存新建立的部落格 - 確保您擁有正確的部落格路徑。 轉到“jobs/index.ts”並加入以下行: ``` export * from "./process.blog"; ``` 現在,讓我們建立一個新的路由來觸發該作業。 前往“app/api”,建立一個名為“blog”的新資料夾,並在一個名為“route.tsx”的新檔案中 新增以下程式碼: ``` import {client} from "@openai-assistant/trigger"; export async function POST(request: Request) { const payload = await request.json(); if (!payload.title || !payload.aId) { return new Response(JSON.stringify({error: 'Missing parameters'}), {status: 400}); } // We send an event to the trigger to process the documentation const {id: eventId} = await client.sendEvent({ name: "process.blog.event", payload }); return new Response(JSON.stringify({eventId}), {status: 200}); } ``` - 我們檢查標題和助理 ID 是否存在。 - 我們在 [Trigger.dev](http://Trigger.dev) 中觸發事件並發送訊息。 - 我們將事件 ID 傳送回客戶端,以便我們可以追蹤作業的進度。 --- ![前端](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kgh52s7mxd20w91kr3c9.png) ## 前端🎩 沒什麼好做的! 在我們的「components」目錄中,建立一個名為「blog.component.tsx」的新檔案和以下程式碼: ``` "use client"; import {FC, useCallback, useEffect, useState} from "react"; import {ExtendedAssistant} from "@openai-assistant/components/main"; import {SubmitHandler, useForm} from "react-hook-form"; import {useEventRunDetails} from "@trigger.dev/react"; interface Blog { title: string, aId: string; } export const BlogComponent: FC<{list: ExtendedAssistant[]}> = (props) => { const {list} = props; const {register, formState, handleSubmit} = useForm<Blog>(); const [event, setEvent] = useState<string | undefined>(undefined); const addBlog: SubmitHandler<Blog> = useCallback(async (param) => { const {eventId} = await (await fetch('/api/blog', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(param) })).json(); setEvent(eventId); }, []); return ( <> <form className="flex flex-col gap-3 mt-5" onSubmit={handleSubmit(addBlog)}> <div className="flex flex-col gap-1"> <div className="font-bold">Assistant</div> <select className="border border-gray-200 rounded-xl py-2 px-3" {...register('aId', {required: true})}> {list.map(val => ( <option key={val.id} value={val.aId}>{val.url}</option> ))} </select> </div> <div className="flex flex-col gap-1"> <div className="font-bold">Title</div> <input className="border border-gray-200 rounded-xl py-2 px-3" placeholder="Blog title" {...register('title', {required: true})} /> </div> <button className="border border-gray-200 rounded-xl py-2 px-3 bg-gray-100 hover:bg-gray-200" disabled={formState.isSubmitting}>Create blog</button> </form> {!!event && ( <Blog eventId={event} /> )} </> ) } export const Blog: FC<{eventId: string}> = (props) => { const {eventId} = props; const { data, error } = useEventRunDetails(eventId); if (data?.status !== 'SUCCESS') { return <div className="pointer bg-yellow-300 border-yellow-500 p-1 px-3 text-yellow-950 border rounded-2xl">Loading</div> } return ( <div> <a href={`http://localhost:3000/blog/${data.output.fileName}`}>Check blog post</a> </div> ) }; ``` - 我們使用「react-hook-form」來輕鬆控制我們的輸入。 - 我們讓使用者選擇他們想要使用的助手。 - 我們建立一個包含文章標題的新輸入。 - 我們將所有內容傳送到先前建立的路由並傳回作業的「eventId」。 - 我們建立一個新的「<Blog />」元件,該元件顯示載入直到事件完成,並使用新建立的教程新增指向我們部落格的連結。 將元件加入我們的“components/main.tsx”檔案中: ``` {assistantState.filter(f => !f.pending).length > 0 && <BlogComponent list={assistantState} />} ``` 我們完成了! ![完成](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fkm37v5idrxexjje2u3o.png) 現在,讓我們新增部落格標題並點擊「生成」。 ![部落格](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gosm1f1ttz3q1m0atu7s.png) --- ![圖片](https://res.cloudinary.com/practicaldev/image/fetch/s--uTFwMeAp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3。 amazonaws.com/uploads/articles/0half2g6r5zfn7asq084.png) ## 讓我們聯絡吧! 🔌 作為開源開發者,您可以加入我們的[社群](https://discord.gg/nkqV9xBYWy) 做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: https://github.com/triggerdotdev/blog/tree/main/openai-blog-writer 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/generate-blogs-with-chatgpt-assistant-1894

keepHQ 如何獲得前 2,000 顆星!

我很高興與 [Keep](https://github.com/keephq/keep) 的執行長兼聯合創始人 Tal 交談。 它最初是一個 CLI 工具,隨著時間的推移,變成了一個警報聚合工具。 如今,他們擁有近 3,000 顆星。 **您可以在這裡觀看完整影片:** {% 嵌入 https://www.youtube.com/watch?v=eykb1zbDwQo %} <小時/> 開始 ------------ 他們製作了一個非常基本的警報 CLI 工具並將其發佈在 Hackernews 上 - **“顯示 HN:”** 他們乘坐飛機,當他們著陸時,**他們看到了 600 顆星星**。 [Hackernews](https://hackernews.com?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-keephq-got-their-first-2-000-stars)是一個有趣的網站。它非常醜陋,牽引力很大,而且很難進入。 我在 Hackernews 上看到並推出了很多產品。雖然在 Hackernews 上發表一篇文章很困難,但 **「Show HN」** 的文章要容易得多。 這通常是一個秘密武器,因為你也許可以每年做一次 - 最好與更多管道合作,以獲得更好的機會在 GitHub 上流行。 他們創造了更多工具,例如[gnip](https://www.gnip.io/?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-keephq-got-their-first-2-000-stars),以及雖然這個工具今天沒有帶來大量流量,但在發布之日就帶來了許多流量。 您會發現,對於您建立的每個副專案,您都可以在 Hackernews (Show HN) 和 Product Hunt 中啟動它。 我的建議? **目標是每月發布一次新專案。** **HACK:** 最近,一些隨機的人在 Hackernews 上發布了 **Novu** 組織頁面。我不知道它是如何被接受的,但它為我們帶來了 400 顆星。 [查看此處的貼文](https://news.ycombinator.com/item?id=38419513&utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-keephq-got-their-first-2-000-stars) <小時/> ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/7eda5059-74b8-4b7a- 9468-d77cce002b2c/image.png?t=1701432843) 與社區一起尋找產品 ------------------------------------------- 雖然 Hackernews 對開源社群(許多早期採用者)非常友好,但 Product Hunt 感覺更像是獨立駭客/非開發人員的工具。 **目標是獲得盡可能多的讚成票並達到列表的頂部。** 您通常應該將 Product Hunt 與其他管道結合。 如果您剛開始並想要星星 - 將您的 GitHub 作為您的“存取”URL。 如果您是一家更知名的公司並且希望獲得更多潛在客戶並預訂會議,請加入您的**網站 URL。** **對於第一次啟動,我通常會瞄準 GitHub 存儲庫。** Keep 的社群很小,但仍然佔據了第一天的份額。 最好的創始人不會讓運氣引導他們。 **這是他們所做的:** * 他們在社交媒體上發布了有關其發布的訊息(像大多數人一樣) * 他們嘗試聯繫盡可能多的人來幫助他們。 * 他們建造了一個秘密武器工具! [Slackline](https://github.com/talboren/slackline?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-keephq-got-their-first-2-000-stars) <小時/> ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/1ff5c353-9447-45cb- 946c-2a4a3f3e29a6/image.png?t=1701433047) 鬆弛的線路 ------------------- 塔爾不久前向我展示了這個工具,從那時起我就一直使用它。 當您是一家小型新創公司時,您必須完成不可能的任務,從 0 到 1。**使用您擁有的所有可能的選項。** Slackline 正是為此而設計的。您可以加入任何 Slack 群組,並向頻道中的每個可能的人發送 DM。 有幾個選項: 1. 您可以在 Slack 頻道上使用它來推動對您的產品的回饋或從社群成員那裡獲得幫助來完成不同的事情。 2. 在 Product Hunt 上向人們尋求協助。 雖然數字 2 聽起來有點冒犯,但它確實有效 - 他們獲得了當天的第一名以及名譽和榮耀🎩 <小時/> ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/2ac46256-fc15-4bfe- a2bb-724e305e20b9/image.png?t=1701433220) 使用賞金 -------------- 您可以對 [Algora](https://algora.io?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-keephq-got-their-first-2-000-stars) 的問題進行懸賞,告訴您這會擴大你的社區——可能不會,但會給你帶來更多的可信度。 如果您剛開始,Algora 可以幫助您獲得更多貢獻者(在貢獻者清單中)並使您看起來更可信。 然而,請記住,為了錢而來的貢獻者通常不會免費做同樣的事情(並非總是如此)。 {% cta https://github.com/keephq/keep %}在 GitHub 上加星 Keep{% endcta %} --- ## 我邀請您註冊我的電子報。 若符合以下條件,本通訊對您有好處: - 您正在考慮開源您的產品(或建立新產品)。 - 您正在考慮開啟一個副產品並將其開源(以反映您的主要產品)。 - 您從事科技業,希望在沒有明星/沒有 GitHub 趨勢的情況下實現成長。 這是一份 100% 免費的時事通訊(並且永遠如此)。請隨時註冊: [https://gitroom.com](https://gitroom.com/) ![技術](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86ywkzncq6erg44d6xy9.gif) --- 原文出處:https://dev.to/github20k/how-keephq-got-their-first-2000-stars-l7i

✨ 用您的文件訓練 ChatGPT 🪄 ✨

# 簡介 ChatGPT 訓練至 2022 年。 但是,如果您希望它專門為您提供有關您網站的資訊怎麼辦?最有可能的是,這是不可能的,**但不再是了!** OpenAI 推出了他們的新功能 - [助手](https://platform.openai.com/docs/assistants/how-it-works)。 現在您可以輕鬆地為您的網站建立索引,然後向 ChatGPT 詢問有關該網站的問題。在本教程中,我們將建立一個系統來索引您的網站並讓您查詢它。我們將: - 抓取文件網站地圖。 - 從網站上的所有頁面中提取資訊。 - 使用新資訊建立新助理。 - 建立一個簡單的ChatGPT前端介面並查詢助手。 ![助手](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekre38der95twom33tqb.gif) --- ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 --- ## 讓我們開始吧🔥 讓我們建立一個新的 NextJS 專案。 ``` npx create-next-app@latest ``` >💡 我們使用 NextJS 新的應用程式路由器。安裝專案之前請確保您的節點版本為 18+ 讓我們建立一個新的資料庫來保存助手和抓取的頁面。 對於我們的範例,我們將使用 [Prisma](https://www.prisma.io/) 和 SQLite。 安裝非常簡單,只需執行: ``` npm install prisma @prisma/client --save ``` 然後加入架構和資料庫 ``` npx prisma init --datasource-provider sqlite ``` 轉到“prisma/schema.prisma”並將其替換為以下架構: ``` // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Docs { id Int @id @default(autoincrement()) content String url String @unique identifier String @@index([identifier]) } model Assistant { id Int @id @default(autoincrement()) aId String url String @unique } ``` 然後執行 ``` npx prisma db push ``` 這將建立一個新的 SQLite 資料庫(本機檔案),其中包含兩個主表:“Docs”和“Assistant” - 「Docs」包含所有抓取的頁面 - `Assistant` 包含文件的 URL 和內部 ChatGPT 助理 ID。 讓我們新增 Prisma 客戶端。 建立一個名為「helper」的新資料夾,並新增一個名為「prisma.ts」的新文件,並在其中新增以下程式碼: ``` import {PrismaClient} from '@prisma/client'; export const prisma = new PrismaClient(); ``` 我們稍後可以使用“prisma”變數來查詢我們的資料庫。 --- ![ScrapeAndIndex](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc05wtlc4peosr62ydnx.png) ## 刮擦和索引 ### 建立 Trigger.dev 帳戶 抓取頁面並為其建立索引是一項長期執行的任務。 **我們需要:** - 抓取網站地圖的主網站元 URL。 - 擷取網站地圖內的所有頁面。 - 前往每個頁面並提取內容。 - 將所有內容儲存到 ChatGPT 助手中。 為此,我們使用 Trigger.dev! 註冊 [Trigger.dev 帳號](https://trigger.dev/)。 註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![pic1](https://res.cloudinary.com/practicaldev/image/fetch/s--B2jtIoA6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdnxq8o7el7t4utvgf1u.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![pic2](https://res.cloudinary.com/practicaldev/image/fetch/s--K4k6T6mi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4kt7e5r1mwg60atqfka.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![pic3](https://res.cloudinary.com/practicaldev/image/fetch/s--Ysm1Dd0r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ser7a2j5qft9vw8rfk0m.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段來安裝 Trigger.dev。 仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` ### 安裝 ChatGPT (OpenAI) 我們將使用OpenAI助手,因此我們必須將其安裝到我們的專案中。 [建立新的 OpenAI 帳戶](https://platform.openai.com/) 並產生 API 金鑰。 ![pic4](https://res.cloudinary.com/practicaldev/image/fetch/s--uV1LwOH---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png) 點擊下拉清單中的「檢視 API 金鑰」以建立 API 金鑰。 ![pic5](https://res.cloudinary.com/practicaldev/image/fetch/s--Tp8aLqSa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png) 接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。 ``` npm install @trigger.dev/openai ``` 將您的 OpenAI API 金鑰新增至「.env.local」檔案。 ``` OPENAI_API_KEY=<your_api_key> ``` 建立一個新目錄“helper”並新增一個新檔案“open.ai.tsx”,其中包含以下內容: ``` import {OpenAI} from "@trigger.dev/openai"; export const openai = new OpenAI({ id: "openai", apiKey: process.env.OPENAI_API_KEY!, }); ``` 這是我們透過 Trigger.dev 整合封裝的 OpenAI 用戶端。 ### 建立後台作業 讓我們繼續建立一個新的後台作業! 前往“jobs”並建立一個名為“process.documentation.ts”的新檔案。 **新增以下程式碼:** ``` import { eventTrigger } from "@trigger.dev/sdk"; import { client } from "@openai-assistant/trigger"; import {object, string} from "zod"; import {JSDOM} from "jsdom"; import {openai} from "@openai-assistant/helper/open.ai"; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-documentation", name: "Process Documentation", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.documentation.event", schema: object({ url: string(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { } }); ``` 我們定義了一個名為「process.documentation.event」的新作業,並新增了一個名為 URL 的必要參數 - 這是我們稍後要傳送的文件 URL。 正如您所看到的,該作業是空的,所以讓我們向其中加入第一個任務。 我們需要獲取網站網站地圖並將其返回。 抓取網站將返回我們需要解析的 HTML。 為此,我們需要安裝 JSDOM。 ``` npm install jsdom --save ``` 並將其導入到我們文件的頂部: ``` import {JSDOM} from "jsdom"; ``` 現在,我們可以新增第一個任務。 用「runTask」包裝我們的程式碼很重要,這可以讓 Trigger.dev 將其與其他任務分開。觸發特殊架構將任務拆分為不同的進程,因此 Vercel 無伺服器逾時不會影響它們。 **這是第一個任務的程式碼:** ``` const getSiteMap = await io.runTask("grab-sitemap", async () => { const data = await (await fetch(payload.url)).text(); const dom = new JSDOM(data); const sitemap = dom.window.document.querySelector('[rel="sitemap"]')?.getAttribute('href'); return new URL(sitemap!, payload.url).toString(); }); ``` - 我們透過 HTTP 請求從 URL 取得整個 HTML。 - 我們將其轉換為 JS 物件。 - 我們找到網站地圖 URL。 - 我們解析它並返回它。 接下來,我們需要抓取網站地圖,提取所有 URL 並返回它們。 讓我們安裝“Lodash”——陣列結構的特殊函數。 ``` npm install lodash @types/lodash --save ``` 這是任務的程式碼: ``` export const makeId = (length: number) => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }; const {identifier, list} = await io.runTask("load-and-parse-sitemap", async () => { const urls = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/g; const identifier = makeId(5); const data = await (await fetch(getSiteMap)).text(); // @ts-ignore return {identifier, list: chunk(([...new Set(data.match(urls))] as string[]).filter(f => f.includes(payload.url)).map(p => ({identifier, url: p})), 25)}; }); ``` - 我們建立一個名為 makeId 的新函數來為所有頁面產生隨機辨識碼。 - 我們建立一個新任務並加入正規表示式來提取每個可能的 URL - 我們發送一個 HTTP 請求來載入網站地圖並提取其所有 URL。 - 我們將 URL「分塊」為 25 個元素的陣列(如果有 100 個元素,則會有四個 25 個元素的陣列) 接下來,讓我們建立一個新作業來處理每個 URL。 **這是完整的程式碼:** ``` function getElementsBetween(startElement: Element, endElement: Element) { let currentElement = startElement; const elements = []; // Traverse the DOM until the endElement is reached while (currentElement && currentElement !== endElement) { currentElement = currentElement.nextElementSibling!; // If there's no next sibling, go up a level and continue if (!currentElement) { // @ts-ignore currentElement = startElement.parentNode!; startElement = currentElement; if (currentElement === endElement) break; continue; } // Add the current element to the list if (currentElement && currentElement !== endElement) { elements.push(currentElement); } } return elements; } const processContent = client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-content", name: "Process Content", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.content.event", schema: object({ url: string(), identifier: string(), }) }), run: async (payload, io, ctx) => { return io.runTask('grab-content', async () => { // We first grab a raw html of the content from the website const data = await (await fetch(payload.url)).text(); // We load it with JSDOM so we can manipulate it const dom = new JSDOM(data); // We remove all the scripts and styles from the page dom.window.document.querySelectorAll('script, style').forEach((el) => el.remove()); // We grab all the titles from the page const content = Array.from(dom.window.document.querySelectorAll('h1, h2, h3, h4, h5, h6')); // We grab the last element so we can get the content between the last element and the next element const lastElement = content[content.length - 1]?.parentElement?.nextElementSibling!; const elements = []; // We loop through all the elements and grab the content between each title for (let i = 0; i < content.length; i++) { const element = content[i]; const nextElement = content?.[i + 1] || lastElement; const elementsBetween = getElementsBetween(element, nextElement); elements.push({ title: element.textContent, content: elementsBetween.map((el) => el.textContent).join('\n') }); } // We create a raw text format of all the content const page = ` ---------------------------------- url: ${payload.url}\n ${elements.map((el) => `${el.title}\n${el.content}`).join('\n')} ---------------------------------- `; // We save it to our database await prisma.docs.upsert({ where: { url: payload.url }, update: { content: page, identifier: payload.identifier }, create: { url: payload.url, content: page, identifier: payload.identifier } }); }); }, }); ``` - 我們從 URL 中獲取內容(之前從網站地圖中提取) - 我們用`JSDOM`解析它 - 我們刪除頁面上存在的所有可能的“<script>”或“<style>”。 - 我們抓取頁面上的所有標題(`h1`、`h2`、`h3`、`h4`、`h5`、`h6`) - 我們迭代標題並獲取它們之間的內容。我們不想取得整個頁面內容,因為它可能包含不相關的內容。 - 我們建立頁面原始文字的版本並將其保存到我們的資料庫中。 現在,讓我們為每個網站地圖 URL 執行此任務。 觸發器引入了名為“batchInvokeAndWaitForCompletion”的東西。 它允許我們批量發送 25 個專案進行處理,並且它將同時處理所有這些專案。下面是接下來的幾行程式碼: ``` let i = 0; for (const item of list) { await processContent.batchInvokeAndWaitForCompletion( 'process-list-' + i, item.map( payload => ({ payload, }), 86_400), ); i++; } ``` 我們以 25 個為一組[手動觸發](https://trigger.dev/docs/documentation/concepts/triggers/invoke)之前建立的作業。 完成後,讓我們將保存到資料庫的所有內容並連接它: ``` const data = await io.runTask("get-extracted-data", async () => { return (await prisma.docs.findMany({ where: { identifier }, select: { content: true } })).map((d) => d.content).join('\n\n'); }); ``` 我們使用之前指定的標識符。 現在,讓我們在 ChatGPT 中使用新資料建立一個新檔案: ``` const file = await io.openai.files.createAndWaitForProcessing("upload-file", { purpose: "assistants", file: data }); ``` `createAndWaitForProcessing` 是 Trigger.dev 建立的任務,用於將檔案上傳到助手。如果您在沒有整合的情況下手動使用“openai”,則必須串流傳輸檔案。 現在讓我們建立或更新我們的助手: ``` const assistant = await io.openai.runTask("create-or-update-assistant", async (openai) => { const currentAssistant = await prisma.assistant.findFirst({ where: { url: payload.url } }); if (currentAssistant) { return openai.beta.assistants.update(currentAssistant.aId, { file_ids: [file.id] }); } return openai.beta.assistants.create({ name: identifier, description: 'Documentation', instructions: 'You are a documentation assistant, you have been loaded with documentation from ' + payload.url + ', return everything in an MD format.', model: 'gpt-4-1106-preview', tools: [{ type: "code_interpreter" }, {type: 'retrieval'}], file_ids: [file.id], }); }); ``` - 我們首先檢查是否有針對該特定 URL 的助手。 - 如果我們有的話,讓我們用新文件更新助手。 - 如果沒有,讓我們建立一個新的助手。 - 我們傳遞「你是文件助理」的指令,需要注意的是,我們希望最終輸出為「MD」格式,以便稍後更好地顯示。 對於拼圖的最後一塊,讓我們將新助手儲存到我們的資料庫中。 **這是程式碼:** ``` await io.runTask("save-assistant", async () => { await prisma.assistant.upsert({ where: { url: payload.url }, update: { aId: assistant.id, }, create: { aId: assistant.id, url: payload.url, } }); }); ``` 如果該 URL 已經存在,我們可以嘗試使用新的助手 ID 來更新它。 這是該頁面的完整程式碼: ``` import { eventTrigger } from "@trigger.dev/sdk"; import { client } from "@openai-assistant/trigger"; import {object, string} from "zod"; import {JSDOM} from "jsdom"; import {chunk} from "lodash"; import {prisma} from "@openai-assistant/helper/prisma.client"; import {openai} from "@openai-assistant/helper/open.ai"; const makeId = (length: number) => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i += 1) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-documentation", name: "Process Documentation", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.documentation.event", schema: object({ url: string(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { // The first task to get the sitemap URL from the website const getSiteMap = await io.runTask("grab-sitemap", async () => { const data = await (await fetch(payload.url)).text(); const dom = new JSDOM(data); const sitemap = dom.window.document.querySelector('[rel="sitemap"]')?.getAttribute('href'); return new URL(sitemap!, payload.url).toString(); }); // We parse the sitemap; instead of using some XML parser, we just use regex to get the URLs and we return it in chunks of 25 const {identifier, list} = await io.runTask("load-and-parse-sitemap", async () => { const urls = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/g; const identifier = makeId(5); const data = await (await fetch(getSiteMap)).text(); // @ts-ignore return {identifier, list: chunk(([...new Set(data.match(urls))] as string[]).filter(f => f.includes(payload.url)).map(p => ({identifier, url: p})), 25)}; }); // We go into each page and grab the content; we do this in batches of 25 and save it to the DB let i = 0; for (const item of list) { await processContent.batchInvokeAndWaitForCompletion( 'process-list-' + i, item.map( payload => ({ payload, }), 86_400), ); i++; } // We get the data that we saved in batches from the DB const data = await io.runTask("get-extracted-data", async () => { return (await prisma.docs.findMany({ where: { identifier }, select: { content: true } })).map((d) => d.content).join('\n\n'); }); // We upload the data to OpenAI with all the content const file = await io.openai.files.createAndWaitForProcessing("upload-file", { purpose: "assistants", file: data }); // We create a new assistant or update the old one with the new file const assistant = await io.openai.runTask("create-or-update-assistant", async (openai) => { const currentAssistant = await prisma.assistant.findFirst({ where: { url: payload.url } }); if (currentAssistant) { return openai.beta.assistants.update(currentAssistant.aId, { file_ids: [file.id] }); } return openai.beta.assistants.create({ name: identifier, description: 'Documentation', instructions: 'You are a documentation assistant, you have been loaded with documentation from ' + payload.url + ', return everything in an MD format.', model: 'gpt-4-1106-preview', tools: [{ type: "code_interpreter" }, {type: 'retrieval'}], file_ids: [file.id], }); }); // We update our internal database with the assistant await io.runTask("save-assistant", async () => { await prisma.assistant.upsert({ where: { url: payload.url }, update: { aId: assistant.id, }, create: { aId: assistant.id, url: payload.url, } }); }); }, }); export function getElementsBetween(startElement: Element, endElement: Element) { let currentElement = startElement; const elements = []; // Traverse the DOM until the endElement is reached while (currentElement && currentElement !== endElement) { currentElement = currentElement.nextElementSibling!; // If there's no next sibling, go up a level and continue if (!currentElement) { // @ts-ignore currentElement = startElement.parentNode!; startElement = currentElement; if (currentElement === endElement) break; continue; } // Add the current element to the list if (currentElement && currentElement !== endElement) { elements.push(currentElement); } } return elements; } // This job will grab the content from the website const processContent = client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "process-content", name: "Process Content", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "process.content.event", schema: object({ url: string(), identifier: string(), }) }), run: async (payload, io, ctx) => { return io.runTask('grab-content', async () => { try { // We first grab a raw HTML of the content from the website const data = await (await fetch(payload.url)).text(); // We load it with JSDOM so we can manipulate it const dom = new JSDOM(data); // We remove all the scripts and styles from the page dom.window.document.querySelectorAll('script, style').forEach((el) => el.remove()); // We grab all the titles from the page const content = Array.from(dom.window.document.querySelectorAll('h1, h2, h3, h4, h5, h6')); // We grab the last element so we can get the content between the last element and the next element const lastElement = content[content.length - 1]?.parentElement?.nextElementSibling!; const elements = []; // We loop through all the elements and grab the content between each title for (let i = 0; i < content.length; i++) { const element = content[i]; const nextElement = content?.[i + 1] || lastElement; const elementsBetween = getElementsBetween(element, nextElement); elements.push({ title: element.textContent, content: elementsBetween.map((el) => el.textContent).join('\n') }); } // We create a raw text format of all the content const page = ` ---------------------------------- url: ${payload.url}\n ${elements.map((el) => `${el.title}\n${el.content}`).join('\n')} ---------------------------------- `; // We save it to our database await prisma.docs.upsert({ where: { url: payload.url }, update: { content: page, identifier: payload.identifier }, create: { url: payload.url, content: page, identifier: payload.identifier } }); } catch (e) { console.log(e); } }); }, }); ``` 我們已經完成建立後台作業來抓取和索引文件🎉 ### 詢問助理 現在,讓我們建立一個任務來詢問我們的助手。 前往“jobs”並建立一個新檔案“question.assistant.ts”。 **新增以下程式碼:** ``` import {eventTrigger} from "@trigger.dev/sdk"; import {client} from "@openai-assistant/trigger"; import {object, string} from "zod"; import {openai} from "@openai-assistant/helper/open.ai"; client.defineJob({ // This is the unique identifier for your Job; it must be unique across all Jobs in your project. id: "question-assistant", name: "Question Assistant", version: "0.0.1", // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: eventTrigger({ name: "question.assistant.event", schema: object({ content: string(), aId: string(), threadId: string().optional(), }) }), integrations: { openai }, run: async (payload, io, ctx) => { // Create or use an existing thread const thread = payload.threadId ? await io.openai.beta.threads.retrieve('get-thread', payload.threadId) : await io.openai.beta.threads.create('create-thread'); // Create a message in the thread await io.openai.beta.threads.messages.create('create-message', thread.id, { content: payload.content, role: 'user', }); // Run the thread const run = await io.openai.beta.threads.runs.createAndWaitForCompletion('run-thread', thread.id, { model: 'gpt-4-1106-preview', assistant_id: payload.aId, }); // Check the status of the thread if (run.status !== "completed") { console.log('not completed'); throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`); } // Get the messages from the thread const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id, { query: { limit: "1" } }); const content = messages[0].content[0]; if (content.type === 'text') { return {content: content.text.value, threadId: thread.id}; } } }); ``` - 該事件需要三個參數 - `content` - 我們想要傳送給助理的訊息。 - `aId` - 我們先前建立的助手的內部 ID。 - `threadId` - 對話的執行緒 ID。正如您所看到的,這是一個可選參數,因為在第一個訊息中,我們還沒有線程 ID。 - 然後,我們建立或取得前一個執行緒的執行緒。 - 我們在助理提出的問題的線索中加入一條新訊息。 - 我們執行線程並等待它完成。 - 我們取得訊息清單(並將其限制為 1),因為第一則訊息是對話中的最後一則訊息。 - 我們返回訊息內容和我們剛剛建立的線程ID。 ### 新增路由 我們需要為我們的應用程式建立 3 個 API 路由: 1、派新助理進行處理。 2. 透過URL獲取特定助手。 3. 新增訊息給助手。 在「app/api」中建立一個名為assistant的新資料夾,並在其中建立一個名為「route.ts」的新檔案。裡面加入如下程式碼: ``` import {client} from "@openai-assistant/trigger"; import {prisma} from "@openai-assistant/helper/prisma.client"; export async function POST(request: Request) { const body = await request.json(); if (!body.url) { return new Response(JSON.stringify({error: 'URL is required'}), {status: 400}); } // We send an event to the trigger to process the documentation const {id: eventId} = await client.sendEvent({ name: "process.documentation.event", payload: {url: body.url}, }); return new Response(JSON.stringify({eventId}), {status: 200}); } export async function GET(request: Request) { const url = new URL(request.url).searchParams.get('url'); if (!url) { return new Response(JSON.stringify({error: 'URL is required'}), {status: 400}); } const assistant = await prisma.assistant.findFirst({ where: { url: url } }); return new Response(JSON.stringify(assistant), {status: 200}); } ``` 第一個「POST」方法取得一個 URL,並使用用戶端傳送的 URL 觸發「process.documentation.event」作業。 第二個「GET」方法從我們的資料庫中透過客戶端發送的 URL 取得助手。 現在,讓我們建立向助手新增訊息的路由。 在「app/api」內部建立一個新資料夾「message」並新增一個名為「route.ts」的新文件,然後新增以下程式碼: ``` import {prisma} from "@openai-assistant/helper/prisma.client"; import {client} from "@openai-assistant/trigger"; export async function POST(request: Request) { const body = await request.json(); // Check that we have the assistant id and the message if (!body.id || !body.message) { return new Response(JSON.stringify({error: 'Id and Message are required'}), {status: 400}); } // get the assistant id in OpenAI from the id in the database const assistant = await prisma.assistant.findUnique({ where: { id: +body.id } }); // We send an event to the trigger to process the documentation const {id: eventId} = await client.sendEvent({ name: "question.assistant.event", payload: { content: body.message, aId: assistant?.aId, threadId: body.threadId }, }); return new Response(JSON.stringify({eventId}), {status: 200}); } ``` 這是一個非常基本的程式碼。我們從客戶端獲取訊息、助手 ID 和線程 ID,並將其發送到我們之前建立的「question.assistant.event」。 最後要做的事情是建立一個函數來獲取我們所有的助手。 在「helpers」內部建立一個名為「get.list.ts」的新函數並新增以下程式碼: ``` import {prisma} from "@openai-assistant/helper/prisma.client"; // Get the list of all the available assistants export const getList = () => { return prisma.assistant.findMany({ }); } ``` 非常簡單的程式碼即可獲得所有助手。 我們已經完成了後端🥳 讓我們轉到前面。 --- ![前端](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3s5gks1j0ojoz11b93i.png) ## 建立前端 我們將建立一個基本介面來新增 URL 並顯示已新增 URL 的清單: ![ss1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihvx4yn6uee6gritr9nh.png) ### 首頁 將 `app/page.tsx` 的內容替換為以下程式碼: ``` import {getList} from "@openai-assistant/helper/get.list"; import Main from "@openai-assistant/components/main"; export default async function Home() { const list = await getList(); return ( <Main list={list} /> ) } ``` 這是一個簡單的程式碼,它從資料庫中取得清單並將其傳遞給我們的 Main 元件。 接下來,讓我們建立“Main”元件。 在「app」內建立一個新資料夾「components」並新增一個名為「main.tsx」的新檔案。 **新增以下程式碼:** ``` "use client"; import {Assistant} from '@prisma/client'; import {useCallback, useState} from "react"; import {FieldValues, SubmitHandler, useForm} from "react-hook-form"; import {ChatgptComponent} from "@openai-assistant/components/chatgpt.component"; import {AssistantList} from "@openai-assistant/components/assistant.list"; import {TriggerProvider} from "@trigger.dev/react"; export interface ExtendedAssistant extends Assistant { pending?: boolean; eventId?: string; } export default function Main({list}: {list: ExtendedAssistant[]}) { const [assistantState, setAssistantState] = useState(list); const {register, handleSubmit} = useForm(); const submit: SubmitHandler<FieldValues> = useCallback(async (data) => { const assistantResponse = await (await fetch('/api/assistant', { body: JSON.stringify({url: data.url}), method: 'POST', headers: { 'Content-Type': 'application/json' } })).json(); setAssistantState([...assistantState, {...assistantResponse, url: data.url, pending: true}]); }, [assistantState]) const changeStatus = useCallback((val: ExtendedAssistant) => async () => { const assistantResponse = await (await fetch(`/api/assistant?url=${val.url}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } })).json(); setAssistantState([...assistantState.filter((v) => v.id), assistantResponse]); }, [assistantState]) return ( <TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}> <div className="w-full max-w-2xl mx-auto p-6 flex flex-col gap-4"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add documentation link" type="text" {...register('url', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300 flex gap-2 flex-wrap"> {assistantState.map(val => ( <AssistantList key={val.url} val={val} onFinish={changeStatus(val)} /> ))} </div> {assistantState.filter(f => !f.pending).length > 0 && <ChatgptComponent list={assistantState} />} </div> </TriggerProvider> ) } ``` 讓我們看看這裡發生了什麼: - 我們建立了一個名為「ExtendedAssistant」的新接口,其中包含兩個參數「pending」和「eventId」。當我們建立一個新的助理時,我們沒有最終的值,我們將只儲存`eventId`並監聽作業處理直到完成。 - 我們從伺服器元件取得清單並將其設定為新狀態(以便我們稍後可以修改它) - 我們新增了「TriggerProvider」來幫助我們監聽事件完成並用資料更新它。 - 我們使用「react-hook-form」建立一個新表單來新增助手。 - 我們新增了一個帶有一個輸入「URL」的表單來提交新的助理進行處理。 - 我們迭代並顯示所有現有的助手。 - 在提交表單時,我們將資訊傳送到先前建立的「路由」以新增助理。 - 事件完成後,我們觸發「changeStatus」以從資料庫載入助手。 - 最後,我們有了 ChatGPT 元件,只有在沒有等待處理的助手時才會顯示(`!f.pending`) 讓我們建立 `AssistantList` 元件。 在「components」內,建立一個新檔案「assistant.list.tsx」並在其中加入以下內容: ``` "use client"; import {FC, useEffect} from "react"; import {ExtendedAssistant} from "@openai-assistant/components/main"; import {useEventRunDetails} from "@trigger.dev/react"; export const Loading: FC<{eventId: string, onFinish: () => void}> = (props) => { const {eventId} = props; const { data, error } = useEventRunDetails(eventId); useEffect(() => { if (!data || error) { return ; } if (data.status === 'SUCCESS') { props.onFinish(); } }, [data]); return <div className="pointer bg-yellow-300 border-yellow-500 p-1 px-3 text-yellow-950 border rounded-2xl">Loading</div> }; export const AssistantList: FC<{val: ExtendedAssistant, onFinish: () => void}> = (props) => { const {val, onFinish} = props; if (val.pending) { return <Loading eventId={val.eventId!} onFinish={onFinish} /> } return ( <div key={val.url} className="pointer relative bg-green-300 border-green-500 p-1 px-3 text-green-950 border rounded-2xl hover:bg-red-300 hover:border-red-500 hover:text-red-950 before:content-[attr(data-content)]" data-content={val.url} /> ) } ``` 我們迭代我們建立的所有助手。如果助手已經建立,我們只顯示名稱。如果沒有,我們渲染`<Loading />`元件。 載入元件在螢幕上顯示“正在載入”,並長時間輪詢伺服器直到事件完成。 我們使用 Trigger.dev 建立的 useEventRunDetails 函數來了解事件何時完成。 事件完成後,它會觸發「onFinish」函數,用新建立的助手更新我們的客戶端。 ### 聊天介面 ![聊天介面](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u7db3qwz03d6jkk965a.png) 現在,讓我們加入 ChatGPT 元件並向我們的助手提問! - 選擇我們想要使用的助手 - 顯示訊息列表 - 新增我們要傳送的訊息的輸入和提交按鈕。 在「components」內部新增一個名為「chatgpt.component.tsx」的新文件 讓我們繪製 ChatGPT 聊天框: ``` "use client"; import {FC, useCallback, useEffect, useRef, useState} from "react"; import {ExtendedAssistant} from "@openai-assistant/components/main"; import Markdown from 'react-markdown' import {useEventRunDetails} from "@trigger.dev/react"; interface Messages { message?: string eventId?: string } export const ChatgptComponent = ({list}: {list: ExtendedAssistant[]}) => { const url = useRef<HTMLSelectElement>(null); const [message, setMessage] = useState(''); const [messagesList, setMessagesList] = useState([] as Messages[]); const [threadId, setThreadId] = useState<string>('' as string); const submitForm = useCallback(async (e: any) => { e.preventDefault(); setMessagesList((messages) => [...messages, {message: `**[ME]** ${message}`}]); setMessage(''); const messageResponse = await (await fetch('/api/message', { method: 'POST', body: JSON.stringify({message, id: url.current?.value, threadId}), })).json(); if (!threadId) { setThreadId(messageResponse.threadId); } setMessagesList((messages) => [...messages, {eventId: messageResponse.eventId}]); }, [message, messagesList, url, threadId]); return ( <div className="border border-black/50 rounded-2xl flex flex-col"> <div className="border-b border-b-black/50 h-[60px] gap-3 px-3 flex items-center"> <div>Assistant:</div> <div> <select ref={url} className="border border-black/20 rounded-xl p-2"> {list.filter(f => !f.pending).map(val => ( <option key={val.id} value={val.id}>{val.url}</option> ))} </select> </div> </div> <div className="flex-1 flex flex-col gap-3 py-3 w-full min-h-[500px] max-h-[1000px] overflow-y-auto overflow-x-hidden messages-list"> {messagesList.map((val, index) => ( <div key={index} className={`flex border-b border-b-black/20 pb-3 px-3`}> <div className="w-full"> {val.message ? <Markdown>{val.message}</Markdown> : <MessageComponent eventId={val.eventId!} onFinish={setThreadId} />} </div> </div> ))} </div> <form onSubmit={submitForm}> <div className="border-t border-t-black/50 h-[60px] gap-3 px-3 flex items-center"> <div className="flex-1"> <input value={message} onChange={(e) => setMessage(e.target.value)} className="read-only:opacity-20 outline-none border border-black/20 rounded-xl p-2 w-full" placeholder="Type your message here" /> </div> <div> <button className="border border-black/20 rounded-xl p-2 disabled:opacity-20" disabled={message.length < 3}>Send</button> </div> </div> </form> </div> ) } export const MessageComponent: FC<{eventId: string, onFinish: (threadId: string) => void}> = (props) => { const {eventId} = props; const { data, error } = useEventRunDetails(eventId); useEffect(() => { if (!data || error) { return ; } if (data.status === 'SUCCESS') { props.onFinish(data.output.threadId); } }, [data]); if (!data || error || data.status !== 'SUCCESS') { return ( <div className="flex justify-end items-center pb-3 px-3"> <div className="animate-spin rounded-full h-3 w-3 border-t-2 border-b-2 border-blue-500" /> </div> } return <Markdown>{data.output.content}</Markdown>; }; ``` 這裡正在發生一些令人興奮的事情: - 當我們建立新訊息時,我們會自動將其呈現在螢幕上作為「我們的」訊息,但是當我們將其發送到伺服器時,我們需要推送事件 ID,因為我們還沒有訊息。這就是我們使用 `{val.message ? <Markdown>{val.message}</Markdown> : <MessageComponent eventId={val.eventId!} onFinish={setThreadId} />}` - 我們用「Markdown」元件包裝訊息。如果您還記得,我們在前面的步驟中告訴 ChatGPT 以 MD 格式輸出所有內容,以便我們可以正確渲染它。 - 事件處理完成後,我們會更新線程 ID,以便我們從以下訊息中獲得相同對話的上下文。 我們就完成了🎉 --- ![完成](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0half2g6r5zfn7asq084.png) ## 讓我們聯絡吧! 🔌 作為開源開發者,您可以加入我們的[社群](https://discord.gg/nkqV9xBYWy) 做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: [https://github.com/triggerdotdev/blog/tree/main/openai-assistant](https://github.com/triggerdotdev/blog/tree/main/openai-assistant) 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/train-chatgpt-on-your-documentation-1a9g

🔥 大幅提升你的 NextJS 能力:嘗試手寫一個 GitHub 星星監視器 🤯

在本文中,您將學習如何建立 **GitHub 星數監視器** 來檢查您幾個月內的星數以及每天獲得的星數。 - 使用 GitHub API 取得目前每天收到的星星數量。 - 在螢幕上每天繪製美麗的星星圖表。 - 創造一個工作來每天收集新星星。 ![吉米](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n524rmr0gpgr79p4qlhj.gif) --- ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 https://github.com/triggerdotdev/trigger.dev --- ## 這是你需要知道的 😻 取得 GitHub 上星星數量的大部分工作將透過 GitHub API 完成。 GitHub API 有一些限制: - 每個請求最多 100 名觀星者 - 最多 100 個同時請求 - 每小時最多 60 個請求 [TriggerDev](https://github.com/triggerdotdev/trigger.dev) 儲存庫擁有超過 5000 顆星,實際上不可能在合理的時間內(即時)計算所有星數。 因此,我們將採用與 [GitHub Stars History](https://star-history.com/) 相同的技巧。 - 取得星星總數 (**5,715**) 除以每頁 **100** 結果 = **58 頁** - 設定我們想要的最大請求量(**20 頁最大**)除以 **58 頁** = 跳過 3 頁。 - 從這些頁面中獲取星星**(2000 顆星)**,然後獲取剩餘的星星,我們將按比例加入到其他日期(**3715 顆星**)。 它會為我們繪製一個漂亮的圖表,並在需要的地方用星星凸起。 當我們每天獲取新數量的星星時,事情就會變得容易得多。 我們將用目前擁有的星星總數減去 GitHub 上的新星星數量。 **我們不再需要迭代觀星者。** --- ## 讓我們來設定一下 🔥 我們的申請將包含一頁: - 新增您想要監控的儲存庫。 - 查看儲存庫清單及其 GitHub 星圖。 - 刪除那些你不再想要的。 ![StarsOverTime](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbii15mn1tyuz63kjphk.png) > 💡 我們將使用 NextJS 新的應用程式路由器,在安裝專案之前請確保您的節點版本為 18+。 > 使用 NextJS 設定一個新專案 ``` npx create-next-app@latest ``` 我們必須將所有星星保存到我們的資料庫中! 在我們的示範中,我們將使用 SQLite 和 `Prisma`。 它非常容易安裝,但可以隨意使用任何其他資料庫。 ``` npm install prisma @prisma/client --save ``` 在我們的專案中安裝 Prisma ``` npx prisma init --datasource-provider sqlite ``` 轉到“prisma/schema.prisma”並將其替換為以下模式: ``` generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Repository { id String @id @default(uuid()) month Int year Int day Int name String stars Int @@unique([name, day, month, year]) } ``` 然後執行 ``` npx prisma db push ``` 我們基本上已經在 SQLite 資料庫中建立了一個名為「Repository」的新表: - 「月」、「年」、「日」是日期。 - `name` 儲存庫的名稱 - 「星星」以及該特定日期的星星數量。 你還可以看到我們在底部加入了一個`@@unique`,這意味著我們可以將`name`,`month`,`year`,`day`一起重複記錄。它會拋出一個錯誤。 讓我們新增 Prisma 客戶端。 建立一個名為「helper」的新資料夾,並新增一個名為「prisma.ts」的新文件,並在其中新增以下程式碼: ``` import {PrismaClient} from '@prisma/client'; export const prisma = new PrismaClient(); ``` 我們稍後可以使用該「prisma」變數來查詢我們的資料庫。 --- ## 應用程式 UI 骨架 💀 我們需要一些函式庫來完成本教學: - **Axios** - 向伺服器發送請求(如果您覺得更舒服,可以隨意使用 fetch) - **Dayjs -** 很棒的處理日期的函式庫。它是 moment.js 的替代品,但不再完全維護。 - **Lodash -** 很酷的資料結構庫。 - **react-hook-form -** 處理表單的最佳函式庫(驗證/值/等) - **chart.js** - 我選擇繪製 GitHub 星圖的函式庫。 讓我們安裝它們: ``` npm install axios dayjs lodash @types/lodash chart.js react-hook-form react-chartjs-2 --save ``` 建立一個名為“components”的新資料夾並新增一個名為“main.tsx”的新文件 新增以下程式碼: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import {useCallback, useState} from "react"; export default function Main() { const [repositoryState, setRepositoryState] = useState([]); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> {/* Charts Component */} </div> </div> </div> ))} </div> </div> ) } ``` **超簡單的React元件** - 允許我們新增新的 GitHub 庫並將其發送到伺服器 POST 的表單 - `/api/repository` `{todo: 'add'}` - 刪除我們不需要 POST 的儲存庫 - `/api/repository` `{todo: 'delete'}` - 所有新增的庫及其圖表的清單。 讓我們轉到本文的複雜部分,新增儲存庫。 --- ## 數星星 ![CountingStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4m2j6046myxwv2c8kwla.gif) 在「helper」內部建立一個名為「all.stars.ts」的新檔案並新增以下程式碼: ``` import axios from "axios"; import dayjs from "dayjs"; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); const requestAmount = 20; export const getAllGithubStars = async (owner: string, name: string) => { // Get the amount of stars from GitHub const totalStars = (await axios.get(`https://api.github.com/repos/${owner}/${name}`)).data.stargazers_count; // get total pages const totalPages = Math.ceil(totalStars / 100); // How many pages to skip? We don't want to spam requests const pageSkips = totalPages < requestAmount ? requestAmount : Math.ceil(totalPages / requestAmount); // Send all the requests at the same time const starsDates = (await Promise.all([...new Array(requestAmount)].map(async (_, index) => { const getPage = (index * pageSkips) || 1; return (await axios.get(`https://api.github.com/repos/${owner}/${name}/stargazers?per_page=100&page=${getPage}`, { headers: { Accept: "application/vnd.github.v3.star+json", }, })).data; }))).flatMap(p => p).reduce((acc: any, stars: any) => { const yearMonth = stars.starred_at.split('T')[0]; acc[yearMonth] = (acc[yearMonth] || 0) + 1; return acc; }, {}); // how many stars did we find from a total of `requestAmount` requests? const foundStars = Object.keys(starsDates).reduce((all, current) => all + starsDates[current], 0); // Find the earliest date const lowestMonthYear = Object.keys(starsDates).reduce((lowest, current) => { if (lowest.isAfter(dayjs.utc(current.split('T')[0]))) { return dayjs.utc(current.split('T')[0]); } return lowest; }, dayjs.utc()); // Count dates until today const splitDate = dayjs.utc().diff(lowestMonthYear, 'day') + 1; // Create an array with the amount of stars we didn't find const array = [...new Array(totalStars - foundStars)]; // Set the amount of value to add proportionally for each day let splitStars: any[][] = []; for (let i = splitDate; i > 0; i--) { splitStars.push(array.splice(0, Math.ceil(array.length / i))); } // Calculate the amount of stars for each day return [...new Array(splitDate)].map((_, index, arr) => { const yearMonthDay = lowestMonthYear.add(index, 'day').format('YYYY-MM-DD'); const value = starsDates[yearMonthDay] || 0; return { stars: value + splitStars[index].length, date: { month: +dayjs.utc(yearMonthDay).format('M'), year: +dayjs.utc(yearMonthDay).format('YYYY'), day: +dayjs.utc(yearMonthDay).format('D'), } }; }); } ``` 那麼這裡發生了什麼事: - `totalStars` - 我們計算圖書館擁有的星星總數。 - `totalPages` - 我們計算頁數 **(每頁 100 筆記錄)** - `pageSkips` - 由於我們最多需要 20 個請求,因此我們檢查每次必須跳過多少頁。 - `starsDates` - 我們填充每個日期的星星數量。 - `foundStars` - 由於我們跳過日期,我們需要計算實際找到的星星總數。 - `lowestMonthYear` - 尋找我們擁有的恆星的最早日期。 - `splitDate` - 最早的日期和今天之間有多少個日期? - `array` - 一個包含 `splitDate` 專案數量的空陣列。 - `splitStars` - 我們缺少的星星數量,需要按比例加入每個日期。 - 最終返回 - 新陣列包含自開始以來每天的星星數量。 所以,我們已經成功建立了一個每天可以給我們星星的函數。 我嘗試過這樣顯示,結果很混亂。 您可能想要顯示每個月的星星數量。 此外,您可能想要累積星星**而不是:** - 二月 - 300 顆星 - 三月 - 200 顆星 - 四月 - 400 顆星 **如果有這樣的就更好了:** - 二月 - 300 顆星 - 三月 - 500 顆星 - 四月 - 900 顆星 兩個選項都有效。 **這取決於你想展示什麼!** 因此,讓我們轉到 helper 資料夾並建立一個名為「get.list.ts」的新檔案。 這是文件的內容: ``` import {prisma} from "./prisma"; import {groupBy, sortBy} from "lodash"; import {Repository} from "@prisma/client"; function fixStars (arr: any[]): Array<{name: string, stars: number, month: number, year: number}> { return arr.map((current, index) => { return { ...current, stars: current.stars + arr.slice(index + 1, arr.length).reduce((acc, current) => acc + current.stars, 0), } }).reverse(); } export const getList = async (data?: Repository[]) => { const repo = data || await prisma.repository.findMany(); const uniqMonth = Object.values( groupBy( sortBy( Object.values( groupBy(repo, (p) => p.name + '-' + p.year + '-' + p.month)) .map(current => { const stars = current.reduce((acc, current) => acc + current.stars, 0); return { name: current[0].name, stars, month: current[0].month, year: current[0].year } }), [(p: any) => -p.year, (p: any) => -p.month] ),p => p.name) ); const fixMonthDesc = uniqMonth.map(p => fixStars(p)); return fixMonthDesc.map(p => ({ name: p[0].name, list: p })); } ``` 首先,它將所有按日的星星轉換為按月的星星。 稍後我們會累積每個月的星星數量。 這裡要注意的一件主要事情是 `data?: Repository[]` 是可選的。 我們制定了一個簡單的邏輯:如果我們不傳遞資料,它將為我們資料庫中的所有儲存庫傳遞資料。 如果我們傳遞資料,它只會對其起作用。 為什麼問? - 當我們建立一個新的儲存庫時,我們需要在將其新增至資料庫後處理特定的儲存庫資料。 - 當我們重新載入頁面時,我們需要取得所有資料。 現在,讓我們來處理我們的星星建立/刪除路線。 轉到“src/app/api”並建立一個名為“repository”的新資料夾。在該資料夾中,建立一個名為「route.tsx」的新檔案。 在那裡加入以下程式碼: ``` import {getAllGithubStars} from "../../../../helper/all.stars"; import {prisma} from "../../../../helper/prisma"; import {Repository} from "@prisma/client"; import {getList} from "../../../../helper/get.list"; export async function POST(request: Request) { const body = await request.json(); if (!body.repository) { return new Response(JSON.stringify({error: 'Repository is required'}), {status: 400}); } const {owner, name} = body.repository.match(/github.com\/(?<owner>.*)\/(?<name>.*)/).groups; if (!owner || !name) { return new Response(JSON.stringify({error: 'Repository is invalid'}), {status: 400}); } if (body.todo === 'delete') { await prisma.repository.deleteMany({ where: { name: `${owner}/${name}` } }); return new Response(JSON.stringify({deleted: true}), {status: 200}); } const starsMonth = await getAllGithubStars(owner, name); const repo: Repository[] = []; for (const stars of starsMonth) { repo.push( await prisma.repository.upsert({ where: { name_day_month_year: { name: `${owner}/${name}`, month: stars.date.month, year: stars.date.year, day: stars.date.day, }, }, update: { stars: stars.stars, }, create: { name: `${owner}/${name}`, month: stars.date.month, year: stars.date.year, day: stars.date.day, stars: stars.stars, } }) ); } return new Response(JSON.stringify(await getList(repo)), {status: 200}); } ``` 我們共享 DELETE 和 CREATE 路由,這些路由通常不應在生產中使用,但我們在本文中這樣做是為了讓您更輕鬆。 我們從請求中取得 JSON,檢查「repository」欄位是否存在,並且它是 GitHub 儲存庫的有效路徑。 如果是刪除請求,我們使用 prisma 根據儲存庫名稱從資料庫中刪除儲存庫並傳回請求。 如果是建立,我們使用 getAllGithubStars 來獲取資料以保存到我們的資料庫中。 > 💡 由於我們已經在 `name`、`month`、`year` 和 `day` 上放置了唯一索引,如果記錄已經存在,我們可以使用 `prisma` `upsert` 來更新資料 最後,我們將新累積的資料回傳給客戶端。 最困難的部分完成了🍾 --- ## 主頁人口 💽 我們還沒有建立我們的主頁元件。 **我們開始做吧。** 前往“app”資料夾建立或編輯“page.tsx”並新增以下程式碼: ``` "use server"; import Main from "@/components/main"; import {getList} from "../../helper/get.list"; export default async function Home() { const list: any[] = await getList(); return ( <Main list={list} /> ) } ``` 我們使用與 getList 相同的函數來取得累積的所有儲存庫的所有資料。 我們還修改主要元件以支援它。 編輯 `components/main.tsx` 並將其替換為: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import {useCallback, useState} from "react"; interface List { name: string, list: Repository[] } export default function Main({list}: {list: List[]}) { const [repositoryState, setRepositoryState] = useState(list); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> {/* Charts Components */} </div> </div> </div> ))} </div> </div> ) } ``` --- ## 顯示圖表! 📈 前往“components”資料夾並新增一個名為“chart.tsx”的新檔案。 新增以下程式碼: ``` "use client"; import {Repository} from "@prisma/client"; import {useMemo} from "react"; import React from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, } from 'chart.js'; import { Line } from 'react-chartjs-2'; ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend ); export default function ChartComponent({repository}: {repository: Repository[]}) { const labels = useMemo(() => { return repository.map(r => `${r.year}/${r.month}`); }, [repository]); const data = useMemo(() => ({ labels, datasets: [ { label: repository[0].name, data: repository.map(p => p.stars), borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.5)', tension: 0.2, }, ], }), [repository]); return ( <Line options={{ responsive: true, }} data={data} /> ); } ``` 我們使用“chart.js”函式庫來繪製“Line”類型的圖表。 這非常簡單,因為我們在伺服器端完成了所有資料結構。 這裡需要注意的一件大事是我們「匯出預設值」我們的 ChartComponent。那是因為它使用了「Canvas」。這在伺服器端不可用,我們需要延遲載入該元件。 讓我們修改“main.tsx”: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import dynamic from "next/dynamic"; import {useCallback, useState} from "react"; const ChartComponent = dynamic(() => import('@/components/chart'), { ssr: false, }) interface List { name: string, list: Repository[] } export default function Main({list}: {list: List[]}) { const [repositoryState, setRepositoryState] = useState(list); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> <ChartComponent repository={val.list} /> </div> </div> </div> ))} </div> </div> ) } ``` 您可以看到我們使用“nextjs/dynamic”來延遲載入元件。 我希望將來 NextJS 能為客戶端元件加入類似「使用延遲載入」的內容 😺 --- ## 但是新星呢?來認識一下 Trigger.Dev! 每天加入新星星的最佳方法是執行 cron 請求來檢查新加入的星星並將其加入到我們的資料庫中。 不要使用 Vercel cron / GitHub 操作,或(上帝禁止)為此建立一個新伺服器。 我們可以使用 [Trigger.DEV](http://Trigger.DEV) 直接與我們的 NextJS 應用程式搭配使用。 那麼就讓我們來設定一下吧! 註冊 [Trigger.dev 帳號](https://trigger.dev/)。 註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![新組織](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdnxq8o7el7t4utvgf1u.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![NextJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4kt7e5r1mwg60atqfka.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![開發金鑰](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ser7a2j5qft9vw8rfk0m.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段以安裝 Trigger.dev。 仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` 讓我們建立 TriggerDev 作業! 您將看到一個新建立的資料夾,名為“jobs”。 在那裡建立一個名為“sync.stars.ts”的新文件 新增以下程式碼: ``` import { cronTrigger, invokeTrigger } from "@trigger.dev/sdk"; import { client } from "@/trigger"; import { prisma } from "../../helper/prisma"; import axios from "axios"; import { z } from "zod"; // Your first job // This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline. client.defineJob({ id: "sync-stars", name: "Sync Stars Daily", version: "0.0.1", // Run a cron every day at 23:00 AM trigger: cronTrigger({ cron: "0 23 * * *", }), run: async (payload, io, ctx) => { const repos = await io.runTask("get-stars", async () => { // get all libraries and current amount of stars return await prisma.repository.groupBy({ by: ["name"], _sum: { stars: true, }, }); }); //loop through all repos and invoke the Job that gets the latest stars for (const repo of repos) { getStars.invoke(repo.name, { name: repo.name, previousStarCount: repo?._sum?.stars || 0, }); } }, }); const getStars = client.defineJob({ id: "get-latest-stars", name: "Get latest stars", version: "0.0.1", // Run a cron every day at 23:00 AM trigger: invokeTrigger({ schema: z.object({ name: z.string(), previousStarCount: z.number(), }), }), run: async (payload, io, ctx) => { const stargazers_count = await io.runTask("get-stars", async () => { const { data } = await axios.get( `https://api.github.com/repos/${payload.name}`, { headers: { authorization: `token ${process.env.TOKEN}`, }, } ); return data.stargazers_count as number; }); await prisma.repository.upsert({ where: { name_day_month_year: { name: payload.name, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(), }, }, update: { stars: stargazers_count - payload.previousStarCount, }, create: { name: payload.name, stars: stargazers_count - payload.previousStarCount, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(), }, }); }, }); ``` 我們建立了一個名為“Sync Stars Daily”的新作業,該作業將在每天下午 23:00 執行 - 它在 cron 文本中的表示為:`0 23 * * *` 我們在資料庫中取得所有目前儲存庫,按名稱將它們分組,並對星星進行求和。 由於一切都在 Vercel 無伺服器上執行,因此我們可能會在檢查所有儲存庫時遇到逾時。 為此,我們將每個儲存庫傳送到不同的作業。 我們使用“invoke”建立新作業,然後在“獲取最新的星星”中處理它們 我們迭代所有新儲存庫並獲取當前的星星數量。 我們用舊的星星數量去除新的星星數量,得到今天的星星數量。 我們使用“prisma”將其新增至資料庫。沒有比這更簡單的了! 最後一件事是編輯“jobs/index.ts”並將內容替換為: ``` export * from "./sync.stars"; ``` 你就完成了🥳 --- ## 讓我們聯絡吧! 🔌 作為開源開發者,我們邀請您加入我們的[社群](https://discord.gg/nkqV9xBYWy),以做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: [https://github.com/triggerdotdev/blog/tree/main/stars-monitor](https://github.com/triggerdotdev/blog/tree/main/stars-monitor) 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/take-nextjs-to-the-next-level-create-a-github-stars-monitor-130a

使用 Python 和 Pytest 自動化 API 測試

您是否想過如何使用 python 測試您的 API?在本文中,我們將學習如何使用 Python 和 pytest 框架來測試我們的 API。 對於本教程,您需要安裝 python,您可以在[此處](https://www.python.org/downloads/)下載它 --- ### 簡介: - [什麼是 Python 和 Pytest 框架](#what-is-python-and-pytest-framework) - [我們專案的設定](#configuration-of-our-project) - [使用python建立虛擬環境](#creation-of-virtual-environment-with-python) - [測試的依賴關係設定](#setup-of-dependency-for-the-tests) - [建立我們的第一個測試](#creating-our-first-test) - [將被測試的 API 的定義](#definition-of-the-api-that-will-be-tested) - [建立我們的測試](#creating-our-test) - [重建我們的測試](#refactoring-our-tests) - [產生 html 報告結果](#generate-html-report-result) - [結論](#conclusion) --- ## 什麼是 Python 和 Pytest 框架 「Python」是一種高級通用程式語言,以其簡單性和可讀性而聞名。它由 Guido van Rossum 建立,於 1991 年首次發布。 Python 的設計易於學習,並且具有乾淨簡潔的語法,這使其成為初學者和經驗豐富的程式設計師的流行選擇。 「pytest」框架可以輕鬆編寫小型、可讀的測試,並且可以擴展以支援應用程式和程式庫的複雜功能測試。 --- ## 我們專案的配置 ### 用python建立虛擬環境 在開始建立之前,我們先來了解一下什麼是Python上的虛擬環境。 Python 中的虛擬環境是一個獨立的目錄或資料夾,可讓您為專案建立和管理隔離的 Python 環境。透過環境,您可以輕鬆管理依賴項,避免與不同版本的 python 發生衝突。 虛擬環境(除其他外)是: - 用於包含支援專案(庫或應用程式)所需的特定 Python 解釋器以及軟體庫和二進位檔案。預設情況下,它們與其他虛擬環境中的軟體以及作業系統中安裝的 Python 解釋器和庫隔離。 - 包含在專案目錄中的目錄中,通常名為“venv”或“.venv”,或在許多虛擬環境的容器目錄下,例如“~/.virtualenvs”。 - 未簽入原始碼控制系統(例如 Git)。 - 被認為是一次性的 - 應該很容易刪除並從頭開始重新建立它。您沒有在環境中放置任何專案程式碼 - 不被視為可移動或可複製 - 您只需在目標位置重新建立相同的環境。 您可以在[此處](https://docs.python.org/3/library/venv.html#venv-def)閱讀有關 python 環境的更多資訊。 #### 視窗 首先,為您的專案建立一個資料夾,然後打開 cmd 並使用命令 cd 導航到該資料夾: ``` cd tests_with_python ``` 如果您不知道資料夾在哪裡,可以執行命令“ls”,您將看到資料夾列表,並且可以瀏覽它們。在我們的專案資料夾中,執行以下命令: ``` python -m venv name_of_environment ``` 您的環境名稱可以是任何人,只需記住python 區分大小寫,請查看[PEP 8 風格指南](https://peps.python.org/pep-0008/) 以了解有關Python 約定的更多資訊. 要啟動我們的環境,我們使用以下命令: ``` name_of_environment\Scripts\Activate ``` 如果一切正確,您的環境將被激活,並且在 cmd 上您將看到如下所示: ``` (name_of_environment) C:\User\tests ``` 要停用您的環境,只需執行: ``` deactivate ``` #### Linux 或 MacOS 為您的專案建立一個資料夾,然後打開 cmd 並使用命令 cd 導航到該資料夾: ``` cd tests_with_python ``` 要啟動我們的環境,我們使用以下命令: ``` source name_of_environment/bin/activate ``` 如果一切正確,您的環境將被激活,並且在 cmd 上您將看到如下所示: ``` (name_of_environment) your_user_name tests % ``` 要停用您的環境,只需執行: ``` deactivate ``` ### 設定測試的依賴關係 當我們要測試 API 時,我們需要安裝依賴項來幫助我們進行測試,首先我們將安裝「requests」函式庫來幫助我們發出請求: PS:在執行此命令之前請確保您的環境已激活 ``` pip install requests ``` 為了進行測試,我們將安裝「pytests」框架: ``` pip install pytest ``` --- ## 建立我們的第一個測試 ### 將要測試的 API 的定義 在本教程中,我們將使用返回小行星列表的 Nasa API:[Asteroids - NeoWs](https://api.nasa.gov/#donkiGST),我們將測試檢索基於小行星列表的端點在他們最接近地球的日期。 關於API: - 基本網址:`https://api.nasa.gov/neo/rest/v1/feed` - 查詢參數: |參數|類型|預設|描述| | --------|---------|--------|--------------------| |start_date|YYYY-MM-DD|無|小行星搜尋的開始日期| |end_date|YYYY-MM-DD|start_date後7天|小行星搜尋的結束日期| |api_key|字串|DEMO_KEY|用於擴展用途的 api.nasa.gov 密鑰| 在本教程中,我們將重點放在三種類型的測試: - 合約:如果 API 能夠驗證傳送的查詢參數 - 狀態:狀態程式碼是否正確 - 身份驗證:即使這個API不需要令牌,我們也可以用它來做測試 我們的場景: |方法|測試|預期結果 | | --------|--------|--------------------| |獲取 |搜尋成功 | - 傳回狀態程式碼 200<br/> 正文回應包含小行星清單| |獲取 |無需任何查詢參數即可搜尋 | - 返回狀態碼403<br/>| |獲取 |僅搜尋開始日期| - 傳回狀態程式碼 200 <br/> 主體回應包含小行星清單| |獲取 |僅搜尋結束日期| - 傳回狀態程式碼 200 <br/> 主體回應包含小行星清單| |獲取 |在有效日期範圍內搜尋| - 傳回狀態碼 200<br/> - 正文回應包含所有非空白欄位| |獲取 |當開始日期大於結束日期時進行搜尋| - 傳回狀態程式碼 400 <br/>| |獲取 |使用無效的 API 令牌進行搜尋| - 傳回狀態程式碼 403 <br/> 主體回應包含小行星清單| ### 建立我們的測試 首先,我們將建立一個名為「tests.py」的文件,我們將在該文件中編寫測試。為了幫助我們使用良好的實踐並編寫良好的自動化測試,讓我們使用 [TDD(測試驅動開發)](https://www.browserstack.com/guide/what-is-test-driven-development?psafe_param=1?keyword=&campaignid=&adgroupid=&adid=8784011037660164696&utm_source=google&utm_medium=cpc&utm_platform=paidads&utm_content=602353912717&utm_campapaidads&utm_content=602353912717&utm_campa.utm_term=+&gad_source=1&gclid=CjwKCAiAxreqBhAxEiwAfGfndN8P705lwnkvEFnCz_lueR2hnhmZXgboBQEtKTaCIRbhcb1SXOxBYhoC-WoQAD_BwwE)技術。 該技術包括: - 紅色 - 進行失敗的測試 - 綠色 - 使此測試通過 - 重構 - 重構所做的事情,刪除重複的內容 為了編寫一套好的測試,我們將使用 3A 技術: - 安排:準備上下文。 - 行動:執行我們想要示範的行動。 - 斷言:表明我們預期的結果確實發生了。 從紅色開始,使用 3A 技術,我們將編寫第一個測試「成功搜尋小行星」: ``` import pytest def test_search_asteroids_with_sucess(): # Arrange: api_key = "DEMO_KEY" #Act: response = make_request(api_key) #Assertion: assert response.status_code == 200 # Validation of status code data = response.json() # Assertion of body response content: assert len(data) > 0 assert data["element_count"] > 0 ``` - 安排:我們建立一個變數來插入 api_key,在此步驟中,您可以插入執行測試所需的任何資料。通常,在這一步驟我們會建立模擬資料。 - Act:在這一步驟中我們呼叫了負責發出請求的方法 - 斷言:我們驗證回應 `方法或類別的名稱應以 test 開頭` 若要執行我們的測試,請在命令提示字元中執行: ``` pytest test.py ``` 我們將收到一個錯誤,因為我們沒有建立執行請求的方法: ``` test.py F [100%] ====================================================================== FAILURES ====================================================================== _________________________________________________________ test_search_asteroids_with_sucess __________________________________________________________ def test_search_asteroids_with_sucess(): > response = make_request() E NameError: name 'make_request' is not defined test.py:5: NameError ============================================================== short test summary info =============================================================== FAILED test.py::test_search_asteroids_with_sucess - NameError: name 'make_request' is not defined ================================================================= 1 failed in 0.01s ================================================================== ``` 現在,讓我們建立方法來執行請求: ``` import requests def make_request(api_key): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?api_key={api_key}') return response ``` 現在,再次執行我們的測試: ``` ================================================================ test session starts ================================================================= platform darwin -- Python 3.11.5, pytest-7.4.3, pluggy-1.3.0 rootdir: /Users/Documents/tests_python collected 1 item test.py . [100%] ================================================================= 1 passed in 20.22s ================================================================= ``` --- ## 重構我們的測試 現在我們已經了解如何使用 pytest 建立測試以及如何建立請求,我們可以編寫其他測試並開始重構測試。我們要做的第一個重構是從測試文件中刪除請求方法。我們將建立一個名為「make_requests.py」的新文件,其中將包含我們的請求,並將我們所做的請求移至此文件: ``` import requests def make_request(api_key): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?api_key={api_key}') return response ``` 現在,我們需要考慮在其他測試中重複使用此方法,因為我們需要為不同的測試傳遞不同的參數。我們可以透過很多方法來做到這一點,在本教程中,我們將參數的名稱從“api_key”更改為“query_parameters”。我們這樣做是為了讓我們的方法更加靈活,我們可以一次傳遞參數進行測試: ``` import requests def make_request(query_parameters): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?{query_parameters}') return response ``` 之後,我們需要更改我們的測試文件。我們將導入我們建立的這個方法: ``` from make_requests import make_request ``` 為了以更好的方式組織我們的測試,並遵循 pytest 文件的建議,我們將測試移至類別「TestClass」: 再次執行我們的測試: ``` ============================= test session starts ============================== collecting ... collected 7 items test.py::TestClass::test_search_asteroids_with_sucess test.py::TestClass::test_search_asteroids_with_query_parameters_empty test.py::TestClass::test_search_asteroids_with_start_date test.py::TestClass::test_search_asteroids_with_end_date test.py::TestClass::test_search_asteroids_in_valid_range test.py::TestClass::test_search_asteroids_in_invalid_range test.py::TestClass::test_search_asteroids_in_invalid_token ============================== 7 passed in 5.85s =============================== PASSED [ 14%]PASSED [ 28%]PASSED [ 42%]PASSED [ 57%]PASSED [ 71%]PASSED [ 85%]PASSED [100%] Process finished with exit code 0 ``` ### 產生 html 報告結果 為了更好地視覺化您的測試結果,我們可以使用 pytest-html-reporter 庫產生報告 html,為此,我們首先需要安裝該套件: ``` pip install pytest-html ``` 若要產生報告,請在執行測試時新增: ``` pytest test.py --html-report=./report/report.html ``` 將產生一個包含測試結果的 .html 文件,如下所示: ![report_example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eol3v7iex84t7prmyb9c.png) ## 結論 本文是一篇教程,介紹如何開始使用 python 和 pytest 框架為 API 編寫自動化測試以及如何產生一個報告 html。 您可以在[此處](https://github.com/aliciamarianne1507/tests_python)存取本教學中使用的專案。 我希望這些內容對您有用。 如果您有任何疑問,請隨時與我聯繫! 親親,下週見💅🏼 --- 原文出處:https://dev.to/m4rri4nne/automating-your-api-tests-using-python-and-pytest-23cc

Giskard 如何獲得第一批 1,200 顆星!

要將其作為影片觀看,請查看此 **YouTube**: https://www.youtube.com/watch?v=uEiWbR7wai0 --- 網紅和權威 =========================== 如果說世界上有什麼比任何行銷都更強大的話,那就是影響力。伊隆馬斯克被認為是有史以來最會貼廢文的人之一,但他發布的所有內容仍然引起了大量的迴響。 很多人傾向於相信我,因為我也在 GitHub 領域建立了一些權威。 這就是為什麼影響者是如此強大的管道。他們的權威和追隨者的意圖可以為公司帶來巨大的轉變。 Giskard 購買了[Santiago's](https://twitter.com/svpino?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-giskard-got-their-first-1-200-starsign=how-giskard-got-their-first-1-200-stars) 服務來推廣他們的套件,並將其數量增加了一倍星星。 雖然星星是很好的指標,但正如你所知,我從有影響力的人那裡得到了很多星星,這些星星不一定會給我帶來合適的受眾,但會讓我在 GitHub 上成為趨勢。像聖地亞哥這樣的影響者是專注於雷射的影響者,他們實際上可以帶來客戶和良好的用戶(有購買力),**通常更少的明星。** 他們向優秀的影響者支付約 **700-1200 美元**。 ![](https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/75633df4-1274-46e2-b1b4-a179cb45a4fb/image.png) --- 錯誤的副產品? ---------------------- Giskard 製作了一個名為 **SafeGPT** 的小產品 - 拼字檢查器。 他們在 [Super human newsletter](https://www.joinsuperhuman.ai/?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-giskard-got-their-first-1-200-newsstarHacker) 以及[Showhower](https://news.ycombinator.com/show?utm_source=nevo.github20k.com&utm_medium=referral&utm_campaign=how-giskard-got-their-first-1-200-stars) 並獲得**2,000** 1週內註冊🔥 主要問題是 SafeGPT 是面向最終用戶的最終產品 **(不一定是開發人員),** 當他們嘗試將這些使用者轉換為 Giskard 時,他們沒有轉換,因為這是一個不同的市場。 實際上,我認為這並不是失敗。仍有一些方法可以利用非觀眾來幫助您完成不同的事情。 **你可以:** * 取得不同文章/Youtube 的反應 * 獲得一些社會證明(在社群媒體上關注) * 透過獎勵獲得星星。 --- 原文出處:https://dev.to/github20k/how-giskard-got-their-first-1200-stars-b8k

🚀使用 NextJS、Trigger.dev 和 GPT4 做一個履歷表產生器🔥✨

## 簡介 在本文中,您將學習如何使用 NextJS、Trigger.dev、Resend 和 OpenAI 建立簡歷產生器。 😲 - 加入基本詳細訊息,例如名字、姓氏和最後工作地點。 - 產生詳細訊息,例如個人資料摘要、工作經歷和工作職責。 - 建立包含所有資訊的 PDF。 - 將所有內容傳送到您的電子郵件 ![猴子手錶](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23k6hee187s62k8y1dmd.gif) *** ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 https://github.com/triggerdotdev/trigger.dev --- ## 讓我們來設定一下吧🔥 使用 NextJS 設定一個新專案 ``` npx create-next-app@latest ``` 我們將建立一個包含基本資訊的簡單表單,例如: - 名 - 姓 - 電子郵件地址 - 你的頭像 - 以及你今天為止的經驗! ![輸入](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01mmvn0lvw1p1i4knoa8.png) 我們將使用 NextJS 的新應用程式路由器。 開啟`layout.tsx`並加入以下程式碼 ``` import { GeistSans } from "geist/font"; import "./globals.css"; const defaultUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; export const metadata = { metadataBase: new URL(defaultUrl), title: "Resume Builder with GPT4", description: "The fastest way to build a resume with GPT4", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={GeistSans.className}> <body className="bg-background text-foreground"> <main className="min-h-screen flex flex-col items-center"> {children} </main> </body> </html> ); } ``` 我們基本上是為所有頁面設定佈局(即使我們只有一頁。) 我們設定基本的頁面元資料、背景和全域 CSS 元素。 接下來,讓我們打開“page.tsx”並加入以下程式碼: ``` <div className="flex-1 w-full flex flex-col items-center"> <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16"> <div className="w-full max-w-6xl flex justify-between items-center p-3 text-sm"> <span className="font-bold select-none">resumeGPT.</span> </div> </nav> <div className="animate-in flex-1 flex flex-col opacity-0 max-w-6xl px-3"> <Home /> </div> </div> ``` 這設定了我們的resumeGPT 的標題和主要的家庭元件。 <小時/> ## 建立表單的最簡單方法 保存表單資訊並驗證欄位最簡單的方法是使用react-hook-form。 我們將上傳個人資料照片。 為此,我們不能使用基於 JSON 的請求。 我們需要將 JSON 轉換為有效的表單資料。 那麼就讓我們把它們全部安裝吧! ``` npm install react-hook-form object-to-formdata axios --save ``` 建立一個名為 Components 的新資料夾,新增一個名為「Home.tsx」的新文件,並新增以下程式碼: ``` "use client"; import React, { useState } from "react"; import {FormProvider, useForm} from "react-hook-form"; import Companies from "@/components/Companies"; import axios from "axios"; import {serialize} from "object-to-formdata"; export type TUserDetails = { firstName: string; lastName: string; photo: string; email: string; companies: TCompany[]; }; export type TCompany = { companyName: string; position: string; workedYears: string; technologies: string; }; const Home = () => { const [finished, setFinished] = useState<boolean>(false); const methods = useForm<TUserDetails>() const { register, handleSubmit, formState: { errors }, } = methods; const handleFormSubmit = async (values: TUserDetails) => { axios.post('/api/create', serialize(values)); setFinished(true); }; if (finished) { return ( <div className="mt-10">Sent to the queue! Check your email</div> ) } return ( <div className="flex flex-col items-center justify-center p-7"> <div className="w-full py-3 bg-slate-500 items-center justify-center flex flex-col rounded-t-lg text-white"> <h1 className="font-bold text-white text-3xl">Resume Builder</h1> <p className="text-gray-300"> Generate a resume with GPT in seconds 🚀 </p> </div> <FormProvider {...methods}> <form onSubmit={handleSubmit(handleFormSubmit)} className="p-4 w-full flex flex-col" > <div className="flex flex-col lg:flex-row gap-4"> <div className="flex flex-col w-full"> <label htmlFor="firstName">First name</label> <input type="text" required id="firstName" placeholder="e.g. John" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('firstName')} /> </div> <div className="flex flex-col w-full"> <label htmlFor="lastName">Last name</label> <input type="text" required id="lastName" placeholder="e.g. Doe" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('lastName')} /> </div> </div> <hr className="w-full h-1 mt-3" /> <label htmlFor="email">Email Address</label> <input type="email" required id="email" placeholder="e.g. [email protected]" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('email', {required: true, pattern: /^\S+@\S+$/i})} /> <hr className="w-full h-1 mt-3" /> <label htmlFor="photo">Upload your image 😎</label> <input type="file" id="photo" accept="image/x-png" className="p-3 rounded-md outline-none border border-gray-500 mb-3" {...register('photo', {required: true})} /> <Companies /> <button className="p-4 pointer outline-none bg-blue-500 border-none text-white text-base font-semibold rounded-lg"> CREATE RESUME </button> </form> </FormProvider> </div> ); }; export default Home; ``` 您可以看到我們從「使用客戶端」開始,它基本上告訴我們的元件它應該只在客戶端上執行。 為什麼我們只需要客戶端? React 狀態(輸入變更)僅在用戶端可用。 我們設定兩個接口,「TUserDetails」和「TCompany」。它們代表了我們正在使用的資料的結構。 我們將“useForm”與“react-hook-form”一起使用。它為我們的輸入建立了本地狀態管理,並允許我們輕鬆更新和驗證我們的欄位。您可以看到,在每個「輸入」中,都有一個簡單的「註冊」函數,用於指定輸入名稱和驗證並將其註冊到託管狀態。 這很酷,因為我們不需要使用像“onChange”這樣的東西 您還可以看到我們使用了“FormProvider”,這很重要,因為我們希望在子元件中擁有“react-hook-form”的上下文。 我們還有一個名為「handleFormSubmit」的方法。這是我們提交表單後呼叫的方法。您可以看到我們使用“serialize”函數將 javascript 物件轉換為 FormData,並向伺服器發送請求以使用“axios”啟動作業。 您可以看到另一個名為“Companies”的元件。該元件將讓我們指定我們工作過的所有公司。 那麼讓我們努力吧。 建立一個名為「Companies.tsx」的新文件 並加入以下程式碼: ``` import React, {useCallback, useEffect} from "react"; import { TCompany } from "./Home"; import {useFieldArray, useFormContext} from "react-hook-form"; const Companies = () => { const {control, register} = We(); const {fields: companies, append} = useFieldArray({ control, name: "companies", }); const addCompany = useCallback(() => { append({ companyName: '', position: '', workedYears: '', technologies: '' }) }, [companies]); useEffect(() => { addCompany(); }, []); return ( <div className="mb-4"> {companies.length > 1 ? ( <h3 className="font-bold text-white text-3xl my-3"> Your list of Companies: </h3> ) : null} {companies.length > 1 && companies.slice(1).map((company, index) => ( <div key={index} className="mb-4 p-4 border bg-gray-800 rounded-lg shadow-md" > <div className="mb-2"> <label htmlFor={`companyName-${index}`} className="text-white"> Company Name </label> <input type="text" id={`companyName-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.companyName`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`position-${index}`} className="text-white"> Position </label> <input type="text" id={`position-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.position`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`workedYears-${index}`} className="text-white"> Worked Years </label> <input type="number" id={`workedYears-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.workedYears`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`workedYears-${index}`} className="text-white"> Technologies </label> <input type="text" id={`technologies-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.technologies`, {required: true})} /> </div> </div> ))} <button type="button" onClick={addCompany} className="mb-4 p-2 pointer outline-none bg-blue-900 w-full border-none text-white text-base font-semibold rounded-lg"> Add Company </button> </div> ); }; export default Companies; ``` 我們從 useFormContext 開始,它允許我們取得父元件的上下文。 接下來,我們使用 useFieldArray 建立一個名為 Companies 的新狀態。這是我們擁有的所有公司的一個陣列。 在「useEffect」中,我們新增陣列的第一項以對其進行迭代。 當點擊“addCompany”時,它會將另一個元素推送到陣列中。 我們已經和客戶完成了🥳 --- ## 解析HTTP請求 還記得我們向“/api/create”發送了一個“POST”請求嗎? 讓我們轉到 app/api 資料夾並在該資料夾中建立一個名為「create」的新資料夾,建立一個名為「route.tsx」的新檔案並貼上以下程式碼: ``` import {NextRequest, NextResponse} from "next/server"; import {client} from "@/trigger"; export async function POST(req: NextRequest) { const data = await req.formData(); const allArr = { name: data.getAll('companies[][companyName]'), position: data.getAll('companies[][position]'), workedYears: data.getAll('companies[][workedYears]'), technologies: data.getAll('companies[][technologies]'), }; const payload = { firstName: data.get('firstName'), lastName: data.get('lastName'), photo: Buffer.from((await (data.get('photo[0]') as File).arrayBuffer())).toString('base64'), email: data.get('email'), companies: allArr.name.map((name, index) => ({ companyName: allArr.name[index], position: allArr.position[index], workedYears: allArr.workedYears[index], technologies: allArr.technologies[index], })).filter((company) => company.companyName && company.position && company.workedYears && company.technologies) } await client.sendEvent({ name: 'create.resume', payload }); return NextResponse.json({ }) } ``` > 此程式碼只能在 NodeJS 版本 20+ 上運作。如果版本較低,將無法解析FormData。 該程式碼非常簡單。 - 我們使用 `req.formData` 將請求解析為 FormData - 我們將基於 FormData 的請求轉換為 JSON 檔案。 - 我們提取圖像並將其轉換為“base64” - 我們將所有內容傳送給 TriggerDev --- ## 製作履歷並將其發送到您的電子郵件📨 建立履歷是我們需要的長期任務 - 使用 ChatGPT 產生內容。 - 建立 PDF - 發送到您的電子郵件 由於某些原因,我們不想發出長時間執行的 HTTP 請求來執行所有這些操作。 1. 部署到 Vercel 時,無伺服器功能有 10 秒的限制。我們永遠不會準時到達。 2.我們希望讓用戶不會長時間掛起。這是一個糟糕的使用者體驗。如果用戶關閉窗口,整個過程將失敗。 ### 介紹 Trigger.dev! 使用 Trigger.dev,您可以在 NextJS 應用程式內執行後台進程!您不需要建立新伺服器。 他們也知道如何透過將長時間執行的作業無縫地分解為短期任務來處理它們。 註冊 [Trigger.dev 帳號](https://trigger.dev/)。註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![CreateOrg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shf1jsb4gio1zrjtz91d.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![下一頁](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5guppb6rot13myu6th5c.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![複製](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5gh527u7sthp6clkcfa.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段以安裝 Trigger.dev。仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中,執行以下程式碼片段以在 Trigger.dev 和 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` 讓我們建立 TriggerDev 作業! 前往新建立的資料夾 jobs 並建立一個名為「create.resume.ts」的新檔案。 新增以下程式碼: ``` client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { } }); ``` 這將為我們建立一個名為「create-resume」的新工作。 如您所見,我們先前從「route.tsx」發送的請求進行了架構驗證。這將為我們提供驗證和“自動完成”。 我們將在這裡執行三項工作 - 聊天GPT - PDF建立 - 電子郵件發送 讓我們從 ChatGPT 開始。 [建立 OpenAI 帳戶](https://platform.openai.com/) 並產生 API 金鑰。 ![ChatGPT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png) 從下拉清單中按一下「檢視 API 金鑰」以建立 API 金鑰。 ![ApiKey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png) 接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。 ``` npm install @trigger.dev/openai ``` 將您的 OpenAI API 金鑰新增至 `.env.local` 檔案中。 ``` OPENAI_API_KEY=<your_api_key> ``` 在根目錄中建立一個名為「utils」的新資料夾。 在該目錄中,建立一個名為「openai.ts」的新文件 新增以下程式碼: ``` import { OpenAI } from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY!, }); export async function generateResumeText(prompt: string) { const response = await openai.completions.create({ model: "text-davinci-003", prompt, max_tokens: 250, temperature: 0.7, top_p: 1, frequency_penalty: 1, presence_penalty: 1, }); return response.choices[0].text.trim(); } export const prompts = { profileSummary: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technologies: ${knownTechnologies}. Can you write a 100 words description for the top of the resume(first person writing)?`, jobResponsibilities: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technolegies: ${knownTechnologies}. Can you write 3 points for a resume on what I am good at?`, workHistory: (fullName: string, currentPosition: string, workingExperience: string, details: TCompany[]) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). ${companyDetails(details)} \n Can you write me 50 words for each company seperated in numbers of my succession in the company (in first person)?`, }; ``` 這段程式碼基本上建立了使用 ChatGPT 的基礎設施以及 3 個函數:「profileSummary」、「workingExperience」和「workHistory」。我們將使用它們來建立各部分的內容。 返回我們的「create.resume.ts」並新增作業: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompts } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); }, }); ``` 我們建立了一個名為「openai-task」的新任務。 在該任務中,我們使用 ChatGPT 同時執行三個提示,並返回它們。 --- ## 建立 PDF 建立 PDF 的方法有很多種 - 您可以使用 HTML2CANVAS 等工具並將 HTML 程式碼轉換為映像,然後轉換為 PDF。 - 您可以使用「puppeteer」之類的工具來抓取網頁並將其轉換為 PDF。 - 您可以使用不同的庫在後端建立 PDF。 在我們的例子中,我們將使用一個名為「jsPdf」的簡單函式庫,它是在後端建立 PDF 的非常簡單的函式庫。我鼓勵您使用 Puppeteer 和更多 HTML 來建立一些更強大的 PDF 檔案。 那我們來安裝它 ``` npm install jspdf @typs/jspdf --save ``` 讓我們回到「utils」並建立一個名為「resume.ts」的新檔案。該文件基本上會建立一個 PDF 文件,我們可以將其發送到使用者的電子郵件中。 加入以下內容: ``` import {TUserDetails} from "@/components/Home"; import {jsPDF} from "jspdf"; type ResumeProps = { userDetails: TUserDetails; picture: string; profileSummary: string; workHistory: string; jobResponsibilities: string; }; export function createResume({ userDetails, picture, workHistory, jobResponsibilities, profileSummary }: ResumeProps) { const doc = new jsPDF(); // Title block doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text(userDetails.firstName + ' ' + userDetails.lastName, 45, 27); doc.setLineWidth(0.5); doc.rect(20, 15, 170, 20); // x, y, width, height doc.addImage({ imageData: picture, x: 25, y: 17, width: 15, height: 15 }); // Reset font for the rest doc.setFontSize(12); doc.setFont('helvetica', 'normal'); // Personal Information block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Summary', 20, 50); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitText = doc.splitTextToSize(profileSummary, 170); doc.text(splitText, 20, 60); const newY = splitText.length * 5; // Work history block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Work History', 20, newY + 65); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitWork = doc.splitTextToSize(workHistory, 170); doc.text(splitWork, 20, newY + 75); const newNewY = splitWork.length * 5; // Job Responsibilities block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Job Responsibilities', 20, newY + newNewY + 75); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitJob = doc.splitTextToSize(jobResponsibilities, 170); doc.text(splitJob, 20, newY + newNewY + 85); return doc.output("datauristring"); } ``` 該文件包含三個部分:「個人資訊」、「工作歷史」和「工作職責」區塊。 我們計算每個區塊的位置和內容。 一切都是以“絕對”的方式設置的。 值得注意的是“splitTextToSize”將文字分成多行,因此它不會超出螢幕。 ![恢復](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdolng9e5ojev895x8i5.png) 現在,讓我們建立下一個任務:再次開啟 `resume.ts` 並新增以下程式碼: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompts } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; import { createResume } from "@/utils/resume"; const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); console.log('passed chatgpt'); const pdf = await io.runTask('convert-to-html', async () => { const resume = createResume({ userDetails: payload, picture: payload.photo, profileSummary: texts[0], jobResponsibilities: texts[1], workHistory: texts[2], }); return {final: resume.split(',')[1]} }); console.log('converted to pdf'); }, }); ``` 您可以看到我們新增了一個名為「convert-to-html」的新任務。這將為我們建立 PDF,將其轉換為 base64 並返回。 --- ## 讓他們知道🎤 我們即將到達終點! 剩下的唯一一件事就是與用戶分享。 您可以使用任何您想要的電子郵件服務。 我們將使用 Resend.com 造訪[註冊頁面](https://resend.com/signup),建立帳戶和 API 金鑰,並將其儲存到 `.env.local` 檔案中。 ``` RESEND_API_KEY=<place_your_API_key> ``` ![密鑰](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yncrarbwcs65j44fs91y.png) 將 [Trigger.dev Resend 整合套件](https://trigger.dev/docs/integrations/apis/resend) 安裝到您的 Next.js 專案。 ``` npm install @trigger.dev/resend ``` 剩下要做的就是加入我們的最後一項工作! 幸運的是,Trigger 直接與 Resend 集成,因此我們不需要建立新的「正常」任務。 這是最終的程式碼: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompt } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; import { createResume } from "@/utils/resume"; import { Resend } from "@trigger.dev/resend"; const resend = new Resend({ id: "resend", apiKey: process.env.RESEND_API_KEY!, }); const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); console.log('passed chatgpt'); const pdf = await io.runTask('convert-to-html', async () => { const resume = createResume({ userDetails: payload, picture: payload.photo, profileSummary: texts[0], jobResponsibilities: texts[1], workHistory: texts[2], }); return {final: resume.split(',')[1]} }); console.log('converted to pdf'); await io.resend.sendEmail('send-email', { to: payload.email, subject: 'Resume', html: 'Your resume is attached!', attachments: [ { filename: 'resume.pdf', content: Buffer.from(pdf.final, 'base64'), contentType: 'application/pdf', } ], from: "Nevo David <[email protected]>", }); console.log('Sent email'); }, }); ``` 我們在檔案頂部的「Resend」實例載入了儀表板中的 API 金鑰。 我們有 ``` integrations: { resend }, ``` 我們將其加入到我們的作業中,以便稍後在“io”內部使用。 最後,我們的工作是發送 PDF `io.resend.sendEmail` 值得注意的是其中的附件,其中包含我們在上一步中產生的 PDF 文件。 我們就完成了🎉 ![我們完成了](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esfhlds2qv1013c6x2h3.png) 您可以在此處檢查並執行完整的源程式碼: https://github.com/triggerdotdev/blog --- ## 讓我們聯絡吧! 🔌 作為開源開發者,我們邀請您加入我們的[社群](https://discord.gg/nkqV9xBYWy),以做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: https://github.com/triggerdotdev/blog/tree/main/blog-resume-builder 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/creating-a-resume-builder-with-nextjs-triggerdev-and-gpt4-4gmf

React 設計模式 Design Patterns

![](https://refine.ams3.cdn.digitaloceanspaces.com/blog-banners/retool-alternative.png) ## 介紹: React 開發人員可以透過使用設計模式來節省時間和精力,設計模式提供了一種使用經過測試且可信賴的解決方案來解決問題的快速方法。它們支援低耦合的內聚模組,從而幫助 React 開發人員建立可維護、可擴展且高效的應用程式。在本文中,我們將探索 React 設計模式並研究它們如何改進 React 應用程式的開發。 ## 容器和表示模式 容器和表示模式是一種旨在將反應程式碼中的表示邏輯與業務邏輯分離的模式,從而使其模組化、可測試並遵循關注點分離原則。 大多數情況下,在 React 應用程式中,我們需要從後端/儲存取得資料或計算邏輯並在 React 元件上表示該計算的結果。在這些情況下,容器和表示模式大放異彩,因為它可用於將元件分為兩類,即: * 容器元件,充當負責資料取得或計算的元件。 * 表示元件,其工作是將獲取的資料或計算值呈現在 UI(使用者介面)上。 容器和表示模式的範例如下所示: ``` import React, { useEffect } from 'react'; import CharacterList from './CharacterList'; const StarWarsCharactersContainer:React.FC = () => { const [characters, setCharacters] = useState<Character>([]) const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<boolean>(false); const getCharacters = async () => { setIsLoading(true); try { const response = await fetch("https://akabab.github.io/starwars-api/api/all.json"); const data = await response.json(); setIsLoading(false); if (!data) return; setCharacters(data); } catch(err) { setError(true); } finally { setIsLoading(true); } }; useEffect(() => { getCharacters(); }, []); return <CharacterList loading={loading} error={error} characters={characters} />; }; export default StarWarsCharactersContainer; ``` ``` // the component is responsible for displaying the characters import React from 'react'; import { Character } from './types'; interface CharacterListProps { loading: boolean; error: boolean; users: Character[]; } const CharacterList: React.FC<CharacterListProps> = ({ loading, error, characters }) => { if (loading && !error) return <div>Loading...</div>; if (!loading && error) return <div>error occured.unable to load characters</div>; if (!characters) return null; return ( <ul> {characters.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default CharacterList; ``` ## 有 Hooks 的元件組合 Hooks 是 React 16.8 中首次推出的全新功能。從那時起,他們在開發 React 應用程式中發揮了至關重要的作用。掛鉤是基本函數,可授予功能元件存取狀態和生命週期方法(以前僅可用於類別元件)的功能。另一方面,掛鉤可以專門設計來滿足元件要求並具有其他用例。 我們現在可以隔離所有狀態邏輯(一種需要反應性狀態變數的邏輯),並使用自訂掛鉤在元件中組合或使用它。因此,程式碼更加模組化和可測試,因為鉤子鬆散地綁定到元件,因此可以單獨測試。 帶有鉤子的元件組合示例如下所示: ``` // creating a custom hook that fetches star wars characters export const useFetchStarWarsCharacters = () => { const [characters, setCharacters] = useState<Character>([]) const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(false); const controller = new AbortController() const getCharacters = async () => { setIsLoading(true); try { const response = await fetch( "https://akabab.github.io/starwars-api/api/all.json", { method: "GET", credentials: "include", mode: "cors", headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, signal: controller.signal } ); const data = await response.json(); setIsLoading(false); if (!data) return; setCharacters(data); } catch(err) { setError(true); } finally { setIsLoading(true); } }; useEffect(() => { getCharacters(); return () => { controller.abort(); } }, []); return [ characters, isLoading, error ]; }; ``` 建立自訂鉤子後,我們將其導入到我們的 **StarWarsCharactersContainer** 元件中並使用它; ``` // importing the custom hook to a component and fetch the characters import React from 'react'; import { Character } from './types'; import { useFetchStarWarsCharacters } from './useFetchStarWarsCharacters'; const StarWarsCharactersContainer:React.FC = () => { const [ characters, isLoading, error ] = useFetchStarWarsCharacters(); return <CharacterList loading={loading} error={error} characters={characters} />; }; export default StarWarsCharactersContainer; ``` --- <橫幅隨機/> --- ## 使用Reducers進行狀態管理 大多數情況下,處理元件中的許多狀態會導致許多未分組狀態的問題,這可能是處理起來很麻煩且具有挑戰性的。在這種情況下,減速器模式可能是有用的選擇。我們可以使用減速器將狀態分類為某些操作,這些操作在執行時可以變更分組的狀態。 此模式允許使用它的開發人員控制元件和/或掛鉤的狀態管理,讓他們在發送事件時管理狀態變更。 使用減速器模式的範例如下所示: ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbob3gmfxws8k4ti0cyx.png) 在上面的程式碼中,元件調度兩個操作: * “**login**”操作類型會觸發狀態更改,影響三個狀態值,即 **loggedIn**、**user**、**token**。 *“**註銷**”操作只是將狀態重設為其初始值。 ## 提供者的資料管理 提供者模式對於資料管理非常有用,因為它利用上下文 API 透過應用程式的元件樹傳遞資料。這種模式是一種有效的解決支柱鑽井問題的方法,這一直是 React 開發中普遍關注的問題。 為了實現提供者模式,我們首先建立一個提供者元件。 Provider 是 Context 物件提供給我們的一個高階元件。我們可以利用React提供的createContext方法來建構一個Context物件。 ``` export const ThemeContext = React.createContext(null); export function ThemeProvider({ children }) { const [theme, setTheme] = React.useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } ``` 建立提供者後,我們將使用建立的提供者元件封裝依賴上下文 API 中的資料的元件。 為了從上下文 API 取得資料,我們呼叫 useContext 鉤子,它接受上下文作為參數(在本例中為 **ThemeContext**)。 ``` import { useContext } from 'react'; import { ThemeProvider, ThemeContext } from "../context"; const HeaderSection = () => { <ThemeProvider> <TopNav /> </ThemeProvider>; }; const TopNav = () => { const { theme, setTheme } = useContext(ThemeContext); return ( <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}> ... </div> ); }; ``` ## 使用 HOC(高階元件)增強元件 高階元件接受一個元件作為參數,並傳回一個注入了附加資料或功能的增壓元件。 React 中 HOC 的可能性是由於 React 更喜歡組合而不是繼承。 高階元件 (HOC) 模式提供了一種增加或修改元件功能的機制,促進元件重複使用和程式碼共用。 HOC 模式的範例如下所示: ``` import React from 'react' const higherOrderComponent = Component => { return class HOC extends React.Component { state = { name: 'John Doe' } render() { return <Component name={this.state.name {...this.props} /> } } const AvatarComponent = (props) => { return ( <div class="flex items-center justify-between"> <div class="rounded-full bg-red p-4"> {props.name} </div> <div> <p>I am a {props.description}.</p> </div> </div> ) } const SampleHOC = higherOrderComponent(AvatarComponent); const App = () => { return ( <div> <SampleHOC description="Frontend Engineer" /> </div> ) } export default App; ``` 在上面的程式碼中, **<AvatarComponent/>** 由 **higherOrderComponent** 提供 props,它將在內部使用。 ## 複合元件 複合元件模式是一種 React 設計模式,用於管理由子元件組成的父元件。 這種模式背後的原理是將父元件分解為更小的元件,然後使用 props、上下文或其他反應資料管理技術來管理這些較小元件之間的互動。 當需要建立由較小元件組成的可重複使用、多功能元件時,這種模式會派上用場。它使開發人員能夠建立複雜的 UI 元件,這些元件可以輕鬆自訂和擴展,同時保持清晰簡單的程式碼結構。 複合元件模式的用例範例如下所示: ``` import React, { createContext, useState } from 'react'; const ToggleContext = createContext(); function Toggle({ children }) { const [on, setOn] = useState(false); const toggle = () => setOn(!on); return ( <ToggleContext.Provider value={{ on, toggle }}> {children} </ToggleContext.Provider> ); } Toggle.On = function ToggleOn({ children }) { const { on } = useContext(ToggleContext); return on ? children : null; } Toggle.Off = function ToggleOff({ children }) { const { on } = useContext(ToggleContext); return on ? null : children; } Toggle.Button = function ToggleButton(props) { const { on, toggle } = useContext(ToggleContext); return <button onClick={toggle} {...props} />; } function App() { return ( <Toggle> <Toggle.On>The button is on</Toggle.On> <Toggle.Off>The button is off</Toggle.Off> <Toggle.Button>Toggle</Toggle.Button> </Toggle> ); } ``` ## 道具組合 這需要從幾個相關的 props 建立一個物件,並將其作為單個 props 傳遞給元件。 這種模式允許我們清理程式碼並使管理 props 變得更簡單,當我們想要將大量相關屬性傳遞給元件時,它特別有用。 ``` import React from 'react'; function P(props) { const { color, size, children, ...rest } = props; return ( <p style={{ color, fontSize: size }} {...rest}> { children } </p> ); } function App() { const paragraphProps = { color: "red", size: "20px", lineHeight: "22px" }; return <P {...paragraphProps}>This is a P</P>; } ``` ## 受控輸入 受控輸入模式可用於處理輸入欄位。此模式涉及使用事件處理程序在輸入欄位的值發生變更時更新元件狀態,以及將輸入欄位的目前值儲存在元件狀態中。 由於React 控制元件的狀態和行為,因此該模式使程式碼比不受控制的輸入模式更具可預測性和可讀性,後者不使用元件的狀態,而是直接透過DOM(文件物件模型)對其進行控制。 受控輸入模式的用例範例如下所示: ``` import React, { useState } from 'react'; function ControlledInput() { const [inputValue, setInputValue] = useState(''); const handleChange = (event) => { setInputValue(event.target.value); }; return ( <input type="text" value={inputValue} onChange={handleChange} /> ); } ``` ## 使用forwardRefs 管理自訂元件 稱為 ForwardRef 的高階元件將另一個元件作為輸入並輸出一個傳遞原始元件引用的新元件。透過這樣做,子元件的 ref(可用於檢索底層 DOM 節點或元件實例)可供父元件存取。 當建立與第三方程式庫或應用程式中的另一個自訂元件互動的自訂元件時,在工作流程中包含 ForwardRef 模式非常有幫助。透過授予對庫的 DOM 節點或另一個元件的 DOM 實例的存取權限,它有助於將此類元件的控制權轉移給您。 forwardRef 模式的用例範例如下所示: ``` import React from "react"; const CustomInput = React.forwardRef((props, ref) => ( <input type="text" {...props} ref={ref} /> )); const ParentComponent = () => { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, []); return <CustomInput ref={inputRef} />; }; ``` 在上面的程式碼中,我們使用「forwardRefs」從元件「<ParentComponent/>」觸發了另一個元件「<CustomInput/>」的焦點。 # 結論 我們在本文中討論了 React 設計模式,包括高階元件、容器呈現元件模式、複合元件、受控元件等等。透過將這些設計模式和最佳實踐合併到您的 React 專案中,您可以提高程式碼質量,促進團隊協作,並使您的應用程式更具可擴展性、靈活性和可維護性。 --- 原文出處:https://dev.to/refine/react-design-patterns-230o