如何托管 Remix 应用程序?

Back4app 容器混音版封面

在本文中,您将了解开始使用 Remix 所需的一切知识。我们将介绍 Remix 的优缺点,演示如何构建 Remix 应用程序,最后介绍如何部署 Remix。

什么是 Remix?

Remix 是一个现代化的全栈网络框架,用于构建快速、流畅和弹性应用程序。它利用服务器端渲染(SSR)的强大功能,使动态网络应用程序的加载时间更快,搜索引擎优化效果更好。

有了 Remix,开发人员可以在统一的代码库中无缝集成客户端和服务器端逻辑。与其他流行的 React 框架相比,Remix 有很多不同之处。这包括

  1. 基于文件的路由选择路由目录中的文件代表特定路由)
  2. 传统表单处理(使用 HTML 和 HTTP 请求,类似于 PHP)
  3. 状态管理(状态只存储在服务器上)
  4. 数据加载(与组件脱钩)

Remix 创建于 2020 年,最初是按年收取许可费的。后来,在 2021 年 10 月,Remix 团队决定将该项目开源。现在,它可以在 MIT 许可下使用。

2022 年底,Remix 被 Shopify 以 21 亿美元的价格收购。

Remix 的优势

让我们来看看使用 Remix 框架的主要好处。

基于文件的导航

Remix 构建在强大的客户端路由解决方案React Router 之上。事实上,Remix 是由创建 React Router 的同一个开发团队创建的。

该框架采用基于文件的导航系统,简化了代码组织。它允许开发人员将路由、组件和资源与特定文件或目录关联起来。

这里有一个例子:

app/
└── routes/
    ├── $noteId.tsx              // matches: /<noteId>/
    ├── $noteId_.destroy.tsx     // matches: /<noteId>/destroy
    ├── $noteId_.edit.tsx        // matches: /<noteId>/edit
    ├── _index.tsx               // matches: /
    └── create.tsx               // matches: /create

最重要的是,它支持通过 .这将缩短加载时间,改善错误处理等!

服务器端渲染(SSR)

Remix 利用了服务器端渲染的强大功能。

在普通的 React 应用程序中,数据通常在客户端获取,然后注入 DOM。这就是所谓的客户端呈现(CSR)。

Remix 则采用了不同的方法。它首先在后台获取数据,用获取的数据渲染 HTML,然后将其提供给客户端。

服务器端渲染往往能带来更好的性能和更有利于搜索引擎优化的应用程序。

表格处理

Remix 在形式处理上返璞归真。

它不使用大量受控组件和 JavaScript,而是使用传统的 HTML 表单和 HTTP 请求。

提交表单后,表单会向特定路由发送 HTTP 请求,然后服务器端会使用action()函数对其进行处理。它的工作原理与传统的PHP 类似。

这意味着 Remix 在处理表单时完全不需要 JavaScript。这固然很好,但可能会让表单验证和错误显示变得更加棘手。

国家管理

在这里,状态指的是同步服务器和客户端数据。

Remix 让状态管理变得轻而易举。它无需使用 Redux、Zustand、React Query、Apollo 或任何其他客户端状态管理库。

使用 Remix 时,所有状态都由服务器处理。客户端几乎不保存任何状态,因此不需要同步过程。

从服务器到客户端的数据是通过各种函数传递的,如loader()action( )

过渡和乐观的用户界面

Remix 过渡效果通过提前加载数据和使用动画,使页面之间的移动流畅而快速。

乐观的用户界面能即时反映用户的操作,在确认更改之前就显示出来,让网站感觉更灵敏。

通过减少感知延迟和提供即时反馈,过渡和乐观的用户界面大大提升了用户体验。

Remix的局限性

Remix 虽然很棒,但也有一些局限性。

复杂

Remix 并不是最容易使用的框架,其文档(在撰写本文时)也不是最好的。

该框架在很多方面也与普通的 React 不同。如果您是 React 开发人员,可能需要一些时间来理解 Remix 的各种概念。

不那么受欢迎

Remix 仍是一个相对较新的框架。它的公开发布日期只能追溯到 2021 年 10 月。与一些竞争对手(如Next.js)相比,它还不够成熟,也没有经过实战检验。

通过查看 GitHub 的星级,我们可以发现 Remix(2.7 万颗星)远不如 Next.js(12 万颗星)受欢迎。在撰写本文时,除了Shopify 之外,Remix 还没有被任何科技巨头采用。

紧密耦合

Remix 应用程序的前台和后台紧密结合。虽然这种方法适用于较小的项目,但有些开发人员更喜欢独立前台和后台的灵活性。

此外,将前台与后台分离可以使代码更易于维护和测试。

无 SSG 或 ISR

Remix 不支持静态网站生成 (SSG) 或增量静态再生 (ISR)。

如何部署 Remix 应用程序?

在本节中,我们将构建并部署 Remix 应用程序。

先决条件

您需要

  • 基本了解TypeScript
  • 具有使用 Docker(和容器技术)的经验
  • 在您的计算机上安装Node.js和 JavaScript IDE
  • 在机器上安装Docker Desktop

项目概述

在本文中,我们将开发一个笔记网络应用程序。该网络应用程序将允许用户创建、检索、编辑和删除笔记。

对于后端,我们将使用Back4app BaaS,而对于前端,我们将使用Remix 框架。前端编码完成后,我们将把它部署到Back4app 容器上。

最终产品将是这样的

Back4app Remix Notes

我建议你先跟着笔记网络应用一起学习。看完这篇文章后,你应该就能部署自己的 Remix 应用程序了。

后台

在本节文章中,我们将使用 Back4app 构建应用程序的后台。

创建应用程序

首先登录您的Back4app 账户(如果还需要,也可以创建一个)。

登录后,您将被重定向到 “我的应用程序 “门户。要创建后台,首先需要创建一个 Back4app 应用程序。为此,请点击 “创建新应用程序”。

Back4app 应用程序控制面板

Back4app 提供两种解决方案:

  1. 后端即服务(BaaS)–成熟的后端解决方案
  2. 容器即服务(CaaS)–基于 Docker 的容器协调平台

由于我们使用的是后台,因此请选择 “后台即服务”。

Back4app BaaS 创建

为应用程序命名,其他一切保持默认,然后点击 “创建”。

Back4app 创建应用程序详细信息

平台需要一段时间来设置后台所需的一切。这包括数据库、应用界面、扩展、安全等。

应用程序准备就绪后,您将被重定向到应用程序的实时数据库视图。

Back4app 数据库视图

数据库

接下来,我们来处理数据库。

由于我们要创建的是一个相对简单的应用程序,所以只需要一个模型–就叫它 “Note "吧。要创建它,请单击屏幕左上方的 “创建类 “按钮。

将其命名为"Note“,启用 “已启用公共读写”,然后点击 “创建类并添加列”。

创建 Back4app 数据库类

然后,添加以下列:

+-----------+--------------+----------------+----------+
| Data type | Name         | Default value  | Required |
+-----------+--------------+----------------+----------+
| String    | emoji        | <leave blank>  | yes      |
+-----------+--------------+----------------+----------+
| String    | title        | <leave blank>  | yes      |
+-----------+--------------+----------------+----------+
| File      | content      | <leave blank>  | no       |
+-----------+--------------+----------------+----------+

创建类后,在数据库中填充示例数据。通过提供表情符号、标题和内容创建一些备注。或者,导入导出数据库

已填充 Back4app 数据库

很好,就是这样!

我们已经成功创建了一个后台,无需编写任何代码。

要了解有关后台即服务的更多信息,请参阅什么是后台即服务?

前端

在本节文章中,我们将使用 Remix 框架构建应用程序的前端。

创建-Remix

引导 Remix 项目的最简单方法是使用create-remix工具。该工具可创建一个生产就绪的 Remix 项目。

它会处理目录结构和依赖关系,设置捆绑程序等。

运行以下命令创建一个新的 Remix 项目:

$ npx create-remix@latest remix-notes

Initialize a new git repository? Yes
Install dependencies with npm?   Yes

如果您从未使用过create-remix,它会自动安装。

创建项目后,将活动目录更改为该项目:

$ cd remix-notes

最后,启动开发服务器:

$ npm run dev

打开您最喜欢的网络浏览器,导航至http://localhost:5173。屏幕上会弹出默认的 Remix 登陆页面。

TailwindCSS

为了让我们的生活更轻松一些,我们将使用TailwindCSS。TailwindCSS 是一个实用至上的框架,让你无需编写任何纯 CSS 就能快速创建前端。

首先,使用 npm 安装:

$ npm install -D tailwindcss postcss autoprefixer

接下来,运行tailwindcss init

$ npx tailwindcss init --ts -p

这将配置项目,并在项目根目录下创建tailwind.config.ts文件。

像这样修改其content属性,让 Tailwind 知道在哪些文件中将使用实用工具类:

// tailwind.config.ts

import type {Config} from "tailwindcss";

export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],  // new
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;

应用程序目录中新建一个名为tailwind.css的文件,内容如下:

/* app/tailwind.css */

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

最后,通过链接将其导入root.tsx

// app/root.tsx

// other imports
import {LinksFunction} from "@remix-run/node";
import stylesheet from "~/tailwind.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

// ...

就是这样!

我们已经成功安装了 TailwindCSS。

路线

我们的网络应用程序将有以下端点:

  1. /显示所有笔记
  2. /create允许用户创建注释
  3. /显示特定注释
  4. //delete允许用户删除特定笔记

要定义这些路由,请在应用程序文件夹中创建以下目录结构:

app/
└── routes/
    ├── $noteId.tsx
    ├── $noteId_.destroy.tsx
    ├── $noteId_.edit.tsx
    ├── _index.tsx
    └── create.tsx

正如你可能已经猜到的,前缀$用于动态参数,.代替/

意见

接下来,让我们来实现视图。

为了使我们的代码更加类型安全,我们将创建一个名为Note的接口,它与我们的Note数据库类相似。

创建名为store 的文件夹,并在其中创建包含以下内容的NoteModel.ts

// app/store/NoteModel.ts

export default interface NoteModel {
  objectId: string;
  emoji: string;
  title: string;
  content: string;
  createdAt: Date;
  updatedAt: Date;
}

然后粘贴_index.tsx$noteId.tsxcreate.tsx 的视图代码:

// app/routes/_index.tsx

import {Link, NavLink} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";

const notes = [
  {objectId: "1", emoji: "📝", title: "My First Note"},
  {objectId: "2", emoji: "📓", title: "My Second Note"},
  {objectId: "3", emoji: "📔", title: "My Third Note"},
] as NoteModel[];

export default function Index() {
  return (
        <>
      <Link to={`/create`}>
        <div className="bg-blue-500 hover:bg-blue-600 text-lg font-semibold text-white
         px-4 py-3 mb-2 border-2 border-blue-600 rounded-md"
        >
          + Create
        </div>
      </Link>
      {notes.map(note => (
        <NavLink key={note.objectId} to={`/${note.objectId}`}>
          <div className="hover:bg-slate-200 text-lg font-semibold
            px-4 py-3 mb-2 border-2 border-slate-300 rounded-md"
          >
            {note.emoji} {note.title}
          </div>
        </NavLink>
      ))}
    </>
  );
}
// app/routes/$noteId.tsx

import {Form} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";

const note = {
  objectId: "1", emoji: "📝", title: "My First Note", content: "Content here.",
  createdAt: new Date(), updatedAt: new Date(),
} as NoteModel;

export default function NoteDetails() {
  return (
    <>
      <div className="mb-4">
        <p className="text-6xl">{note.emoji}</p>
      </div>
      <div className="mb-4">
        <h2 className="font-semibold text-2xl">{note.title}</h2>
        <p>{note.content}</p>
      </div>
      <div className="space-x-2">
        <Form
          method="post" action="destroy"
          onSubmit={(event) => event.preventDefault()}
          className="inline-block"
        >
          <button
            type="submit"
            className="bg-red-500 hover:bg-red-600 font-semibold text-white
              p-2 border-2 border-red-600 rounded-md"
          >
            Delete
          </button>
        </Form>
      </div>
    </>
  );
}
// app/routes/create.tsx

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

export default function NoteCreate() {
  return (
    <>
      <div className="mb-4">
        <h2 className="font-semibold text-2xl">Create Note</h2>
      </div>
      <Form method="post" className="space-y-4">
        <div>
          <label htmlFor="emoji" className="block">Emoji</label>
          <input
            type="text" id="emoji" name="emoji"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <label htmlFor="title" className="block">Title</label>
          <input
            type="text" id="title" name="title"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <label htmlFor="content" className="block">Content</label>
          <textarea
            id="content" name="content"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <button
            type="submit"
            className="bg-blue-500 hover:bg-blue-600 font-semibold
              text-white p-2 border-2 border-blue-600 rounded-md"
          >
            Create
          </button>
        </div>
      </Form>
    </>
  );
}

这段代码没有任何花哨之处。我们所做的只是使用 JSX 和 TailwindCSS 来创建用户界面。

您可能已经注意到,所有组件都是不受控的(我们没有使用useState())。除此之外,我们还使用了一个真正的 HTML 表单。

这是因为 Remix 框架使用 HTTP 请求处理与 PHP 类似的表单,这与 React 不同。

Parse

有多种方法可以连接到基于 Back4app 的后台。您可以使用

  1. RESTful API
  2. GraphQL API
  3. Parse SDK

最简单、最强大的方法当然是Parse SDK。Parse SDK 是一个软件开发工具包,它提供了大量实用类和方法,可以轻松查询和处理数据。

首先通过npm 安装 Parse:

$ npm install -i parse @types/parse

像这样在项目根目录下创建一个.env文件:

# .env

PARSE_APPLICATION_ID=<your_parse_app_id>
PARSE_JAVASCRIPT_KEY=<your_parse_js_key>

确保更换 <your_parse_app_id><your_parse_javascript_key> 替换为实际凭据。要获取凭证,请导航到您的应用程序,选择侧边栏上的 “应用程序设置 > 服务器和安全”。

然后,像这样在root.tsx文件顶部初始化 Parse:

// app/root.tsx

// other imports
import Parse from "parse/node";

const PARSE_APPLICATION_ID = process.env.PARSE_APPLICATION_ID || "";
const PARSE_HOST_URL = "https://parseapi.back4app.com/";
const PARSE_JAVASCRIPT_KEY = process.env.PARSE_JAVASCRIPT_KEY || "";
Parse.initialize(PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY);
Parse.serverURL = PARSE_HOST_URL;

我们将创建一个名为api/backend.ts的单独文件,以保持视图与后台通信逻辑的一致性。

创建api/backend.ts,内容如下

// app/api/backend.ts

import Parse from "parse/node";
import NoteModel from "~/store/NoteModel";

export const serializeNote = (note: Parse.Object<Parse.Attributes>): NoteModel => {
  return {
    objectId: note.id,
    emoji: note.get("emoji"),
    title: note.get("title"),
    content: note.get("content"),
    createdAt: new Date(note.get("createdAt")),
    updatedAt: new Date(note.get("updatedAt")),
  };
}

export const getNotes = async (): Promise<NoteModel[]> => {
  // ...
}

export const getNote = async (objectId: string): Promise<NoteModel | null> => {
  // ...
}

// Grab the entire file from: 
// https://github.com/duplxey/back4app-containers-remix/blob/master/app/api/backend.ts

最后,修改视图以获取和操作后台数据:

// app/routes/index.tsx

import {json} from "@remix-run/node";
import {Link, NavLink, useLoaderData} from "@remix-run/react";
import {getNotes} from "~/api/backend";

export const loader = async () => {
  const notes = await getNotes();
  return json({notes});
};

export default function Index() {
  const {notes} = useLoaderData<typeof loader>();
  return (
    // ...
  );
}
// app/routes/$noteId.tsx

import {getNote} from "~/api/backend";
import {json, LoaderFunctionArgs} from "@remix-run/node";
import {Form, useLoaderData} from "@remix-run/react";
import {invariant} from "@remix-run/router/history";

export const loader = async ({params}: LoaderFunctionArgs) => {
  invariant(params.noteId, "Missing noteId param");
  const note = await getNote(params.noteId);
  if (note == null) throw new Response("Not Found", {status: 404});
  return json({note});
};

export default function NoteDetails() {
  const {note} = useLoaderData<typeof loader>();
  return (
    // ...
  );
}
// app/routes/create.tsx

import {ActionFunctionArgs, redirect} from "@remix-run/node";
import {Form} from "@remix-run/react";
import {createNote} from "~/api/backend";

export const action = async ({request}: ActionFunctionArgs) => {
  const formData = await request.formData();
  const {emoji, title, content} = Object.fromEntries(formData) 
                                    as Record<string, string>;
  const note = await createNote(emoji, title, content);
  return redirect(`/${note?.objectId}`);
};

export default function NoteCreate() {
  return (
    // ...
  );
}
// app/routes/$noteId_.destroy.tsx

import type {ActionFunctionArgs} from "@remix-run/node";
import {redirect} from "@remix-run/node";
import {invariant} from "@remix-run/router/history";
import {deleteNote} from "~/api/backend";

export const action = async ({params}: ActionFunctionArgs) => {
  invariant(params.noteId, "Missing noteId param");
  await deleteNote(params.noteId);
  return redirect(`/`);
};

代码概览

  • 我们使用 Remixloader()函数加载数据。加载数据后,我们通过json()函数将数据以 JSON 格式传递给视图。
  • 我们使用 Remixaction()函数来处理表单提交(如POST)。
  • noteId作为参数传递给了视图。

应用程序现在应该可以完全正常运行,并与 Back4app 后台同步。创建几个笔记,编辑它们,然后删除它们,确保一切正常。

Dockerize 应用程序

在本节中,我们将对 Remix 前端进行 docker 化。

Dockerfile

Dockerfile 是一个纯文本文件,概述了 Docker 引擎构建映像所需的步骤。

这些步骤包括设置工作目录、指定基本映像、传输文件、执行命令等。

这些指令通常以大写字母表示,并紧接着各自的参数。

要了解所有说明的更多信息,请查看Dockerfile 参考资料

在项目根目录下创建一个Dockerfile,内容如下

# Dockerfile

FROM node:20

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start"]

Dockerfile基于node:20映像。它首先设置了工作目录,复制了package.json,并安装了依赖项。

然后,它将构建项目,公开3000 端口,并为应用程序提供服务。

.dockerignore

在使用 Docker 时,您通常会努力构建尽可能小的映像。

由于我们的项目包含了一些不需要出现在镜像中的文件(例如.git、 构建、集成开发环境设置),所以我们要将它们排除在外。为此,我们将创建一个.dockerignore文件,其作用类似于.gitignore文件。

在项目根目录下创建.dockerignore文件:

# .dockerignore

.idea/
.cache/
build/
node_modules/

根据项目需要调整.dockerignore文件。

构建和测试

在将 Docker 映像推送到云端之前,最好先在本地进行测试。

首先,建立图像:

$ docker build -t remix-notes:1.0 .

接下来,使用新创建的映像创建一个容器:

$ docker run -it -p 3000:3000
    -e PARSE_APPLICATION_ID=<your_parse_app_id> 
    -e PARSE_JAVASCRIPT_KEY=<your_parse_javascript_key> 
    -d remix-notes:1.0

确保更换 <your_parse_app_id><your_parse_javascript_key> 替换为实际凭证。

现在,你可以在http://localhost:3000 上访问你的应用程序了。它的运行方式应与 docker 化之前的方式相同。

推送到 GitHub

要将应用程序部署到 Back4app Containers,必须先将源代码推送到 GitHub。为此,您可以按照以下步骤操作:

  1. 登录GitHub 账户(或注册)。
  2. 创建一个新的 GitHub 仓库
  3. 导航至本地项目并初始化:git init
  4. 将所有代码添加到版本控制系统:git add .
  5. 通过git remote addorigin 添加远程原点
  6. 通过git commit -m "initial commit"提交所有代码
  7. 将代码推送到 GitHubgit push origin master

部署应用程序

在最后一部分,我们将把前端部署到 Back4app 容器中。

登录您的 Back4app 账户,点击 “创建新应用”,启动应用创建流程。

Back4app 创建应用程序

由于我们现在部署的是容器化应用程序,因此请选择 “容器即服务”。

Back4app 容器即服务

接下来,您必须将 GitHub 账户与 Back4app 链接,并导入之前创建的仓库。连接后,选择软件仓库。

选择 Back4app 存储库

Back4app Containers 允许进行高级配置。不过,对于我们这个简单的应用程序来说,以下设置就足够了:

  1. 应用程序名称:Remix笔记(或自行取名)
  2. 环境变量: parse_application_idparse_javascript_key

环境变量使用.env文件中的值。

完成部署配置后,点击 “部署”。

Back4app 配置环境

稍等片刻,等待部署完成。部署完成后,点击屏幕左侧的绿色链接,在浏览器中打开应用程序。

有关详细教程,请查看Back4app 的 Container Remix 文档

就是这样!您的应用程序现已成功部署并可通过提供的链接访问。此外,Back4app 还为您的应用程序免费颁发了 SSL 证书。

结论

尽管 Remix 是一个相对较新的框架,但它允许开发人员构建功能强大的全栈网络应用程序。

该框架可解决网络应用程序的许多复杂问题,如表单处理、状态管理等。

在本文中,你已经学会了如何构建和部署 Remix 应用程序。现在,您应该能够利用Back4app创建一个简单的后端,并利用Back4app Containers部署您的容器化应用程序。

项目源代码可在back4app-containers-remixrepo 上获取。


Leave a reply

Your email address will not be published.