使用 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 文档。