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官方推荐了两种做法
-
组件代码直接写在全局样式中 - 维护性不够友好,也会导致页面加载很多不必要的样式
-
组件代码
export
出一个links
方法,在使用到该组件的路由模块中,引入并与路由样式一块输出。代码如下
// components/Button.jsximport 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.jsximport Button, { links as buttonLinks } from '~/components/Button';import styleUrl from './app.css';export const links = () => [ { rel: 'stylesheet', href: styleUrl }, ...buttonLinks() // 在使用到该组件的地方,输出组件样式];
这种写法从开发体验上还是可以接受的,虽然引入组件会变得麻烦一点点。但好处是
- 样式依赖清洗
- Remix可以预加载这些样式,在离开该路由后,也会卸载这些样式
- 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过程修改了css文件的内容,但文件名没有变化。而CDN文件都是有强缓存的,这会导致
- CDN节点的文件没有更新 - 这一点我们可以通过刷新cdn来解决
- 用户的浏览器缓存无法更新 - 不受我们控制。可以想象一个场景,需求上线后,用户访问了这个页面(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
标签,所以你不仅仅可以用来加载样式。你可以可以用来做preload
和prefetch
export const links = () => [ { rel: 'preload', href: xxx, }, { rel: 'prefetch', href: xxxx }]
你也可以根据loader
返回的数据,返回不同的值
export const links = (data) => { if (data.lang = 'en') { return []; } else { return []; }}