在不断发展的Web开发环境中,效率和模块化已变得至关重要。这是Nx和Qwik发挥作用的地方。
Qwik是一个现代的网络框架,通过减少需要运送到浏览器的JavaScript的数量来重点关注应用程序性能。您可以了解有关Qwik如何通过ROSMUMASIOBY here实现这一目标的更多信息。
nx是一种强大的工具,可帮助您构建可扩展和可维护的代码库,随着应用程序和团队的增长,扩展的代码库。 NX利用计算缓存和工作空间分析来确保最大的效率和开发人员的经验。您可以了解有关NX here的更多信息。
在这篇博客文章中,我们将探讨如何结合NX和Qwik的优势以创建一个TODO应用程序。为此,我们将利用Qwikifiers团队创建的NX插件来最大化Qwik和NX之间的集成,称为koude0。
您不一定需要为qwik使用NX插件。相反,您可以使用Qwik CLI创建应用程序并添加Nx later。
此博客文章已决定使用theqwik-nx
插件来利用插件提供的发电机提供的更好的DX。
您可以在下面的视频中了解有关此集成的更多信息:
创建工作区
让我们从设置开发环境开始。我们将创建一个NX工作区,并将QWIK集成到其中。首先生成一个空的集成工作区:
npx create-nx-workspace@latest qwik-todo-app
_您还可以使用运行
npx -y create-qwik-nx
或npx -y create-nx-workspace@latest --preset=qwik-nx
创建的qwik-nx
插件的preset
。这将通过安装适当的依赖项并生成Qwik应用程序来跳过下一步的几个步骤。
create-qwik-nx
软件包是使用NX创建安装软件包的示例。您可以在这里了解更多:https://nx.dev/extending-nx/recipes/create-install-package_
接下来,导航到工作区并安装qwik-nx
插件。
npm install --save-dev qwik-nx
您可以查看一个兼容矩阵,用于哪个版本的
qwik-nx
适用于每个版本的nx
here。
生成应用
使用NX插件的好处之一是,它具有其他功能,例如自动迁移,执行者在代码上采取行动以及发电机对脚手架代码(例如CodeMods)。
现在,让我们使用qwik-nx
提供的应用程序生成器来踩todo应用程序:
nx g qwik-nx:app todo
这将生成Qwik本身在您的NX工作区中提供的入门项目。它还将安装所有必要的软件包来构建QWIK应用程序。
在这一点
生成新路线
Qwik拥有另一个称为Qwik City的软件包,该软件包使用基于目录的路由来处理应用程序中的导航。您可以与Qwik City here了解有关基于目录的路由的更多信息。
qwik-nx
插件可以帮助在我们的应用程序中生成新路由。让我们使用它来生成一条可以存储托多逻辑的路线。
nx g qwik-nx:route --name=todo --project=todo
运行此命令后,您将看到一个新的目录和工作空间中创建的文件:
新创建的文件应该看起来像这样
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <div>This is the todo</div>;
});
您可以看到,这很简单,只是标准的Qwik组件。
如果您运行nx serve todo
并导航到http://localhost:4200/todo
,则可以看到该路由可以正常工作,并且该组件可以正确地呈现内容。
构建基本的UI
我们想构建一个todo应用程序,所以让我们添加一些UI元素以使其看起来更像是一个实际的TODO应用程序。
更新apps/todo/src/routes/todo/index.tsx
以匹配以下内容:
import {component$} from '@builder.io/qwik';
import {Form} from "@builder.io/qwik-city";
export default component$(() => {
return <div>
<h1>Todos</h1>
<div>
<label>
<input type="checkbox"/> {"My First Todo"}
</label>
</div>
<Form>
<input type="hidden" name="id" value={1}/>
<input type="text" name="message"/>
<button type="submit">Add</button>
</Form>
</div>;
});
您会看到页面更新,看起来像以下
很棒!
但是,您会注意到,当您单击Add
时,什么也不会发生!让我们添加一些逻辑以存储新的todos。
生成库
nx通过创建专注于特定功能的工作区库来帮助您以模块化方式组织工作区。
您没有将功能组织到应用程序的子文件夹中,而是将其提取到工作区库中(不打算发布的库,但仍由其他库和应用程序中的应用程序使用)。这有助于在应用程序中的模块和功能之间创建更强的边界,因为库具有公共API(index.ts
文件),从而使您可以准确地控制消费者可以访问的内容。
您可以了解有关定义和确保项目边界here的更多信息。
另外,通过这样做,您开始为工作区和应用程序构建一个项目图。以这种方式定义您的体系结构也有助于减少应用程序中每个变化影响的领域。
您可以了解有关项目图here的更多信息。
使用NX的这一功能,我们可以将todo应用程序的状态管理组织到自己的库中,将逻辑与应用程序本身分开。
让我们在qwik-nx
的帮助下生成一个新库。
nx g qwik-nx:lib data-access
我们不需要自动生成的某些文件,因此我们可以删除它们:
libs/data-access/src/lib/data-access.tsx
libs/data-access/src/lib/data-access.css
libs/data-access/src/lib/data-access.spec.tsx
添加QWIK上下文
Qwik使用Contexts来帮助在服务器端和客户端以及应用程序中的路线上存储状态。
我们将使用上下文存储在应用程序中,但首先,让我们创建一个文件来存储我们在应用程序中使用的TS接口。
创建libs/data-access/src/lib/api.ts
并添加以下内容:
export interface Todo {
id: number;
message: string;
}
接下来,让我们创建一个新文件libs/data-access/src/lib/todo.context.tsx
并添加以下内容:
import {
component$,
createContextId,
Slot,
useContextProvider,
useStore,
} from '@builder.io/qwik';
import { Todo } from './api';
interface TodoStore {
todos: Todo[];
lastId: number;
}
export const TodoContext = createContextId<TodoStore>('todo.context');
export const TodoContextProvider = component$(() => {
const todoStore = useStore<TodoStore>({
todos: [],
lastId: 0,
});
useContextProvider(TodoContext, todoStore);
return <Slot />;
});
这将创建我们的上下文,并在我们的应用程序中设置一个Store来存储Todos。 Qwik利用信号更新状态并告知在状态更改时需要重新渲染哪些组件的框架。
您可以了解更多有关Qwik使用信号here 。
的更多信息。
最后,让我们更新到库的公共入口点以公开我们的上下文和接口。
使用上下文
让我们更新根页以添加我们的上下文提供商。打开apps/todo/src/root.tsx
并在组件树中添加TodoContextProvider
之后的TodoContextProvider
。您的文件应如下:
import { component$, useStyles$ } from '@builder.io/qwik';
import {
QwikCityProvider,
RouterOutlet,
ServiceWorkerRegister,
} from '@builder.io/qwik-city';
import { RouterHead } from './components/router-head/router-head';
import globalStyles from './global.css?inline';
import { TodoContextProvider } from '@qwik-todo-app/data-access';
export default component$(() => {
/**
* The root of a QwikCity site always start with the <QwikCityProvider> component,
* immediately followed by the document's <head> and <body>.
*
* Don't remove the `<head>` and `<body>` elements.
*/
useStyles$(globalStyles);
return (
<QwikCityProvider>
<TodoContextProvider>
<head>
<meta charSet="utf-8" />
<link rel="manifest" href="/manifest.json" />
<RouterHead />
</head>
<body lang="en">
<RouterOutlet />
<ServiceWorkerRegister />
</body>
</TodoContextProvider>
</QwikCityProvider>
);
});
更新libs/data-access/src/index.ts
以匹配以下内容:
export * from './lib/todo.context';
export * from './lib/api';
现在我们的上下文已经到位,让我们在我们的todo
路线中使用它来管理我们的毒品。
更新apps/todo/src/routes/todo/index.tsx
以匹配以下内容:
import {component$} from '@builder.io/qwik';
import {Form} from "@builder.io/qwik-city";
import {TodoContext} from "@qwik-todo-app/data-access";
export default component$(() => {
const todoStore = useContext(TodoContext);
return <div>
<h1>Todos</h1>
{todoStore.todos.map((t) => (
<div key={`todo-${t.id}`}>
<label>
<input type="checkbox" /> {t.message}
</label>
</div>
))}
<Form>
<input type="hidden" name="id" value={1}/>
<input type="text" name="message"/>
<button type="submit">Add</button>
</Form>
</div>;
});
应用程序启动时,我们的商店中没有招待,因此,如果您提供应用程序,则将不再看到列出的任何招待。让我们修复!
添加routeLoader$
来加载导航的数据
QWIK允许您在浏览路由时获取数据,从而允许您在渲染页面之前获取数据。在将组件渲染并下载到客户端之前,将在服务器上获取数据。
您可以了解有关
的更多信息routeLoader$
here
它通过提供称为routeLoader$
的函数来做到这一点。我们将使用此功能来预加载我们的商店,并在数据库中理论上存在一些招待员。
对于此博客文章,我们将创建一个内存数据库来存储一些初始的戒酒。
我们将从更新我们的libs/data-access/src/lib/api.ts
以添加我们的内存DB。
export interface Todo {
id: number;
message: string;
}
interface DB {
store: Record<string, any[]>;
get: (storeName: string) => any[];
set: (storeName: string, value: any[]) => boolean;
add: (storeName: string, value: any) => boolean;
}
export const db: DB = {
store: { todos: [] },
get(storeName) {
return db.store[storeName];
},
set(storeName, value) {
try {
db.store[storeName] = value;
return true;
} catch (e) {
return false;
}
},
add(storeName, value) {
try {
db.store[storeName].push(value);
return true;
} catch (e) {
return false;
}
},
};
现在我们有了这个,让我们在/todo
路线中使用它来加载一些数据,当用户导航到/todo
。
更新apps/todo/src/routes/todo/index.tsx
以匹配以下内容:
import {component$} from '@builder.io/qwik';
import {Form, routeLoader$} from "@builder.io/qwik-city";
import {TodoContext, db} from "@qwik-todo-app/data-access";
export const useGetTodos = routeLoader$(() => {
// A network request or db connection could be made here to fetch persisted todos
// For illustrative purposes, we're going to seed a rudimentary in-memory DB if it hasn't been already
// Then return the value from it
if (db.get('todos')?.length === 0) {
db.set('todos', [
{
id: 1,
message: 'First todo',
},
]);
}
const todos: Todo[] = db.get('todos');
const lastId = [...todos].sort((a, b) => b.id - a.id)[0].id;
return { todos, lastId };
});
export default component$(() => {
const todoStore = useContext(TodoContext);
const persistedTodos = useGetTodos();
useTask$(({ track }) => {
track(() => persistedTodos.value);
if (persistedTodos.value) {
todoStore.todos = persistedTodos.value.todos;
todoStore.lastId =
todoStore.lastId > persistedTodos.value.lastId
? todoStore.lastId
: persistedTodos.value.lastId;
}
});
return <div>
<h1>Todos</h1>
{todoStore.todos.map((t) => (
<div key={`todo-${t.id}`}>
<label>
<input type="checkbox" /> {t.message}
</label>
</div>
))}
<Form>
<input type="hidden" name="id" value={1}/>
<input type="text" name="message"/>
<button type="submit">Add</button>
</Form>
</div>;
});
服务应用程序时,您会看到第一个待办事项被正确获取并呈现!
处理表单动作以添加戒酒
QWIK还允许您使用routeAction$
API在服务器上处理形式操作。让我们创建逻辑以在商店中添加新的Todos。
您可以了解有关
routeAction$
here的更多信息。
更新apps/todo/src/routes/todo/index.tsx
import {component$} from '@builder.io/qwik';
import {Form, routeLoader$} from "@builder.io/qwik-city";
import {TodoContext, db} from "@qwik-todo-app/data-access";
export const useGetTodos = routeLoader$(() => {
// A network request or db connection could be made here to fetch persisted todos
// For illustrative purposes, we're going to seed a rudimentary in-memory DB if it hasn't been already
// Then return the value from it
if (db.get('todos')?.length === 0) {
db.set('todos', [
{
id: 1,
message: 'First todo',
},
]);
}
const todos: Todo[] = db.get('todos');
const lastId = [...todos].sort((a, b) => b.id - a.id)[0].id;
return { todos, lastId };
});
export const useAddTodo = routeAction$(
(todo: {id: string, message: string}) => {
const success = db.add('todos', {
id: parseInt(todo.id),
message: todo.message,
});
return { success };
},
zod$({ id: z.string(), message: z.string() })
);
export default component$(() => {
const todoStore = useContext(TodoContext);
const persistedTodos = useGetTodos();
const addTodoAction = useAddTodo();
useTask$(({ track }) => {
track(() => persistedTodos.value);
if (persistedTodos.value) {
todoStore.todos = persistedTodos.value.todos;
todoStore.lastId =
todoStore.lastId > persistedTodos.value.lastId
? todoStore.lastId
: persistedTodos.value.lastId;
}
});
return <div>
<h1>Todos</h1>
{todoStore.todos.map((t) => (
<div key={`todo-${t.id}`}>
<label>
<input type="checkbox" /> {t.message}
</label>
</div>
))}
<Form action={addTodoAction}>
<input type="hidden" name="id" value={todoStore.lastId + 1} />
<input type="text" name="message"/>
<button type="submit">Add</button>
</Form>
{addTodoAction.value?.success && <p>Todo added!</p>}
</div>;
});
真棒!现在,我们可以在我们的应用程序中添加Todos!
但是,您可能已经注意到我们的文件开始变得很长。不仅是路由文件本身中有很多逻辑。让我们使用nx将逻辑分为我们之前创建的库,以使逻辑相处融合。
改善体系结构
要分开逻辑,创建一个新的文件libs/data-access/src/lib/todos.ts
,然后移动逻辑以加载和添加todos到自己的功能中:
import { db, Todo } from './api';
export function getTodos() {
// A network request or db connection could be made here to fetch persisted todos
// For illustrative purposes, we're going to seed a rudimentary in-memory DB if it hasn't been already
// Then return the value from it
if (db.get('todos')?.length === 0) {
db.set('todos', [
{
id: 1,
message: 'First todo',
},
]);
}
const todos: Todo[] = db.get('todos');
const lastId = [...todos].sort((a, b) => b.id - a.id)[0].id;
return { todos, lastId };
}
export function addTodo(todo: { id: string; message: string }) {
const success = db.add('todos', {
id: parseInt(todo.id),
message: todo.message,
});
return { success };
}
接下来,更新libs/data-access/src/index.ts
export * from './lib/todo.context';
export * from './lib/api';
export * from './lib/todo';
最后,让我们更新apps/todo/src/routes/todo/index.tsx
以使用我们新创建的功能:
import { component$, useContext, useTask$ } from '@builder.io/qwik';
import {
Form,
routeAction$,
routeLoader$,
z,
zod$,
} from '@builder.io/qwik-city';
import { addTodo, getTodos, TodoContext } from '@acme/data-access';
export const useGetTodos = routeLoader$(() => getTodos());
export const useAddTodo = routeAction$(
(todo) => addTodo(todo),
zod$({ id: z.string(), message: z.string() })
);
export default component$(() => {
const todoStore = useContext(TodoContext);
const persistedTodos = useGetTodos();
const addTodoAction = useAddTodo();
useTask$(({ track }) => {
track(() => persistedTodos.value);
if (persistedTodos.value) {
todoStore.todos = persistedTodos.value.todos;
todoStore.lastId =
todoStore.lastId > persistedTodos.value.lastId
? todoStore.lastId
: persistedTodos.value.lastId;
}
});
return (
<div>
<h1>Todos</h1>
{todoStore.todos.map((t) => (
<div key={`todo-${t.id}`}>
<label>
<input type="checkbox" /> {t.message}
</label>
</div>
))}
<Form action={addTodoAction}>
<input type="hidden" name="id" value={todoStore.lastId + 1} />
<input type="text" name="message" />
<button type="submit">Add</button>
</Form>
{addTodoAction.value?.success && <p>Todo added!</p>}
</div>
);
});
如果您再次运行nx serve todo
,您会注意到我们的重构不会为用户更改任何内容,但它使代码库更易于管理!
现在,如果我们需要更新加载或添加托多斯的逻辑,我们只需要重新测试库,而不是完整的应用程序,以改进我们的CI时间!
结论
NX和Qwik之间的合作使我们创建了一个托DO应用,以展示有效的开发实践和模块化设计。通过集中路由逻辑在库中,我们不仅展示了NX和QWIK的功能,而且还强调了这种方法如何显着改善缓存和CI时间。
这次穿越Qwik和NX的旅程展示了周到的体系结构和正确的工具如何显着增强您的开发体验。因此,继续,Qwikify您的开发并轻松构建出色的Web应用程序!
进一步阅读
了解更多
- ð§ Nx Docs
- ð©âð» Nx GitHub
- ð¬ Nx Community Slack
- ð¹ Nx Youtube Channel
- ð Speed up your CI.