使用 Nx 優化 Next.js 應用程序
已發表: 2022-03-10在本文中,我們將介紹如何使用 Nx 及其豐富的功能優化和構建高性能 Next.js 應用程序。 我們將介紹如何設置 Nx 服務器,如何向現有服務器添加插件,以及具有實用可視化的 monorepo 的概念。
如果您是一名希望優化應用程序並有效地跨應用程序創建可重用組件的開發人員,本文將向您展示如何快速擴展您的應用程序,以及如何使用 Nx。 要繼續學習,您將需要 Next.js 框架和 TypeScript 的基本知識。
什麼是 Nx?
Nx 是一個開源構建框架,可幫助您在任何規模上進行架構、測試和構建——與現代技術和庫無縫集成,同時提供強大的命令行界面 (CLI)、緩存和依賴項管理。 Nx 為開發人員提供用於現代框架、測試和工具的高級 CLI 工具和插件。
在本文中,我們將重點關注 Nx 如何與 Next.js 應用程序配合使用。 Nx 為您的 Next.js 應用程序(例如 Cypress、Storybook 和 styled-components)中的測試和样式提供標準工具。 Nx 為您的應用程序提供了一個 monorepo,創建一個可以容納多個應用程序的源代碼和庫的工作區,允許您在應用程序之間共享資源。
為什麼使用 Nx?
Nx 為開發人員提供了合理數量的開箱即用功能,包括用於對應用程序進行端到端 (E2E) 測試的樣板、樣式庫和 monorepo。
使用 Nx 帶來了許多優勢,我們將在本節中介紹其中的一些。
- 基於圖的任務執行
Nx 使用基於圖的分佈式任務執行和計算緩存來加速任務。 系統將使用圖形系統調度任務和命令,以確定哪個節點(即應用程序)應該執行每個任務。 這可以處理應用程序的執行並有效地優化執行時間。 - 測試
Nx 為單元測試和 E2E 測試提供預配置的測試工具。 - 緩存
Nx 還存儲緩存的項目圖。 這使其能夠僅重新分析更新的文件。 Nx 跟踪自上次提交以來更改的文件,並允許您僅對這些文件進行測試、構建和執行操作; 當您使用大型代碼庫時,這允許進行適當的優化。 - 依賴圖
可視依賴圖使您能夠檢查組件如何相互交互。 - 雲儲存
Nx 還提供雲存儲和 GitHub 集成,以便您可以與團隊成員共享鏈接以查看項目日誌。 - 代碼共享
為每個項目創建一個新的共享庫可能會非常費力。 Nx 消除了這種複雜性,讓您可以專注於應用程序的核心功能。 使用 Nx,您可以跨應用程序共享庫和組件。 您甚至可以在前端和後端應用程序之間共享可重用代碼。 - 支持單倉庫
Nx 為多個應用程序提供一個工作空間。 通過這種設置,一個 GitHub 存儲庫可以在您的工作區下存放各種應用程序的代碼源。
用於可發布庫的 Nx
Nx 允許您創建可發布的庫。 當您擁有將在 monorepo 之外使用的庫時,這是必不可少的。 在您使用 Nx Storybook 集成開發組織 UI 組件的任何情況下,Nx 都會在您的故事旁邊創建可發布的組件。 可發布組件可以編譯這些組件以創建一個庫包,您可以將其部署到外部註冊表。 生成庫時,您將使用--publishable
選項,這與--buildable
不同,後者用於生成僅在 monorepo 中使用的庫。 Nx 不會自動部署可發布的庫; 您可以通過諸如nx build mylib
(其中mylib
是庫的名稱)之類的命令調用構建,然後它將在dist
/ mylib
文件夾中生成一個優化的包,該包可以部署到外部註冊表。
Nx 讓您可以選擇使用 Next.js 作為預設創建新工作區,或將 Next.js 添加到現有工作區。
要使用 Next.js 作為預設創建新工作區,可以使用以下命令:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
此命令將使用名為“todo”的 Next.js 應用程序和styled-components
作為樣式庫創建一個新的 Nx 工作區。
然後,我們可以使用以下命令將 Next.js 應用程序添加到現有的 Nx 工作區:
npx nx g @nrwl/next:app
構建 Next.js 和 Nx 應用程序
Next.js 的 Nx 插件包括用於運行和優化 Next.js 應用程序的工具和執行器。 首先,我們需要創建一個新的 Nx 工作區,並將next
作為預設:
npx create-nx-workspace happynrwl \ --preset=next \ --style=styled-components \ --appName=todo
上面的代碼塊將生成一個新的 Nx 工作區和 Next.js 應用程序。 我們將收到使用 Nx Cloud 的提示。 對於本教程,我們將選擇“否”,然後等待我們的依賴項安裝。 完成後,我們應該有一個類似於以下的文件樹:
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-e2e ┃ ┗ .gitkeep ┣ libs ┣ node_modules ┣ tools ┣ .editorconfig ┣ .eslintrc.json ┣ .gitignore ┣ .prettierignore ┣ .prettierrc ┣ README.md ┣ babel.config.json ┣ jest.config.js ┣ jest.preset.js ┣ nx.json ┣ package-lock.json ┣ package.json ┣ tsconfig.base.json ┗ workspace.json
在apps
文件夾中,我們將擁有Next.js 應用程序“todo”,並為待辦事項應用程序預先配置了E2E 測試。 這一切都是使用強大的 Nx CLI 工具自動生成的。
要運行我們的應用程序,請使用npx nx serve todo
命令。 為應用程序提供服務後,您應該會看到以下屏幕:
構建 API
至此,我們已經搭建好了工作空間。 接下來是構建我們將在 Next.js 應用程序上使用的 CRUD API。 為此,我們將使用 Express; 為了演示對 monorepo 的支持,我們將在工作區中將服務器構建為應用程序。 首先,我們必須通過運行以下命令為 Nx 安裝 Express 插件:
npm install --save-dev @nrwl/express
完成後,我們就可以在提供的工作區中設置我們的 Express 應用程序了。 要生成 Express 應用程序,請運行以下命令:
npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo
命令nx g @nrwl/express:application
將生成一個 Express 應用程序,我們可以向其傳遞額外的規範參數; 要指定應用程序的名稱,請使用--name
標誌; 要指示將使用 Express 應用程序的前端應用程序,請將我們工作區中的應用程序名稱傳遞給--frontendProject
。 Express 應用程序還有一些其他選項可用。 完成後,我們將在apps
文件夾中擁有一個更新的文件結構,其中添加了todo-api
文件夾。
happynrwl ┣ apps ┃ ┣ todo ┃ ┣ todo-api ┃ ┣ todo-e2e ┃ ┗ .gitkeep …
todo-api
文件夾是一個帶有main.ts
入口文件的 Express 樣板文件。
/** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser app.get('/api', (req, res) => { res.send({ message: 'Welcome to todo-api!' }); }); const port = process.env.port || 3333; const server = app.listen(port, () => { console.log(`Listening at http://localhost:${port}/api`); }); server.on('error', console.error);
我們將在這個應用程序中創建我們的路線。 首先,我們將在 app 聲明下方使用兩個鍵值對item
和id
初始化一個對像數組。
/** * This is not a production server yet! * This is only minimal back end to get started. */ import * as express from 'express'; import {v4 as uuidV4} from 'uuid'; const app = express(); app.use(express.json()); // used instead of body-parser let todoArray: Array<{ item: string; id: string }> = [ { item: 'default todo', id: uuidV4() }, ]; …
接下來,我們將設置路由以獲取app.get()
下的所有待辦事項列表:
… app.get('/api', (req, res) => { res.status(200).json({ data: todoArray, }); }); …
上面的代碼塊將返回todoArray
的當前值。 隨後,我們將有用於從數組中創建、更新和刪除待辦事項的路線。
… app.post('/api', (req, res) => { const item: string = req.body.item; // Increment ID of item based on the ID of the last item in the array. let id: string = uuidV4(); // Add the new object to the array todoArray.push({ item, id }); res.status(200).json({ message: 'item added successfully', }); }); app.patch('/api', (req, res) => { // Value of the updated item const updatedItem: string = req.body.updatedItem; // ID of the position to update const id: string = req.body.id; // Find index of the ID const arrayIndex = todoArray.findIndex((obj) => obj.id === id); // Update item that matches the index todoArray[arrayIndex].item = updatedItem res.status(200).json({ message: 'item updated successfully', }); }); app.delete('/api', (req, res) => { // ID of the position to remove const id: string = req.body.id; // Update array and remove the object that matches the ID todoArray = todoArray.filter((val) => val.id !== id); res.status(200).json({ message: 'item removed successfully', }); }); …
要創建一個新的待辦事項,我們需要的只是新項目的值作為一個字符串。 我們將通過遞增服務器上數組中最後一個元素的 ID 來生成一個 ID。 要更新現有項目,我們將傳入項目的新值和要更新的項目對象的 ID; 在服務器上,我們將使用forEach
方法遍歷每個項目,並在 ID 與隨請求發送的 ID 匹配的位置更新項目。 最後,要從數組中刪除一個項目,我們將在請求中發送要刪除的項目 ID; 然後,我們對數組進行過濾,並返回一個新數組,其中包含與請求發送的 ID 不匹配的所有項目,並將新數組分配給todoArray
變量。
注意:如果您查看 Next.js 應用程序文件夾,您應該會看到一個proxy.conf.json
文件,其配置如下:
{ "/api": { "target": "http://localhost:3333", "secure": false } }
這將創建一個代理,允許所有對匹配/api
的路由的 API 調用以todo-api
服務器為目標。
使用 Nx 生成 Next.js 頁面
在我們的 Next.js 應用程序中,我們將生成一個新頁面home
和一個 item 組件。 Nx 提供了一個 CLI 工具,方便我們輕鬆創建頁面:
npx nx g @nrwl/next:page home
運行此命令後,我們將提示選擇要用於頁面的樣式庫; 對於本文,我們將選擇styled-components
。 瞧! 我們的頁面已創建。 要創建組件,請運行npx nx g @nrwl/next:component todo-item
; 這將使用todo-item
組件創建一個component
文件夾。
Next.js 應用程序中的 API 消耗
在每個待辦事項中,我們將有兩個按鈕,用於編輯和刪除待辦事項。 執行這些操作的異步函數作為 props 從主頁傳遞。
… export interface TodoItemProps { updateItem(id: string, updatedItem: string): Promise<void>; deleteItem(id: string): Promise<void>; fetchItems(): Promise<any>; item: string; id: string; } export const FlexWrapper = styled.div` width: 100%; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-top: 20px; @media all and (max-width: 470px) { flex-direction: column; input { width: 100%; } button { width: 100%; } } `; export function TodoItem(props: TodoItemProps) { const [isEditingItem, setIsEditingItem] = useState<boolean>(false); const [item, setNewItem] = useState<string | null>(null); return ( <FlexWrapper> <Input disabled={!isEditingItem} defaultValue={props.item} isEditing={isEditingItem} onChange={({ target }) => setNewItem(target.value)} /> {!isEditingItem && <Button onClick={() => setIsEditingItem(true)} > Edit </Button>} {isEditingItem && <Button onClick={async () => { await props.updateItem(props.id, item); //fetch updated items await props.fetchItems(); setIsEditingItem(false) }}> Update </Button>} <Button danger onClick={async () => { await props.deleteItem(props.id); //fetch updated items await await props.fetchItems(); }} > Delete </Button> </FlexWrapper> ); }
對於更新功能,我們有一個在isEditingItem
狀態為false
時禁用的輸入。 單擊“編輯”按鈕後,它isEditingItem
狀態切換為true
並顯示“更新”按鈕。 這裡啟用了輸入組件,用戶可以輸入一個新的值; 當點擊“更新”按鈕時,它會調用帶有傳入參數的updateItem
函數,並將isEditingItem
切換回false
。
在home
組件中,我們有執行 CRUD 操作的異步函數。
… const [items, setItems] = useState<Array<{ item: string; id: string }>>([]); const [newItem, setNewItem] = useState<string>(''); const fetchItems = async () => { try { const data = await fetch('/api/fetch'); const res = await data.json(); setItems(res.data); } catch (error) { console.log(error); } }; const createItem = async (item: string) => { try { const data = await fetch('/api', { method: 'POST', body: JSON.stringify({ item }), headers: { 'Content-Type': 'application/json', }, }); } catch (error) { console.log(error); } }; const deleteItem = async (id: string) => { try { const data = await fetch('/api', { method: 'DELETE', body: JSON.stringify({ id }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; const updateItem = async (id: string, updatedItem: string) => { try { const data = await fetch('/api', { method: 'PATCH', body: JSON.stringify({ id, updatedItem }), headers: { 'Content-Type': 'application/json', }, }); const res = await data.json(); alert(res.message); } catch (error) { console.log(error); } }; useEffect(() => { fetchItems(); }, []); …
在上面的代碼塊中,我們有fetchItems
,它從服務器返回todoArray
。 然後,我們有createItem
函數,它接受一個字符串; 該參數是新的待辦事項的值。 updateItem
函數有兩個參數,要更新的項目的 ID 和updatedItem
值。 並且deleteItem
函數刪除與傳入的 ID 匹配的項目。
為了呈現待辦事項,我們映射了items
狀態:
… return ( <StyledHome> <h1>Welcome to Home!</h1> <TodoWrapper> {items.length > 0 && items.map((val) => ( <TodoItem key={val.id} item={val.item} id={val.id} deleteItem={deleteItem} updateItem={updateItem} fetchItems={fetchItems} /> ))} </TodoWrapper> <form onSubmit={async(e) => { e.preventDefault(); await createItem(newItem); //Clean up new item setNewItem(''); await fetchItems(); }} > <FlexWrapper> <Input value={newItem} onChange={({ target }) => setNewItem(target.value)} placeholder="Add new item…" /> <Button success type="submit"> Add + </Button> </FlexWrapper> </form> </StyledHome> ); …
我們的服務器和前端現在已經設置好了。 我們可以通過運行npx nx serve todo-api
為 API 應用程序提供服務,對於 Next.js 應用程序,我們運行 npx npx nx serve todo
。 單擊“繼續”按鈕,您將看到一個顯示默認待辦事項的頁面。
現在,我們在一個工作區中有一個可以工作的 Next.js 和 Express 應用程序。
Nx 有另一個 CLI 工具,它允許我們在終端運行中查看應用程序的依賴關係圖。 運行npx nx dep-graph
,我們應該看到一個類似於下圖的屏幕,描繪了我們應用程序的依賴關係圖。
Nx 的其他 CLI 命令
nx list
列出當前安裝的 Nx 插件。-
nx migrate latest
將package.json
中的包更新到最新版本。 -
nx affected
僅對受影響或修改的應用程序執行操作。 -
nx run-many --target serve --projects todo-api,todo
在列出的所有項目中運行目標命令。
結論
作為 Nx 的總體概述,本文介紹了 Nx 提供的功能以及它如何使我們的工作更輕鬆。 我們還介紹了在 Nx 工作區中設置 Next.js 應用程序,將 Express 插件添加到現有工作區,並使用 monorepo 功能在我們的工作區中容納多個應用程序。
您將在 GitHub 存儲庫中找到完整的源代碼。 有關 Nx 的更多信息,請查看 Next.js 的文檔或 Nx 文檔。