SolidStart:与MongoDB集成
#typescript #mongodb #solidjs #solidstart

介绍

SolidStart是基于SolidJ和实心路由器的元式框架(例如for vuejs的react或nuxt)。

它仍在Beta中,但我们可以创建出色的应用程序。
在本文中,我们将看到如何将MongoDB数据库与SolidStart集成。

为了说明该方法,我们将创建一个基本应用程序:“购物清单” WebApp。

Image description

我们假设您在端口27017上可以访问一个运行的mongoDB实例。

准备

在整合MongoDB之前,我们需要准备我们的应用程序。

生成应用程序

首先,我们生成一个solidstart应用程序。我们使用recommended starter

mkdir shopping-list && cd shopping-list
npm init solid@latest

并选择bare模板:

? Which template do you want to use?
>    bare
     hackernews
     todomvc
     with-auth
     with-authjs
     with-mdx
     with-prisma
     with-solid-styled
     with-tailwindcss
     with-vitest

在模板生成器的其余部分中,我们选择了Server Side RenderingTypeScript的用法。

最后,我们安装了NPM依赖项并运行DEV脚本:

npm install
npm run dev -- --open

在此步骤中,我们的应用程序已准备好开发!

在添加MongoDB集成之前,我们通过做一些更改来准备我们的应用程序。

模型

我们通过使用以下内容创建文件src/models/shopping.ts来创建ShoppingListItem打字字体类型:

src/models/shopp.ts

export type ShoppingListItem = {
  _id: string;
  label: string;
};

export type NewShoppingListItem = Omit<ShoppingListItem, "_id">;

风格

我们在src/root.css的末尾添加了一些CS。

src/root.css

ul {
  display: flex;
  flex-direction: column;
  align-items: baseline;
}

li {
  margin-top: 10px;
}

li > button {
  margin-left: 10px;
}

成分

为了帮助我们在应用程序中,我们创建了一个ListInput组件,使我们可以键入一个新项目。我们创建src/components/ListInput.tsx

src/components/listInput.tsx

import { createSignal } from "solid-js";

interface ListInputProps {
  onValidate: (value: string) => void;
}

export default function ListInput(props: ListInputProps) {
  const [value, setValue] = createSignal("");

  const onValidateValue = () => {
    props.onValidate(value());
    setValue("");
  };

  return (
    <div>
      <input
        value={value()}
        onInput={(evt) => setValue(evt.currentTarget.value)}
        type="text"
        placeholder="New item"
        onKeyPress={(evt) => {
          if (evt.key === "Enter") {
            evt.preventDefault();
            onValidateValue();
          }
        }}
      />

      <button type="button" onClick={onValidateValue}>
        Add
      </button>
    </div>
  );
}

卸下导航标头

不需要来自模板的默认导航标头,因此我们将其删除。
为此,您只需要删除src/root.tsx文件中的<A />标签。

index.tsx页面

在此步骤中,我们使用createSignal处理购物清单项目。没有持久性,但是逻辑有效。

我们用以下内容替换src/routes/index.tsx

src/routes/index.tsx

import { createSignal, For } from "solid-js";
import ListInput from "~/components/ListInput";
import { ShoppingListItem } from "~/models/shopping";

export default function Home() {
  const [items, setItems] = createSignal<ShoppingListItem[]>([]);

  const addNewItem = (label: string) => {
    setItems((prev) => [...prev, { label, _id: Date.now().toString() }]);
  };

  const deleteItem = (itemId: string) => {
    setItems((prev) => prev.filter((item) => item._id !== itemId));
  };

  return (
    <main>
      <ul>
        <For each={items()}>
          {(item) => (
            <li>
              <span>{item.label}</span>
              <button type="button" onClick={() => deleteItem(item._id)}>
                Delete
              </button>
            </li>
          )}
        </For>
      </ul>
      <ListInput onValidate={addNewItem} />
    </main>
  );
}

添加此点,该应用程序无效。

MongoDB的整合

添加mongodb

首先,我们安装了mongodb@types/mongodb npm依赖项:

npm install --save mongodb
npm install --save-dev @types/mongodb

然后,我们创建一个类来管理与数据库的连接。我们使用以下内容创建src/lib/database.ts

src/lib/database.ts

import { Db, MongoClient } from "mongodb";

const MONGODB_HOST = "localhost";
const MONGODB_PORT = 27017;

export class Database {
  private db?: Db;
  private static instance: Database;

  private constructor() {}

  async init() {
    const client = new MongoClient(`mongodb://${MONGODB_HOST}:${MONGODB_PORT}`);
    await client.connect();
    this.db = client.db("shopping-list");
  }

  public static getInstance() {
    if (!Database.instance) {
      Database.instance = new Database();
    }

    return Database.instance;
  }

  getDb() {
    if (!this.db) {
      throw new Error("DB is not init");
    }

    return this.db;
  }
}

初始数据库连接

启动应用程序时,我们需要启动数据库连接。
这是使用Database类的init()方法完成的。 Singleton模式的使用有助于我们。

我们编辑src/entry-server.tsx文件以在createHandler函数之前添加数据库初始化:

src/entry-server.tsx

import {
  createHandler,
  renderAsync,
  StartServer,
} from "solid-start/entry-server";
import { Database } from "./lib/database";

// Initialization of the database (connection + retrieve db instance)
const database = Database.getInstance();
database.init();

export default createHandler(
  renderAsync((event) => <StartServer event={event} />)
);

创建一个与数据库互动的ShoppingService

我们创建一个ShoppingServiceDatabase类交互,并添加 /列表 /删除购物列表项目。< / p>

您可以使用以下内容创建文件src/lib/shopping-service.ts

src/lib/shopping-service.ts

import { Db, ObjectId } from "mongodb";
import { NewShoppingListItem, ShoppingListItem } from "../models/shopping";
import { Database } from "./database";

export class ShoppingService {
  db: Db;
  collectionName = "items";
  private static instance: ShoppingService;

  private constructor() {
    this.db = Database.getInstance().getDb();
  }

  public static getInstance() {
    if (!ShoppingService.instance) {
      ShoppingService.instance = new ShoppingService();
    }

    return ShoppingService.instance;
  }

  async getAllItems(): Promise<ShoppingListItem[]> {
    const itemsFromDb = await this.db
      .collection(this.collectionName)
      .find()
      .toArray();
    return itemsFromDb.map((elt) => ({
      ...elt,
      _id: elt._id.toString(), // Get _id as String
    })) as ShoppingListItem[];
  }

  async addNewItem(item: NewShoppingListItem) {
    await this.db.collection(this.collectionName).insertOne(item);
  }

  async deleteItem(itemId: string) {
    await this.db
      .collection(this.collectionName)
      .deleteOne({ _id: new ObjectId(itemId) });
  }
}

与头版集成

在这一点上,我们需要在ShoppingService和我们的index.tsx页面之间添加交互。这将以2个步骤完成:

  1. 创建routeData来检索我们的物品并通过useRouteData使用它们
  2. 创建服务器端操作以使用createServerAction$添加和删除项目

使用routeData检索我们的数据

要在我们的页面中检索数据,SolidStart使用routeData。我们创建一个名为routeData的函数,并使用createServerData$在服务器端执行操作并获取我们的项目。

在我们的组件中,我们具有useRouteData功能来检索我们的项目。

要识别我们的请求(并在更新数据时能够使其无效),我们在src/constants.ts文件中创建一个常数。

src/startants.ts

export const SHOPPING_ITEMS_REQ_KEY = "shoppingItems";

然后,我们在src/routes/index.tsx文件中创建函数routeData

src/routes/index.tsx

import { createServerData$ } from "solid-start/server";
import { ShoppingService } from "~/lib/shopping-service";
import { ShoppingListItem } from "~/models/shopping";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants";

export function routeData() {
  return createServerData$(
    async () => {
      const shoppingService = ShoppingService.getInstance();
      const data = await shoppingService.getAllItems();
      return data as ShoppingListItem[];
    },
    { key: SHOPPING_ITEMS_REQ_KEY }
  );
}

export default function Home() {
  // Content of our page
}

routeData中,我们使用ShoppingService获取我们的物品并返回。
要使用我们检索的项目,我们只需要在我们的Home组件中使用useRouteData

src/routes/index.tsx

import { useRouteData } from "solid-start";

export default function Home() {
  const items = useRouteData<typeof routeData>();

  // Rest of our component
}

因为我们由于以前的createSignal逻辑而进行了重复,因此我们可以删除createSignal函数并编辑我们的addNewItemdeleteItem,如下所示:

src/routes/index.tsx

import { useRouteData } from "solid-start";

export default function Home() {
  const items = useRouteData<typeof routeData>();

  const addNewItem = (label: string) => {};
  const deleteItem = (itemId: string) => {};

  // Rest of our component
}

目前,我们的项目是从数据库中检索的! (但是我们在数据库中没有数据,现在我们将添加操作以添加 /删除项目来解决此问题)< / p>

添加服务器端操作

对于 add delete 操作,我们将创建一些将在服务器端执行的功能。查看routeData功能下的功能:

src/routes/index.tsx

import { ShoppingListItem, NewShoppingListItem } from "~/models/shopping";

export function routeData() {
  // routeData content
}

async function addNewItemAction(newItem: NewShoppingListItem) {
  const shoppingService = ShoppingService.getInstance();
  await shoppingService.addNewItem(newItem);
}

async function deleteItemAction(itemId: string) {
  const shoppingService = ShoppingService.getInstance();
  await shoppingService.deleteItem(itemId);
}

使用createServerAction$,我们的Home组件将调用这些功能,该功能使我们调用服务器端函数。它特别是为更新数据而设计的,因为此功能使我们可以指出要无效的数据(让SolidStart自动重新提取它们)。

我们在Home组件中添加我们的动作,在我们的useRouteData下:

src/routes/index.tsx

import { createServerAction$ } from "solid-start/server";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants.ts";

export default function Home() {
  const items = useRouteData<typeof routeData>();
  const [_adding, addNewItem] = createServerAction$(addNewItemAction, {
    invalidate: SHOPPING_ITEMS_REQ_KEY,
  });
  const [_deleting, deleteItem] = createServerAction$(deleteItemAction, {
    invalidate: SHOPPING_ITEMS_REQ_KEY,
  });

  // Rest of our component
}

我们还可以删除我们的空addNewItemdeleteItem函数。它们被我们的服务器操作所取代。有关使用我们的行动的更多详细信息,请参阅文档。

简而言

  1. 在第一个索引中:动作状态(待处理,错误等...)
  2. 在第二个索引中:将调用我们的服务器端操作的功能

在我们的情况下,我们没有使用元组的第一个索引。

在选项中,我们还指出了提取请求的数据无效(使用SHOPPING_ITEMS_REQ_KEY)。

最后,我们更新src/models/shopping.tsonValidate Prop6以匹配参数:

src/routes/index.tsx

<ListInput onValidate={(label: string) => addNewItem({ label })} />

那就是!

我们的组件看起来像这样:

src/routes/index.tsx

import { For } from "solid-js";
import { useRouteData } from "solid-start";
import { createServerAction$, createServerData$ } from "solid-start/server";
import ListInput from "~/components/ListInput";
import { ShoppingService } from "~/lib/shopping-service";
import { NewShoppingListItem, ShoppingListItem } from "~/models/shopping";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants.ts";

export function routeData() {
  return createServerData$(
    async () => {
      const shoppingService = ShoppingService.getInstance();
      const data = await shoppingService.getAllItems();
      return data as ShoppingListItem[];
    },
    { key: SHOPPING_ITEMS_REQ_KEY }
  );
}

async function addNewItemAction(newItem: NewShoppingListItem) {
  const shoppingService = ShoppingService.getInstance();
  await shoppingService.addNewItem(newItem);
}

async function deleteItemAction(itemId: string) {
  const shoppingService = ShoppingService.getInstance();
  await shoppingService.deleteItem(itemId);
}

export default function Home() {
  const items = useRouteData<typeof routeData>();
  const [_adding, addNewItem] = createServerAction$(addNewItemAction, {
    invalidate: SHOPPING_ITEMS_REQ_KEY,
  });
  const [_deleting, deleteItem] = createServerAction$(deleteItemAction, {
    invalidate: SHOPPING_ITEMS_REQ_KEY,
  });

  return (
    <main>
      <ul>
        <For each={items()}>
          {(item) => (
            <li>
              <span>{item.label}</span>
              <button type="button" onClick={() => deleteItem(item._id)}>
                Del
              </button>
            </li>
          )}
        </For>
      </ul>
      <ListInput onValidate={(label: string) => addNewItem({ label })} />
    </main>
  );
}

结论

现在您知道如何将MongoDB与SolidStart整合!

我们看到了如何:

  • 用启动器生成一个实体应用
  • 与IT一起使用Typescript
  • 创建MongoDB连接
  • 请从我们的组件向MongoDB做服务器端请求

随时发表评论以给我您的反馈或提供一些改进。

感谢您的阅读!