跳到主要内容位置

Remix简介与入门

传统的 React 或 Vue 开发的应用,是由服务器把整个应用发送到浏览器的,之后的交互完全在客户端,这样有两个问题:

  • 网页首次加载速度慢,需要下载完整的网页代码。
  • SEO 优化不友好,网页没有实际的 HTML 代码,完全由 JS 生成。

那么好多公司为了提升自己网站的响应速度和提高搜索引擎收录,开始采用了服务端渲染框架进行开发。

服务端渲染是说,用户根据 URL 请求页面的时候,服务器根据 URL 生成对应的 HTML 代码,发送到客户端浏览器上,这样减少了页面数据传输,提高网页加载速度,并且都是 HTML 静态代码,有助于搜索引擎收录。

在 React 等框架出现以前,所有编程语言的 web 框架,例如 django、rails、laravel 都是服务端渲染的,但是它们需要掌握编程语言本身,还有各种框架的特性,对于前端开发者来说上手比较困难,所以就有人基于 React、Vue 这样的框架推出了相应的服务端渲染框架,写法和普通前端应用几乎一样。

Remix 就是这样一个框架,基于 React,由 React Router 团队开发。Remix 应用的开发都是基于 React 组件,只是有几个封装好的函数来处理数据,所以对于前端开发者来说,入手非常方便,学习成本很低。

这里简单介绍一下开发过程,具体源码可以查看视频附带的链接。

安装 Remix

要创建一个 Remix 应用,运行 npx create-remix@latest命令:

npx create-remix@latest

命令会:

  • 提示输入项目的名字。
  • 选择要部署的环境,Remix App Server 是 Remix 自带的产品级服务器,一般情况下选择它就够了,如果要部署到 Fly.io、Vercel 等商业的服务环境,可以选择对应的选项:

img

接下来选择语言,Remix 推荐使用 TypeScript,但 JavaScript 也完全没问题。

img

最后提示要不要运行 npm install 安装依赖包,如过你使用本身就是 npm,可以选择是,如果你使用的是 yarn 可以选择否,后面自己安装。

项目运行之后它会创建这样的项目结构:

├── README.md
├── app
│ ├── entry.client.jsx
│ ├── entry.server.jsx
│ ├── root.jsx
│ └── routes
│ └── index.jsx
├── jsconfig.json
├── package.json
├── public
│ └── favicon.ico
└── remix.config.js

这里:

  • app 是应用开发的主要目录,entry.client.jsx 和 entry.server.jsx 是应用的入口文件,一般不用动。
  • root.jsx 是页面的入口文件,所有的组件文件都以 JSX 命名。root.jsx 里边有一些默认的配置。其中 LiveReload 组件可以在开发的时候自动重启服务。
  • routes 文件夹是 Remix 页面组件存放的地方,remix 的路由是按文件名和目录约定的。
  • public 文件夹下就是存放静态的资源文件,例如图片、视频等。
  • 外面的 remix.config.js 是 remix 的配置文件。

Remix 的核心:路由

Remix 的核心就是路由和嵌套的子路由,在 routes 根目录下编写的组件都是顶级页面,例如:

  • /routes/notes.jsx 代表 /notes 路径。
  • /routes/about.jsx 代表 /about 路径。

而在子目录下定义的则是子路由:

/routes/notes/new.jsx  -> /routes/new

跟上级路由同名:

/routes/notes.jsx
/routes/notes/ ... 子路由

对于上级路由,也可以把它定义在子路由目录的 index.jsx 文件里,

/routes/notes.jsx -> /routes/notes/index.jsx

这样访问一个深层的子路由,它是由所有上级路由的组件拼接而成的:

/notes/new

notes.jsx
notes/new.jsx

// 图

上级路由需要使用 Outlet 组件定义动态切换的部分,用来切换子路由,除 Outlet 之外的部分,都是常驻页面的,在子路由跳转的时候不会发生变化:

/routes/notes.jsx

import { Link, Outlet } from "remix";

export default function NoteIndex() {
return (
<div className="notes-layout">
<aside>
<div className="logo">NOTED</div>
<Link to="new" className="link-button">
添加一条笔记
</Link>
<ul className="notes-list">

</ul>
</aside>
<main className="notes-outlet">
<Outlet />
</main>
</div>
);
}

这样递归的在子路由中使用 Outlet,到最后不再有动态部分的路由,就组成了一个完整的页面,这个是 Remix 页面开发的主要步骤。

路由跳转

路由跳转使用 Remix 的 Link 组件,它和 Outlet 一样,都是 Remix 封装的 React Router 6 中的组件,所以使用方式也一样,例如定义跳转到 /notes 页面,可以设置 link 的 to 属性:

<div className="homePage">
<Link to="notes">查看笔记</Link>
</div>

为组件提供数据

要为组件提供数据,需要导出一个名字为 loader 的函数,可以是异步的,里边返回的数据就可以在组件中,通过 useLoaderData() Hook 访问到。loader 函数中的数据可以使用 fetch 从后端获取,也可以直接查询数据库,或者使用 mdx 文件。这里注意,loader 函数是运行在服务端的,不能访问浏览器相关的 API,也不能使用 JSX。

import { getAllNotes } from "~/data/notes.js"
import { useLoaderData } from "remix";

export async function loader() {
const data = {
noteList: getAllNotes(),
};
return data;
}

export default function NoteIndex() {
const data = useLoaderData();
return (
<div className="notes-layout">
<!-- ... -->
<ul className="notes-list">
{data.noteList.map((note) => {
return (
<li key={note.id}>
<Link to={`${note.id}`}>{note.title}</Link>
</li>
);
})}
</ul>
<!-- ... -->
</div>
);

动态路由参数

在路由跳转的时候,经常用到动态参数,例如列表页跳转到详情页,需要访问/1,/2 这样的动态路径,而这些路径使用同一个组件渲染,我们需要把 1、2 这样的值放到一个动态参数中,让组件使用。

Remix 定义动态参数的方式也是通过文件名的方式,使用 $开头,后面的名字就是动态参数的名字:

app/routes
├── index.jsx
├── notes
│ └── $noteId.jsx // <-- 这里创建文件
└── notes.jsx

之后在组件的 loader 方法中,可以解构出 params 参数,通过它访问文件名去掉$部分的变量名,就能获得 url 中的动态部分的值:

// $noteId.jsx

import { getNoteById } from "~/data/notes";
export async function loader({ params }) {
const note = getNoteById(params.noteId); // /1 -> 1, /2 -> 2
return { note };
}

添加样式

如果要给组件添加样式,可以直接导入 CSS 文件,然后在组件中导出一个 links() 函数,返回一个数组,里边是一个个 HTML link 元素的对象形式配置,设置 rel 和 href,把 href 指向导入进来的 CSS 变量,之后 Remix 就会自动在 HTML 中添加 link 属性。这里的 "~"波浪线符号代表 /app 目录:

/routes/notes.jsx

import notesStyle from "~/styles/notes.css";

export function links() {
return [
{
rel: "stylesheet",
href: notesStyle,
},
];
}

对于 root.jsx 这个入口组件,因为它是每个页面都必渲染的常驻组件,所以它里边可以加载一些全局的样式:

import globalStyles from "./styles/global.css";

export function links() {
return [
{
rel: "stylesheet",
href: globalStyles,
},
];
}

表单处理

Remix 还支持处理表单数据,表单元素可以使用 Remix 提供的 Form 组件,也可以使用普通的 form,但是 Remix 提供的 Form 组件提供的表单状态获取、以及异步提交数据功能。这里在定义表单组件时要注意:

  • form 的 method 必须是 post。
  • 每个表单控件需要有 name 属性,用于获取数据。
import { Form } from "remix";

export default function NewNoteRoute() {
return (
<Form method="post">
<h2>添加笔记</h2>
<label>
标题:
<input type="text" name="title" />
</label>
<label>
笔记:
<textarea name="content" cols="30" rows="10"></textarea>
</label>

<button type="submit">保存</button>
</Form>
);
}

当表单提交时,可以组件文件中导出一个 action 函数来获取表单数据,在 action 函数参数中,解构出 request 请求对象,调用 formData() 方法,就可以获取保存了所有表单数据的对象,之后通过它的 get 方法,传递输入控件的 name 属性,就能获取对应的值:

import { Form, redirect } from "remix";
import { addANewNote } from "~/data/notes";

export async function action({ request }) {
const form = await request.formData();
const title = form.get("title");
const content = form.get("content");

const note = addANewNote(title, content);

return redirect(`/notes/${note.id}`);
}

Return 中的 redirect() 方法是编程式的进行 URL 重定向跳转。

表单验证

利用 Remix 的 action 函数,我们可以方便的进行表单数据验证,action 函数可以返回数据,供组件使用,我们可以把验证错误放到里边:

  • 如果有验证错误,我们可以把错误信息,连同用户之前填写的信息一同返回出去,避免用户填写的数据丢失,这种情况容易出现在用户禁用了客户端 JS 的情况,Remix 无法利用异步提交表单的情况。
  • 返回响应数据到客户端,可以通过 Remix 的 json 方法,传递一个 json 对象和状态码。
  • 另外 action 函数也是运行在服务端的。
import { Form, redirect, json, useActionData } from "remix";
import { addANewNote } from "~/data/notes";

function validateTitle(title) {
// 验证标题
}

function validateContent(content) {
// 验证内容
}

export async function action({ request }) {
const form = await request.formData();
const title = form.get("title");
const content = form.get("content");

// 表单验证开始
const fieldErrors = {
title: validateTitle(title),
content: validateContent(content),
};

const fields = { title, content };

if (Object.values(fieldErrors).some(Boolean)) {
return json({ fieldErrors, fields }, { status: 400 });
}
// 表单验证结束

const note = addANewNote(title, content);

return redirect(`/notes/${note.id}`);
}

之后在组件代码中,使用 useActionData() 可以使用 json() 函数返回的数据,利用它们我们就可以显示表单验证错误信息,并回填用户输入了:

export default function NewNoteRoute() {
const actionData = useActionData();
return (
<Form method="post">
<h2>添加笔记</h2>
<label>
标题:
<input
type="text"
name="title"
defaultValue={actionData?.fields.title}
/>
{actionData?.fieldErrors?.title && (
<p className="field-error">{actionData?.fieldErrors?.title}</p>
)}
</label>
<label>
笔记:
<textarea
name="content"
cols="30"
rows="10"
defaultValue={actionData?.fields.content}
></textarea>
{actionData?.fieldErrors?.content && (
<p className="field-error">{actionData?.fieldErrors?.content}</p>
)}
</label>

<button type="submit">保存</button>
</Form>
);
}

响应 DELETE 等请求

因为 form 组件只支持 get 和 post 请求,如果要发送 DELETE、PUT 等请求,可以在表单中,定义一个隐藏的输入域,name 设置为_method,value 设置为 delete 或 put:

import { Form, useLoaderData } from "remix";

<Form method="post">
<input type="hidden" name="_method" value="delete" />
<button type="submit">删除</button>
</Form>;

之后在 action 中,通过表单 _method 字段值来区分:

import { Form, redirect, useLoaderData } from "remix";

export async function action({ request, params }) {
const form = await request.formData();
if (form.get("_method") === "delete") {
removeANote(params.noteId);
return redirect("/notes");
}
return null;
}

打包部署

Remix 项目开发完成之后,打包和部署很简单,运行 yarn build 或 npm run build 就可以了,remix 会生成一个 build 目录,和一个 public/build 目录,分别是客户端代码和服务端代码,之后直接运行 yarn start 启动产品服务器,就可以了:

yarn build
yarn start

把项目部署到任何支持 Node.js 环境的服务器上,就能发布到线上了。

小结

好了,这个就是 Remix 的简介和基本功能,你学会了吗?如果有帮助请三连,想学更多有用的前端开发知识,请关注峰华前端工程师,感谢观看!

提示

一系列的课程让你成为高级前端工程师。课程覆盖工作中所有常用的知识点和背后的使用逻辑,示例全部都为工作项目简化而来,学完即可直接上手开发!

即使你已经是高级前端工程师,在课程里也可能会发现新的知识点和技巧,让你的工作更加轻松!

《React 完全指南》课程,包含 React、React Router 和 Redux 详细介绍,所有示例改编自真实工作代码。点击查看详情。

《Vue 3.x 全家桶完全指南与实战》课程,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。

《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。

《JavaScript 基础语法详解》本人所著图书,包含 JavaScript 全面的语法知识和新特性, 可在京东、当当、淘宝等各大电商购买