avatarGithub

Remix实战系列 - 样式篇I

2022-07-30

先说结论:在Remix中写样式/css,对于Remix的其他功能来说,开发体验是相对不友好的。甚至对于最后的用户体验,也不够好。

怎么写

Remix中,路由模块支持export出一个links方法,通过links来引入样式。就和<link />标签一样。


import styleUrl from './app.css';
export const links = () => [
{
rel: 'stylesheet',
href: styleUrl
}
];

可以看到,links方法最终返回的是一个数组,所以引入多个样式也很简单


import globalUrl from './global.css';
import styleUrl from './app.css';
export const links = () => [
{
rel: 'stylesheet',
href: globalUrl
},
{
rel: 'stylesheet',
href: styleUrl
}
];

在访问到这个路由的时候,这些样式会被加载。同时在离开这个路由的时候,相应的样式也会被卸载,避免了路由之间样式的冲突。Easy and Simple.

那组件样式怎么写呢?

项目中难免会封装通用的组件,组件也会有自己的样式。在以往的项目中,我们只需要在组件代码中import css就可以了。那在Remix中呢?直接export links可以吗?。答案是否定的,links方法只在路由模块中生效。Remix官方推荐了两种做法

  1. 组件代码直接写在全局样式中 - 维护性不够友好,也会导致页面加载很多不必要的样式

  2. 组件代码export出一个links方法,在使用到该组件的路由模块中,引入并与路由样式一块输出。代码如下


// components/Button.jsx
import buttonStyle from './button.css';
// 组件内export出一个link方法
export const links = () => [
{
rel: 'stylesheet',
href: buttonStyle
}
];
export function Button() {
return <button className="btn">click me</button>
}
// routes/index.jsx
import Button, { links as buttonLinks } from '~/components/Button';
import styleUrl from './app.css';
export const links = () => [
{
rel: 'stylesheet',
href: styleUrl
},
...buttonLinks() // 在使用到该组件的地方,输出组件样式
];

这种写法从开发体验上还是可以接受的,虽然引入组件会变得麻烦一点点。但好处是

  1. 样式依赖清洗
  2. Remix可以预加载这些样式,在离开该路由后,也会卸载这些样式
  3. Remix中每个引入的css文件,最终打包后都会生成一个单独的css文件,这样改变组件的样式,不会影响到其他样式的缓存

简单介绍了Remix中如何引入样式后,那在实际项目中,又会遇到哪些问题呢?

Remix在打包中不会压缩css

Remix对于css处理是最简单/简陋的 - 只是帮你加一个文件名hash,不会做任何处理。这意味着如果你想对css做压缩的,你需要自己做

Remix默认只支持css

大家可能已经习惯写Less, Sass 甚至css in js。但这些在Remix中都不是开箱即用的,你需要自己扩展这些功能。好在Remix Examples中有很对例子可以参考

组件库的样式引入

大家项目里可能会用到像antd这样的组件库,那你是需要按Remix的方式去引入样式的。如果你想支持按需加载的,就需要在每个用到组件的地方,都单独引入组件的样式。不再像之前一样直接引入组件,再加上babel-import-plugin就可以了。

Purge CSS - 移除未使用的样式

这个是我遇到的最大问题了。我们项目内引入了一个内部的组件库,该组件库还不支持样式的按需引入,所以需要引入全部的样式。组件库样式大小在100KB左右。所以我想着引入一下purgecss,移除一下未使用到的样式。效果还是很明显的,组件库样式大小减小到20KB左右。但有一个问题,应该在什么时候做purge呢?

我一开始的方案是在Remix打包构建后,过程如图所示

purge

问题就在于,purge过程修改了css文件的内容,但文件名没有变化。而CDN文件都是有强缓存的,这会导致

  1. CDN节点的文件没有更新 - 这一点我们可以通过刷新cdn来解决
  2. 用户的浏览器缓存无法更新 - 不受我们控制。可以想象一个场景,需求上线后,用户访问了这个页面(css资源被缓存)。此时发现一个样式bug,你修复上线,但却不会生效。因为用户访问时,加载的是本地缓存的CSS

所以我想着那就在Remix构建前做吧,这样文件内容变了,文件名就会变。就不会缓存的问题了,但这就引入的新的问题,purge移除掉了不该移除的样式 - 为啥呢?

如果我们要在构建前做purge,那我们能分析的代码只有我们的源代码 - 即我们会看源代码中用到了哪些css样式,没用到的都删除。但是我们的代码中会引入三方组件,这部分组件也会用到css样式。由于purge过程中没有包括我们引入的组件,就导致这些样式被误删了。打包构建后就出问题了

这个问题我还没想到好的解法

css文件过多的问题

前面有提到,Remix不会对css做过多处理,你有几个css文件,最终就会生成几个css文件,最终也就会加载几个css文件。想象一下如果你的路由中引入了8个组件,每个组件都有自己的样式,而你的路由也有自己的样式。你可能还会有全局样式,和normalize样式。这时候访问这个路由就需要加载11个样式文件,每个样式文件都很小。

虽然这样可以达到很精确的缓存控制,虽然你可以开启http 2了。但css毕竟是会阻塞我们页面渲染的资源,在用户网络不好的情况下,加载11个网络资源是一个好的方案吗?对于这个问题,同样需要你自己去解决。无论是inline critical css,懒加载其他css。还是合并css为一个文件

后记

Remix官方也并非不想更好的支持样式,只是他们还没想好怎么做。对样式更好的支持也在他们的规划中。目前links的方式,除了我上面说的这些问题,从API设计的角度上看,我觉得还是很好的。links基本就是我们的link标签,所以你不仅仅可以用来加载样式。你可以可以用来做preloadprefetch


export const links = () => [
{
rel: 'preload',
href: xxx,
},
{
rel: 'prefetch',
href: xxxx
}
]

你也可以根据loader返回的数据,返回不同的值


export const links = (data) => {
if (data.lang = 'en') {
return [];
} else {
return [];
}
}