East's Blog

Remix 入门

感谢 Randy Lu 的 Remix 入门实战教程,非常 Nice!

围绕一个博客系统,对 Remix 快速入门。最终实现的功能包括:

  • 查看文章列表
  • 发布、编辑、删除文章
  • 用户登录、注册、编辑文章鉴权

新建 Remix 项目

初始化 Remix 项目

# pnpm
pnpm create remix@latest remix-blog

# npm
npm create-remix@latest remix-blog

创建目录后,进入 remix-blog, 安装依赖:

pnpm i
# 或者
npm i

Remix 项目结构一览

- app
  - routes
    - _index.tsx
  - entry.client.tsx
  - entry.server.tsx
  - root.tsx
- public

在 Remix 项目中,所有的业务代码主要在 app/ 目录中,public/ 用于存放外部直接可以访问的静态文件,比如 favicon.ico。

  • app/routes 所有的页面、路由都在这个目录
  • entry.client.tsx 和 entry.server.tsx 在日常开发一般不会动它们,是一些 Remix 的固定逻辑。
  • root.tsx 整个 web 的入口,每个页面都会继承这个入口作为 layout。

Remix 的路由设计

首先,启动项目

npm run dev

首页对应的就是 app/routes/_index.tsx 所写的 React 页面。

为什么要以下划线(_)开头呢?因为在 Remix 中,页面路由会以文件名自动生成。所以,如果你写了一个 app/routes/index.tsx,它会指向 localhost:3000/index 而不是 localhost:3000/

在一个复杂的 web app 里,会有很多嵌套的路由,比如:

  • /posts/:postId
  • /tags
  • /tags/:tagId

那么在 Remix 中,可以写成:

- app
  - routes
    - _index.tsx
    - posts.$postId.tsx
    - tags._index.tsx
    - tags.$tagId.tsx
  - root.tsx
url文件名继承 layout
/app/routes/_index.tsxapp/root.tsx
/posts/xxxapp/routes/posts.$postId.tsxapp/root.tsx
/tagsapp/routes/tags._index.tsxapp/root.tsx
/tags/xxxapp/routes/tags.$tagId.tsxapp/root.tsx

如果希望 /tags 和 /tags/:tagId 使用同一个 layout,就要再添加一个 app/routes/tags.tsx 文件作为父级 layout

这时,继承的对应关系变为:

url文件名继承 layout
/app/routes/_index.tsxapp/root.tsx
/posts/xxxapp/routes/posts.$postId.tsxapp/root.tsx
/tagsapp/routes/tags._index.tsxapp/routes/tags.tsx
/tags/xxxapp/routes/tags.$tagId.tsxapp/routes/tags.tsx

而在作为 layout 的组件中,我们可以直接用 Remix 的 组件把子路由引入进来

import { Outlet } from "@remix-run/react";

export default function TagLayout() {
  return (
    <div>
      <h1>Tag Layout</h1>
      <div>
        <Outlet />
      </div>
    </div>
  )
}

So,为什么需要写成 _index 的形式,因为不带 _index 的页面,都会作为 layout。

这样的情况下,tags.tsx 是作为 layout 存在,而 tags._index.tsx 才是访问 /tags 时的页面。

加入 Tailwind CSS

在写业务代码之前,我们还需要把样式引入进来,这个入门项目使用 TailwindCSS + NextUI 的方案。

Remix 脚手架目前已经自带 Tailwind CSS,如果 package.json 中已经有 tailwindcss postcss autoprefixer 这些依赖了,就可以跳过这个部分。

在项目根目录中,安装Tailwind CSS 必要的依赖:

pnpm i tailwindcss postcss autoprefixer

然后用 tailwindcss 的 CLI 初始化 tailwindcss:

pnpm exec tailwindcss init -p
#npm
npm tailwindcss init -p

然后在创建一个 app/main.css 的 css 文件, 引入 Tailwind CSS 的必要样式:

@tailwind base;
@tailwind components;
@tailwind utilities;

下一步,在 app/root.tsx 中引入 main.css:

import "./main.css";

到这里,Tailwind CSS 就已经可以在你的 Remix app 中使用了。

加入 NextUI

初始化 tailwindcss 后,安装 NextUI 必要的依赖

pnpm i @nextui-org/react framer-motion

然后在 tailwind.config.js 中引入 NextUI:

import { nextui } from "@nextui-org/react";

export default {
  content: [
    "./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}",
    "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
  ],
  plugins: [nextui()],
}

然后在 root.tsx 中加入 ,注意要嵌套

import { NextUIProvider } from "@nextui-org/react";

export default function App() {
  return (
    <NextUIProvider>
      <Outlet />
    </NextUIProvider>
  );
}

无论你用的是什么 UI 库,一般来说你都应该在这个地方套用 UI 库提供的 Provider.

另外,如果你用的是 pnpm,则需要以下步骤:

在根目录创建 .npmrc 文件:

public-hoist-pattern[]=*@nextui-org/*

重新安装依赖:

pnpm i

到这里,NextUI 也安装完毕了。

数据库设计

对于这个博客系统来说,数据库模型不复杂。首先整理出要做的功能:

  • post 列表
  • 创建、修改、删除 Post

根据这个需求,只需要一张数据表就可以了。这里会使用 Prisma 作为 ORM 工具对数据库进行读写。Prisma 的 Schema 易懂易写,对 TypeScript 的支持也非常好,在写代码时有充分的类型提示。

加入 Prisma

安装 Prisma:

pnpm i prisma @prisma/client

在项目中初始化 prisma:

pnpm exec prisma init
# 或者 npm
npm prisma init

运行命令后,会自动创建一个 prisma/ 目录,里面有 prisma/schema.prisma 文件,就是我们需要编写的数据库模型文件。

把数据库模型写成 schema

打开 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 = "postgresql"
  url      = env("DATABASE_URL")
}

这是初始化 prisma 时的默认模板,为了方便,我们不使用 postgresql,而是使用本地的数据库 sqlite,所以我们要把 provider 改成 sqlite 以及定义数据库文件的地址:

datasource db {
  provider = "sqlite"
  url      = "file:./data.db"
}

接下来,开始编写数据库的模型。现在只需要一张 Post 表来存储博客文章,每个 post 有以下属性:

  • id 文章的唯一 ID
  • title 文章标题
  • content 文章内容
  • created_at 文章创建时间

那么在 prisma 的 schema 中,可以写成:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./data.db"
}

model Post {
  id         String   @id
  title      String
  content    String
  created_at DateTime @default(now())
}
  • @id 用于指定它作为索引值
  • @default 用于指定它的默认值,这里为 now(), 那么在创建数据的时候会以当前时间为默认值

写好 schema 后,需要运行:

pnpm exec prisma db push
# 或 npm
npx prisma db push

这个命令会使 prisma 根据 schema 创建或更新现有的数据库,并且生成对应的 TypeScript 类型。这时你会看到 prisma/data.db sqlite 数据库文件已经被创建了。

在 Remix 项目中引入 Prisma

数据库的准备工作已经完成,现在就来准备在 Node.js 中使用它了。创建一个 app/prisma.server.ts 文件:

import { PrismaClient } from "@prisma/client"

declare global {
  var __prisma: PrismaClient
}

if (!global.__prisma) {
  global.__prisma = new PrismaClient()
}

global.__prisma.$connect()
export const prisma = global.__prisma