avatarGithub

Remix框架初体验1

2022-04-04

其实很早就对remix感兴趣了,不过那时候的remix是封闭的,只给付费人员使用,不舍花钱的我只能馋着了。不过去年10月份remix开源后,也有机会一探究竟了

remix是大名鼎鼎的react-router的作者Ryan FlorenceMICHAEL JACKSON一起创建的。最近又加入了React社区非常有名的Kent。Kent的个人博客上也有蛮多文章介绍remix的,感兴趣的可以去看一下。我会从个人实际项目出发,讲一讲remix的设计理念和特性

项目介绍

先简单介绍一下这个项目。最近一直在学英语,学习方法是找一些youtube的视频跟读,就需要一边播放视频,一边听写出文本,然后跟着读。所以就用remix实现了这么一个网站https://shadowing-henna.vercel.app/

项目比较简单,就是一个列表页,一个新增页,以及编辑页。但能覆盖基本的CRU - create, read, update场景。我们跟着这些页面,来看看remix的一些基本特性

列表页

app/routes/index.jsx

import { useLoaderData, Link } from 'remix';
import supabase from '~/utils/supabase';
import { format } from '~/utils/date';
export const loader = async () => {
// supabase是我们的数据库
const { data, error } = await supabase
.from('shadows')
.select('id,title,created_at')
.order('created_at', { ascending: false })
if (error) throw error.message;
return data
}
export default function Index() {
const execises = useLoaderData();
return (
<div className="root">
{
execises.map(item => <Item key={item.id} {...item} />)
}
</div>
);
}
function Item({ id, title, created_at }) {
return (
<article className="item">
<Link className="item-title" to={`/${id}`}>{title}</Link>
<p>{format(created_at)}</p>
</article>
)
}

以上就是列表页的代码了。除了export出react组件外,还export出了一个loader方法,这就是remix获取服务端数据的方法。

remix是一个全栈的react框架,loader是直接运行在服务端的,也就是说我们可以在loader里使用服务端能力,比如读取本地文件,访问数据库等等。像我们这个地方就是直接读取数据库里的数据然后返回。组件里的useLoaderData方法就是用来获取loader中返回的数据的。

当我们的组件依赖服务端数据,需要做异步请求时,通常都需要考虑这么几个问题 - loading怎么处理?error怎么处理?子组件需要用的话数据怎么传递?

loading

remix走的是SSR路线,即使在做客户端页面跳转时,remix也会保证在loader执行成功后,才会开始渲染我们的组件。所以loading状态默认不用处理。Goodbye if (loading) return <div>loaindg...</div>

error

当接口报错时又如何处理呢?remix拥抱了react的做法 - ErrorBoundary。我们可以export出一个ErrorBoundary组件,处理异常

app/routes/index.jsx

...
export function ErrorBoundary({ error }) {
return (
<div className="error-container">
出错了 - {error.message}
</div>
);
}

数据共享

路由下的所有子组件,都可以直接调用用useLoaderData方法获取这份数据。所以基本上我们也不再需要引入其他的状态管理方案了

新增页

remix又是如何做数据更新的呢?大家还记得大明湖畔的<form action="">吗?


import { redirect, useTransition, useActionData, json } from 'remix';
import { useState, useEffect } from 'react';
import supabase from '~/utils/supabase';
export const action = async ({ request }) => {
const body = await request.formData();
const title = body.get('title');
const content = body.get('content')
const vid = body.get('vid')
const { data, error } = await supabase.from('shadows')
.insert([
{ title, content, vid, type: 1 },
])
if (error) return json(error.message || `Something went wrong!`, {
status: 500
})
return redirect(`/${data[0].id}`);
}
export default function Index() {
const [vid, setVid] = useState();
const handleVidChange = e => {
const url = new URL(e.target.value);
setVid(url.searchParams.get('v'))
}
return (
<form method="post" className="form">
<textarea name="content" className="editor" />
<div>
<input required name="title" type="text" placeholder="title" />
<input autoFocus required name="url" placeholder="youtube video link" type="text" onChange={handleVidChange} />
<input name="vid" hidden value={vid} />
<button className="button" type="submit">Save</button>
<div className="video">
{vid && <lite-youtube videoid={vid} />}
</div>
</div>
</form>
);
}

就是最普通的form,唯一不同的是export出了一个action方法,action也是运行在服务端的。这就是remix处理表单的方法 - 拥抱标准。当表单提交时,服务端会收到客户端请求,action方法被执行,通过request.formData可以拿到表单的数据。这里的request, reponse等用的都是fetch API的标准实现

这一点也体现出remix的一个设计理念,拥抱web标准

拥抱标准有什么好处呢?表单提交逻辑在js被禁用时依然可以正常工作。大家可能觉得有点多余,什么情况下js会被禁用啊?那js加载过程中呢?用户不用等你的js加载完成,就可以直接提交表单。做过性能优化的应该都能懂这一点有多么重要~

拥抱标准的另一个好处就是,你学习的知识是可迁移的,而不是跟某个框架绑定的。举个简单例子,大家现在可能会用各种表单库或ui库去做表单开发,但你学习的是这个库的api。当你没法使用这个库时,这部分知识就作用不大了。举个具体例子,当一个开发中后台,用惯了antd表单的同学,被安排去开发h5页面,且涉及表单的时候,这个同学会怎么写表单呢?

这也是remix文档上所写的 - 在用remix开发应用时,可能大部分时候你都在看MDN的文档

remix pattern

我们来简单总结一下remix pattern


// 读数据
export async function loader({ request }) {
...
}
// 表单提交 - 写数据
export async function action({ request }) {
...
}
export default function () {
// 取数据
const data = useLoaderData();
...
}

以上就基本是remix给我们规范的读数据和写数据的方法。不过这种模式只能在路由组件中使用。

这也是remix对于「数据在哪里获取」这个问题给出的答案 - 统一在路由这一层获取。 配合上react router提供的nested routes,可以避免一个页面所有的数据都在集中在一个loader中。

这个pattern我认为有几点非常好

  1. 作为一个全栈的react框架,既拥有的服务端的能力,同时又和前端的代码够内聚,使用和实现维护在一个地方,开发体验非常好
  2. 数据和ui做了很好地分离 - 组件变得非常简单,基本就和一个接收props的组件一样,且代码都是同步的,思路不用在异步和同步间切换
  3. 不用在组件内处理loading和error,用更声明式的方式替代过程式的 if (loading)if(errro)代码
  4. 拥抱标准

当然,我上面说的都是DX,开发体验层面的。remix还提供了很多用户体验层面的优化,这个在后续的文章中会为大家详细介绍,不在这里展开

如何引入样式

目前用下来,remix中处理样式的方法,是我唯一感到开发体验不太友好的地方


import styles from "~/styles/index.css";
export function links() {
return [{ rel: "stylesheet", href: styles }];
}

如上代码所示,你可以通过在路由组件中exportlinks方法来引入样式。对于路由组件的样式来讲,这样写是可以接受的。但是对于通用组件的样式呢?官方提供了这么一个例子

app/components/button/style.css

[data-button] {
border: solid 1px;
background: white;
color: #454545;
}

app/components/button/index.jsx

import styles from "./styles.css";
export const links = () => [
{ rel: "stylesheet", href: styles },
];
export const Button = React.forwardRef(
({ children, ...props }, ref) => {
return <button {...props} ref={ref} data-button />;
}
);

如果你想使用这个组件,你需要在路由组件中显示的引入这个样式

app/routes/index.jsx

import styles from "~/styles/index.css";
import {
Button,
links as buttonLinks,
} from "~/components/button";
export function links() {
return [
...buttonLinks(),
{ rel: "stylesheet", href: styles },
];
}

想象一下当我一个页面用了很多通用组件的时候。。。

另外remix默认是不支持css预处理器和后处理器的,如果想引入的话,需要自己处理。

以及按remix的设计,import出来的style需要是一个url。所以和目前社区上的一些css in js库可能无法兼容,比如vanilla-extract

由于目前我还没有在样式复杂的项目上体验remix,所以具体的开发体验还有待验证

浏览器兼容性

国内当前的环境,当开发C端,特别是移动端页面时,还是需要考虑兼容性问题的。remix只能运行在支持ES Modules的浏览器上。对于国内的环境是一个挑战

不过如上文中提到的,即使禁用js,remix应用依然是可以运行,提供基本的功能的。这也是remix遵循的另一个理念,Progressive enhancement - 渐进增强,即每个人都可以访问到内容和使用基本的功能,而对于使用设备更好的浏览器的用户,提供更好的交互体验

不过这个可能很难说服PM同学~大家加油,不过中后台项目还是可以放心使用。我已经计划把我们中后台用用remix重写了,冲冲冲

好啦,这就是最近一段时间使用remix的体验,我个人认为是一个非常好的框架,开发体验,用户体验,设计上都非常solid,值得大家尝试和学习。后续会为大家带来更多原理分析和实战分享