前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Remix 快速体验

Remix 快速体验

作者头像
玖柒的小窝
发布2021-12-11 23:20:25
8000
发布2021-12-11 23:20:25
举报
文章被收录于专栏:各类技术文章~各类技术文章~

Remix 体验

该文章是基于Remix 官网快速开始进行体验并翻译的。所以内容跟官网上是一样的。

  • 创建项目
  • 你的第一个路由
  • 加载数据(Loading Data)
  • 一点小小的重构
  • 从数据源拉取数据
  • 动态路由参数
  • 创建博客文章
  • 根路由
  • 提交表单

创建项目

初始化一个新的 Remix 项目

代码语言:javascript
复制
npx create-remix@latest
# 选择 Remix App Server
cd [你自己命名的项目目录]
npm run dev
复制代码
image.png
image.png

注意此处选择 Remix App Server

运行npx create-remix@latest之后,选择Remix App Server,开发语言选择 TypeScript,之后选择运行npm install。然后就可以等待下载依赖包。依赖包下载完成之后,浏览器打开 http://localhost:3000,就能看到如下的界面:

image.png
image.png

你的第一个路由

我们将新增一个路由 /posts。在这之前,我们需要先创建一个 Link,用于跳转到这个路由。

首先,打开 app/root.tsx,找到<Link to="/">Home</Link>,紧挨着它新建一个链接到/posts的链接

添加一个跳转到文章的 link 链接

代码语言:javascript
复制
<li>
  <Link to="/posts">Posts</Link>
</li>
复制代码

此时,如果我们在页面中点击这个链接的时候,我们将会看到一个404的页面。因为我们还没有添加路由。那接下来就让我们添加这个路由:

创建一个新的文件: app/routes/posts/index.tsx

路由文件都是放置在 routes 下的。一个文件就代表一个路由。我们也可以直接创建一个 posts.jsx 的文件,不过如果以后还会有文章详情之类的路由我们可能会创建类似post-detail.tsx 的路由,或者有路由嵌套的时候,不太好管理。所以我们可以在 posts 文件夹下创建一个 index.tsx 作为当前文件夹下的路由入口,就像 index.html 那样作为入口文件。

此时访问该链接的时候,我们会看到如下的页面。因为此时我们还没有添加任何的组件。

image.png
image.png

创建博客文章页组件

代码语言:javascript
复制
export default function Posts() {
  return (
    <div>
      <h1>Posts</h1>
    </div>
  );
}
复制代码

添加完以上的代码之后,我们再点击 Posts 链接的时候,在页面中就能看到 Posts 已经渲染出来了。

image.png
image.png

加载数据

数据加载是内置的 Remix 中的。

传统的 web 项目中,我们获取数据的 api 和用于渲染数据的前端组件是分开的。在 Remix 中,前端组件就是我们的 API 路由。当然如果我们获取数据的 api 接口是通过其他服务来提供的, 那也可以把 Remix 中的路由层作为前端的数据渲染控制器。接下来我们就为我们的组件设置一些数据。

为 posts 路由添加 useLoaderData

代码语言:javascript
复制
import { useLoaderData } from "remix";

export const loader = () => {
  return [
    {
      slug: "my-first-post",
      title: "My First Post"
    },
    {
      slug: "90s-mixtape",
      title: "A Mixtape I Made Just For You"
    }
  ];
};

export default function Posts() {
  const posts = useLoaderData();
  console.log(posts);
  return (
    <div>
      <h1>Posts</h1>
    </div>
  );
}
复制代码

Loaders 就是当前组件的 API,并且已经通过 useLoaderData 进行的封装。如果你同时打开浏览器控制台和后台控制台,你会发现日志里都打印了 posts 的内容。这是由于Remix是在服务端渲染完页面,然后把 html 发送到浏览器端显示的,同时也会在前端里注入并输入日志数据。

在文章列表里添加链接

代码语言:javascript
复制
  <ul>
    {posts.map(post => (
      <li key={post.slug}>
        <Link to={post.slug}>{post.title}</Link>
      </li>
    ))}
  </ul>
复制代码

记得在文件头部引入Link。此时TypeScript会报错。我们添加一些类型解决下报错问题。

代码语言:javascript
复制
import { Link, useLoaderData } from "remix";

type Post = {
  slug: string;
  title: string;
};

export const loader = () => {
  const posts: Post[] = [
    {
      slug: "my-first-post",
      title: "My First Post"
    },
    {
      slug: "90s-mixtape",
      title: "A Mixtape I Made Just For You"
    }
  ];
  return posts;
};

export default function Posts() {
  const posts = useLoaderData<Post[]>();
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <Link to={post.slug}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
复制代码

一点小小的重构

根据以往的经验来说,我们最好是创建一个模块来处理特定的场景。在我们的例子中,会涉及到读取博客以及添加博客。让我们开始创建他们。创建一个 getPosts 方法并在我们的 post 模块中导出。

代码语言:javascript
复制
// 创建 post 文件: app/post.ts
复制代码
代码语言:javascript
复制
export type Post = {
  slug: string;
  title: string;
};

export function getPosts() {
  const posts: Post[] = [
    {
      slug: "my-first-post",
      title: "My First Post"
    },
    {
      slug: "90s-mixtape",
      title: "A Mixtape I Made Just For You"
    }
  ];
  return posts;
}
复制代码

修改 posts 路由。在路由中使用我们的 Post 模块

代码语言:javascript
复制
// posts/index.jsx
import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";

export const loader = () => {
  return getPosts();
};

// ...
复制代码

从数据源拉取数据

在实际的项目中,我们将会根据实际需要选择数据存储方式。会选择使用合适的数据库,比如Postgres, FaunaDB, Supabase。不过在该体验中,我们将使用文件系统。

在项目根目录下创建 posts 文件夹以及在文件夹里创建一些MarkDown 格式的博客文章

代码语言:javascript
复制
mkdir posts

touch posts/my-first-post.md
touch posts/90s-mixtape.md

复制代码

在这些md 文件里随意放一些内容。不过确保里面有带 title 的 front matter的属性。

修改 getPosts 方法,从文件系统里读取内容

我们将会用到一个 node 模块:

代码语言:javascript
复制
npm add front-matter
复制代码

修改 app/posts 文件,内容如下:

代码语言:javascript
复制
import path from "path";
import fs from "fs/promises";
import parseFrontMatter from "front-matter";

export type Post = {
  slug: string;
  title: string;
};

// relative to the server output not the source!
const postsPath = path.join(__dirname, "..", "posts");

export async function getPosts() {
  const dir = await fs.readdir(postsPath);
  return Promise.all(
    dir.map(async filename => {
      const file = await fs.readFile(
        path.join(postsPath, filename)
      );
      const { attributes } = parseFrontMatter(
        file.toString()
      );
      return {
        slug: filename.replace(/\.md$/, ""),
        title: attributes.title
      };
    })
  );
}
复制代码

此时 TypeScript 应该会报错了。让我们来解决下错误。

由于我们是通过读取文件获取到内容,所以类型检查不知道里面有什么类型的数据。所以我们需要运行时检查。我们将引入 invariant 来帮助我们更加容易的处理这个问题。

app/post.ts 文件内容如下:

代码语言:javascript
复制
import path from "path";
import fs from "fs/promises";
import parseFrontMatter from "front-matter";
import invariant from "tiny-invariant";

export type Post = {
  slug: string;
  title: string;
};

export type PostMarkdownAttributes = {
  title: string;
};

const postsPath = path.join(__dirname, "..", "posts");

function isValidPostAttributes(
  attributes: any
): attributes is PostMarkdownAttributes {
  return attributes?.title;
}

export async function getPosts() {
  const dir = await fs.readdir(postsPath);
  return Promise.all(
    dir.map(async filename => {
      const file = await fs.readFile(
        path.join(postsPath, filename)
      );
      const { attributes } = parseFrontMatter(
        file.toString()
      );
      invariant(
        isValidPostAttributes(attributes),
        `${filename} has bad meta data!`
      );
      return {
        slug: filename.replace(/\.md$/, ""),
        title: attributes.title
      };
    })
  );
}
复制代码

即便我们没有使用 TS,我们也会想要通过使用 invariant 来知道具体哪个地方报错了。此时我们再去访问 http://localhost:3000/posts 的时候,我们就可以看到从文件系统里读取的文章列表。你可以自由的添加其他的文章来观察数据的变化。

动态路由参数

接下来让我们创建访问具体文章的路由。我们希望下面的路由能够生效:

代码语言:javascript
复制
/posts/my-first-post
/posts/90s-mix-cdr
复制代码

我们不用为每一篇文章都创建一个路由。取而代之的是在 url 中通过动态路由标识来进行处理。 Remix 会解析并传递动态的参数到路由中。

创建一个动态的路由文件: app/routes/posts/$slug.tsx

代码语言:javascript
复制
export default function PostSlug() {
  return (
    <div>
      <h1>Some Post</h1>
    </div>
  );
}
复制代码

添加一个 loader 访问参数

代码语言:javascript
复制
import { useLoaderData } from "remix";

export const loader = async ({ params }) => {
  return params.slug;
};

export default function PostSlug() {
  const slug = useLoaderData();
  return (
    <div>
      <h1>Some Post: {slug}</h1>
    </div>
  );
}
复制代码

路由上$符号后面的值会作为 loader 参数 params里的 key 值。添加一些 TypeScript 的类型校验:

代码语言:javascript
复制
import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";

export const loader: LoaderFunction = async ({
  params
}) => {
  return params.slug;
};
复制代码

接下来让我们从文件系统里读取文章内容。

在 post 模块里添加 getPost 方法

代码语言:javascript
复制
// ...
export async function getPost(slug: string) {
  const filepath = path.join(postsPath, slug + ".md");
  const file = await fs.readFile(filepath);
  const { attributes } = parseFrontMatter(file.toString());
  invariant(
    isValidPostAttributes(attributes),
    `Post ${filepath} is missing attributes`
  );
  return { slug, title: attributes.title };
}
复制代码

在路由中使用新的 getPost 方法

代码语言:javascript
复制
// routes/posts/$slug.tsx
import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";
import { getPost } from "~/post";
import invariant from "tiny-invariant";

export const loader: LoaderFunction = async ({
  params
}) => {
  invariant(params.slug, "expected params.slug");
  return getPost(params.slug);
};

export default function PostSlug() {
  const post = useLoaderData();
  return (
    <div>
      <h1>{post.title}</h1>
    </div>
  );
}
复制代码

由于params 里的参数不一定是什么值,有可能不是 slug, 所以我们依然使用 invariant来进行错误判断。同时也能够让 TS不报错。

我们使用 marked 来对 markdown 进行解析。

代码语言:javascript
复制
npm add marked
# if using typescript (如果使用typescript的话,还需要安装以下的包)
npm add @types/marked
复制代码

在路由中渲染 HTML

代码语言:javascript
复制
// ...
export default function PostSlug() {
  const post = useLoaderData();
  return (
    <div dangerouslySetInnerHTML={{ __html: post.html }} />
  );
}
复制代码

至此,我们可以撒花开香槟庆祝一下,我们拥有了自己的博客!

创建博客文章

我们以上的博客系统就开发完成可以进行部署了。但是我们最好的方式是把博客文章数据存储到数据库中,这样我们有什么修改也不用对系统进行修改上线。所以我们需要一个创建文章的入口,我们将会使用到表单提交。

创建一个 admin 路由

代码语言:javascript
复制
touch app/routes/admin.tsx
复制代码
代码语言:javascript
复制
import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";

export const loader = () => {
  return getPosts();
};

export default function Admin() {
  const posts = useLoaderData<Post[]>();
  return (
    <div className="admin">
      <nav>
        <h1>Admin</h1>
        <ul>
          {posts.map(post => (
            <li key={post.slug}>
              <Link to={`/posts/${post.slug}`}>
                {post.title}
              </Link>
            </li>
          ))}
        </ul>
      </nav>
      <main>...</main>
    </div>
  );
}
复制代码

除了添加的一些额外的 html结构之外,你会发现admin.tsx 里的大部分内容都是从 posts 路由里拷贝过来的。我们接下来将会进行一些样式的修改。

创建一个admin.css样式文件

代码语言:javascript
复制
touch app/styles/admin.css
复制代码
代码语言:javascript
复制
.admin {
  display: flex;
}

.admin > nav {
  padding-right: 2rem;
}

.admin > main {
  flex: 1;
  border-left: solid 1px #ccc;
  padding-left: 2rem;
}

em {
  color: red;
}
复制代码

在 admin 路由中关联样式文件

代码语言:javascript
复制
import { Link, useLoaderData } from "remix";
import { getPosts } from "~/post";
import type { Post } from "~/post";
import adminStyles from "~/styles/admin.css";

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

// ...
复制代码

预览admin 路由,效果如下:

image.png
image.png

每一个路由都可以导出一个返回 link 数组的 links 方法。 我们使用 { rel: "stylesheet", href: adminStyles} 来代替 <link rel="stylesheet" href="..." />。这允许 Remix 合并已经渲染的路由集合并在页面顶部的 <Links/>中渲染出来。现在我们就能够看到一个左侧有文章列表,右侧有一个展位的页面呈现出来。 你可以手动的访问 http://localhost:3000/admin 这个路由。

根路由(Index Routes)

让我们为 admin 创建一个index route。我们将会介绍嵌套路由的使用方法。

为 admin 路由的子路由创建一个文件夹,同时在里面创建一个 index.tsx

代码语言:javascript
复制
mkdir app/routes/admin
touch app/routes/admin/index.tsx
复制代码
代码语言:javascript
复制
import { Link } from "remix";

export default function AdminIndex() {
  return (
    <p>
      <Link to="new">Create a New Post</Link>
    </p>
  );
}
复制代码

此时如果刷新浏览器,我们并不能看到刚才创建的内容。在app/routes/admin/下面的每一个路由,当他们的路由匹配的时候,都会在app/routes/admin.tsx里面渲染出来。你得控制 admin 中如何去展示这些匹配的路由。

在 admin 页面里添加 outlet

代码语言:javascript
复制
// admin.tsx

import { Outlet, Link, useLoaderData } from "remix";

//...
export default function Admin() {
  const posts = useLoaderData<Post[]>();
  return (
    <div className="admin">
      <nav>
        <h1>Admin</h1>
        <ul>
          {posts.map(post => (
            <li key={post.slug}>
              <Link to={`/posts/${post.slug}`}>
                {post.title}
              </Link>
            </li>
          ))}
        </ul>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}
复制代码

当 URL匹配父路由的路径的时候,index routes将会被渲染到 outlet 中。接下来让我们添加 /admin/new 路由,然后点击Create a New Post,看看会发生什么。

创建 app/routes/admin/new.tsx 路由

代码语言:javascript
复制
touch app/routes/admin/new.tsx
复制代码
代码语言:javascript
复制
export default function NewPost() {
  return <h2>New Post</h2>;
}
复制代码

当我们点击 <Link to="new">Create a New Post</Link>的时候,会发现,路由到了admin/new,同时内容也发生了变化,在 outlet中渲染除了admin/new的内容。

表单提交(Actions)

接下来我们将要干一件大事,在new路由中创建一个 form 表单来提交新的博客文章。

在 new 路由中添加一个 form 表单

代码语言:javascript
复制
import { Form } from "remix";

export default function NewPost() {
  return (
    <Form method="post">
      <p>
        <label>
          Post Title: <input type="text" name="title" />
        </label>
      </p>
      <p>
        <label>
          Post Slug: <input type="text" name="slug" />
        </label>
      </p>
      <p>
        <label htmlFor="markdown">Markdown:</label>
        <br />
        <textarea id="markdown" rows={20} name="markdown" />
      </p>
      <p>
        <button type="submit">Create Post</button>
      </p>
    </Form>
  );
}
复制代码

这跟我们之前写的提交 form 表单没什么两样。让我们在 post.ts 模块里创建提交一个文章的必要代码。

在 app/post.ts 的任何位置添加 createPost 方法

代码语言:javascript
复制
// ...
export async function createPost(post) {
  const md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(
    path.join(postsPath, post.slug + ".md"),
    md
  );
  return getPost(post.slug);
}
复制代码

在 new 路由的 action 中调用 createPost 方法

代码语言:javascript
复制
import { redirect, Form } from "remix";
import { createPost } from "~/post";

export const action = async ({ request }) => {
  const formData = await request.formData();

  const title = formData.get("title");
  const slug = formData.get("slug");
  const markdown = formData.get("markdown");

  await createPost({ title, slug, markdown });

  return redirect("/admin");
};

export default function NewPost() {
  // ...
}
复制代码

解决 TS 报错:

代码语言:javascript
复制
// app/post.ts
type NewPost = {
  title: string;
  slug: string;
  markdown: string;
};

export async function createPost(post: NewPost) {
  const md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(
    path.join(postsPath, post.slug + ".md"),
    md
  );
  return getPost(post.slug);
}

//...
复制代码
代码语言:javascript
复制
import { Form, redirect } from "remix";
import type { ActionFunction } from "remix";
import { createPost } from "~/post";

export const action: ActionFunction = async ({
  request
}) => {
  const formData = await request.formData();

  const title = formData.get("title");
  const slug = formData.get("slug");
  const markdown = formData.get("markdown");

  await createPost({ title, slug, markdown });

  return redirect("/admin");
};
复制代码

不管我们是否使用 TS,当用户并没有输入表单字段的时候就进行提交将会有问题。在我们提交表单之前,让我们添加一些校验。

校验表单是否包含我们需要的数据,如果校验失败,则返回错误信息

代码语言:javascript
复制
//...
export const action: ActionFunction = async ({
  request
}) => {
  const formData = await request.formData();

  const title = formData.get("title");
  const slug = formData.get("slug");
  const markdown = formData.get("markdown");

  const errors = {};
  if (!title) errors.title = true;
  if (!slug) errors.slug = true;
  if (!markdown) errors.markdown = true;

  if (Object.keys(errors).length) {
    return errors;
  }

  await createPost({ title, slug, markdown });

  return redirect("/admin");
};
复制代码

注意此时我们并没有返回 redirect 信息。而是返回了错误信息。在组件中,这些信息可以通过 useActionData 进行访问。它跟 useLoaderData 很像。不过只是数据是在表单提交之后通过 action获取到的。

在 UI 上添加校验信息显示

代码语言:javascript
复制
import {
  useActionData,
  Form,
  redirect,
  ActionFunction
} from "remix";

// ...

export default function NewPost() {
  const errors = useActionData();

  return (
    <Form method="post">
      <p>
        <label>
          Post Title:{" "}
          {errors?.title && <em>Title is required</em>}
          <input type="text" name="title" />
        </label>
      </p>
      <p>
        <label>
          Post Slug:{" "}
          {errors?.slug && <em>Slug is required</em>}
          <input type="text" name="slug" />
        </label>
      </p>
      <p>
        <label htmlFor="markdown">Markdown:</label>{" "}
        {errors?.markdown && <em>Markdown is required</em>}
        <br />
        <textarea rows={20} name="markdown" />
      </p>
      <p>
        <button type="submit">Create Post</button>
      </p>
    </Form>
  );
}
复制代码

有意思的地方在于:当在开发者工具中禁用JavaScript,然后再试试。由于 Remix 是基于 HTTP以及 HTML来构建的,我们禁用 JavaScript 之后,程序在浏览器中依然可以很好的工作。这还不是重点,当我们减慢数据的处理,在 form表单中添加一些加载中的UI。

通过一个模拟的延迟来让我们的 action 变慢

代码语言:javascript
复制
// ...
export const action: ActionFunction = async ({
  request
}) => {
  await new Promise(res => setTimeout(res, 1000));

  const formData = await request.formData();

  const title = formData.get("title");
  const slug = formData.get("slug");
  const markdown = formData.get("markdown");
  // ...
};
//...
复制代码

通过 useTransition 添加加载中的 UI

代码语言:javascript
复制
import {
  useTransition,
  useActionData,
  Form,
  redirect
} from "remix";

// ...

export default function NewPost() {
  const errors = useActionData();
  const transition = useTransition();

  return (
    <Form method="post">
      {/* ... */}

      <p>
        <button type="submit">
          {transition.submission
            ? "Creating..."
            : "Create Post"}
        </button>
      </p>
    </Form>
  );
}
复制代码

现在用户能够在没有 JavaScript 支持的情况下就拥有很好的体验了。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Remix 体验
    • 创建项目
      • 你的第一个路由
        • 首先,打开 app/root.tsx,找到<Link to="/">Home</Link>,紧挨着它新建一个链接到/posts的链接
        • 添加一个跳转到文章的 link 链接
        • 创建一个新的文件: app/routes/posts/index.tsx
        • 创建博客文章页组件
      • 加载数据
        • 为 posts 路由添加 useLoaderData
        • 在文章列表里添加链接
      • 一点小小的重构
        • 修改 posts 路由。在路由中使用我们的 Post 模块
      • 从数据源拉取数据
        • 在项目根目录下创建 posts 文件夹以及在文件夹里创建一些MarkDown 格式的博客文章
        • 修改 getPosts 方法,从文件系统里读取内容
      • 动态路由参数
        • 创建一个动态的路由文件: app/routes/posts/$slug.tsx
        • 添加一个 loader 访问参数
        • 在 post 模块里添加 getPost 方法
        • 在路由中使用新的 getPost 方法
        • 在路由中渲染 HTML
      • 创建博客文章
        • 创建一个 admin 路由
        • 创建一个admin.css样式文件
        • 在 admin 路由中关联样式文件
      • 根路由(Index Routes)
        • 为 admin 路由的子路由创建一个文件夹,同时在里面创建一个 index.tsx
        • 在 admin 页面里添加 outlet
        • 创建 app/routes/admin/new.tsx 路由
      • 表单提交(Actions)
        • 在 new 路由中添加一个 form 表单
        • 在 app/post.ts 的任何位置添加 createPost 方法
        • 在 new 路由的 action 中调用 createPost 方法
        • 校验表单是否包含我们需要的数据,如果校验失败,则返回错误信息
        • 在 UI 上添加校验信息显示
        • 通过一个模拟的延迟来让我们的 action 变慢
        • 通过 useTransition 添加加载中的 UI
    相关产品与服务
    数据库
    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档