Linaria 也许是现在 React 最佳的 JSS 方案

9,586

React 社区一直在探索各种 JSS 方案,比如现在比较出名的 styled-components ,但他们或多或少都有些问题存在,但是社区对 JSS 方案的探索一直没有停下,而现在看上去最像最佳方案的是 Linaria 库。翻了下这个库相关的中文资料几乎没有,于是写了这篇与大家分享介绍下相关的内容

介绍

Linaria 是一个 零运行时 的JSS 框架,其特点有:

  • 将 CSS 纳入到 JS 体系中,并且这种支持是零成本的! CSS 相关代码会在编译期被抽出到 CSS 文件中
  • 类 Sass 的 CSS 的语法
  • 通过使用 CSS 变量,Linaria 支持快速创建动态属性的 React 样式组件
  • 使用 CSS sourcemaps 易于定位样式位置
  • 支持 stylint
  • 不再需要预处理器,可以使用 JavaScript 控制 CSS 的逻辑
  • 但是支持使用预处理器,比如 Sass 或 PostCSS

相对于传统 CSS 方案的优点

1. 样式隔离

const title = css`
  font-size: 18px;
`;

类似代码最终会被会被编译成

.k4yi6fg {
  font-size: 18px;
}

其 class 命名是通过计算文件路径的哈希值确定的

2. 样式和组件归属到同一文件中

不会再有编写组件时需要在 JS 文件和 CSS 文件跳转的上下文切换的情况。不过如果你想要分离的话,同样也是支持的

3. 可靠安全的重构支持

因为 JSS 的样式其实就是 JS 变量而已,所以你可以很容易通过代码逻辑找到组件相关的样式逻辑,而不用害怕重构会造成预料之外影响

4. 不再需要预处理器

JSS 最迷人的一点就是,当你将 CSS 归属到 JS 下时,你就自动获得来使用 JS 编写 CSS 逻辑的能力,最基本的条件计算,被包装成简单函数调用的的复杂逻辑。这意味着 CSS 的表达能力的上限不再局限于自身,而是由 JS 决定的

例如你可以编写这样的代码


const DEFAULT_COLOR = '#fffff'
const PRIMARY_COLOR = '#de2d68';

const getColor = Math.random() > 0.5 ? PRIMARY_COLOR : DEFAULT_COLOR;

const button = css`
  background-color: ${getColor()};

  &:hover {
    background-color: ${Math.random() > 0.5 ? PRIMARY_COLOR : DEFAULT_COLOR};
  }
`;

5. Tree shaking

就像我们刚才说的一样, JSS 其实只是 JS 变量,那么自然而然 JS 能做到的 Tree shaking ,Linaria 一样能做到。 这点对于 UI 库的开发者其实很有吸引力,不再需要引入额外的 babel 插件,而是自动通过 Tree shaking 来做到样式的按需引入

6. 自动添加浏览器前缀

Linaria 会自动通过添加浏览器前缀,帮你对一些特殊属性做兼容性支持,同时你依然可以使用 PostCSS 做进一步的优化

7. 声明式且动态化的控制 React 组件样式的能力

通过 styled API ,很容易去声明 React 动态样式组件。原理是通过 CSS 变量来实现组件样式自动更新的能力,常规的 CSS 方案则需要你手动的去维护相关的逻辑

const Box = styled.div`
  background-color: orange;
  height: ${props => props.size}px;
  width: ${props => props.size}px;
`;

<Box size={48}>

相比于 CSS 预处理器的优势

1. 没有新的学习成本

Linaria 的语法可以看作只是支持嵌套的 CSS 语法而已。没有变量,mixins 或 函数什么的,这些都可以用 JS 的逻辑来代替

2. 相对于传统 CSS 方案的优点对于CSS 预处理器也一样

相比于直接写 行内样式 的优势

1. 完全的 CSS 能力支持

行内样式存在局限性,而 Linaria 则支持 CSS 的所有特性:

  • 媒体查询
  • CSS3 动画
  • 伪类,伪元素

2. 性能优势

通过 class 命名来应用样式要比行内样式快

相比其他的 JSS 方案的优势

1. CSS 的下载和解析是和 JS 分开的

因为 CSS 在编译器被抽出到 CSS 文件中了,因此浏览器可以并行的下载 CSS 和 JS 文件,加速首屏时间

2. 没有额外的解析成本

很多 JSS 框架是通过某个第三方 JS 库来解析 CSS 字符串的,由于需要包含解析器,会使得库的体积增大。 并且 CSS 的解析执行被延迟到了 JS 运行时,在一些低端设备上,很容易带来可以感知到的延迟

Linaria 特殊就特殊在它 没有运行时 这一说,它的样式会在编译期解析抽出来,生成 CSS 文件,不需要在运行时额外解析一次。

3. SSR 时没有重复渲染的性能损耗

对于基于组件的 JSS 框架来说,使用不同的 props 渲染同一个组件会使得同一份样式被复制多次,这使得 SSR 时产物的体积会增大。尽管大部分情况下,这种问题带来的性能损耗不值一提,但是对于渲染多个仅有细微差异的大型列表时,很容易使得体积迅速增长

除此之外,在做 SSR 你需要将写在 JS 文件中的 CSS 样式抽取出来,然后传输给浏览器,这同样增加产物体积

Linaria 会生成唯一的样式规则,使用 CSS 变量来应用不同的差异,所以不会有重复样式的问题,也就减少产物的体积

4. 编译期检查语法错误

非法 JS 值和错误的 CSS 语法,都会在编译期检查出来,而不用等到运行时才暴露出来。这意味着你不会在生产模式遇到这些低级错误,同样的, Linaria 支持 stylelint ,你依旧可以获得原来一样的 lint 体验。

5. 熟悉的 CSS 语法

不同于其他的一些 JSS 框架, Linaria 语法只是支持嵌套的 CSS 原生语法而已,没有什么上手成本。完美支持面向 Copy-Paste 编程

6. 支持无 JavaScript 运行

如果你的网站需要在禁止 JavaScript 的情况运行,或者想要在编译生成静态网页的运行, Linaria 同样可以在这些情况下正常运行,因为它的 没有运行时 特性

安装配置

Linaria 同时支持 Webpack , Rollup 和 Sevlte ,本文只会讲述配合 Webpack 使用的方式,如有其他需求,可以去官网看下配合其他构建工具的文档

首先

如果你在项目里使用里 Babel 做转译或这 polyfill 什么的,那么一定要在根项目下建立一个 Babel 配置文件,把你需要的 presets 和 plugin 写在里面,不然 Linaria 会无法解析你的代码

和 Webpack 配合

和 Webpack 配合很容易,只需要将 babel-loader 加上 linaria/loader 即可,一定确保 linaria/loader 紧挨着且在 babel-loader 之后

{
  test: /\.js$/,
  use: [
    { loader: 'babel-loader' },
    {
      loader: 'linaria/loader',
      options: {
        sourceMap: process.env.NODE_ENV !== 'production',
      },
    }
  ],
}

此外,为了将收集到的样式抽取出来,你需要另外一个 Webpack 插件 mini-css-extract-plugin , 执行 npm i -D css-loader mini-css-extract-plugin 来安装

然后导入 mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

然后设置相应的解析规则和插件

{
  test: /\.css$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        hmr: process.env.NODE_ENV !== 'production',
      },
    },
    {
      loader: 'css-loader',
      options: {
        sourceMap: process.env.NODE_ENV !== 'production',
      },
    },
  ],
},

mini-css-extract-plugin 加入 Webpack 配置的 plugins 属性中

new MiniCssExtractPlugin({
  filename: 'styles.css',
});

你可以通过 HTMLWebpackPlugin 插件来将抽离出来的 CSS 文件与构建产生的 html 文件连接起来,对于生产模式,你也许需要将哈希值设置的 CSS 文件名上:

new MiniCssExtractPlugin({
  filename: 'styles-[contenthash].css',
});

因为 Linaria 会抽离出来的样式文件过一遍 Webpack .css 规则的 loader 流水线,所以你可以很容易去使用 postcssclean-css 来做一些定制化操作

一个完整的 Webpack 配置例子

const webpack = require('webpack');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const dev = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: dev ? 'development' : 'production',
  devtool: 'source-map',
  entry: {
    app: './src/index',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/',
    filename: '[name].bundle.js',
  },
  optimization: {
    noEmitOnErrors: true,
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV) },
    }),
    new MiniCssExtractPlugin({ filename: 'styles.css' }),
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          { loader: 'babel-loader' },
          {
            loader: 'linaria/loader',
            options: { sourceMap: dev },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: process.env.NODE_ENV !== 'production',
            },
          },
          {
            loader: 'css-loader',
            options: { sourceMap: dev },
          },
        ],
      },
      {
        test: /\.(jpg|png|gif|woff|woff2|eot|ttf|svg)$/,
        use: [{ loader: 'file-loader' }],
      },
    ],
  },
  devServer: {
    contentBase: [path.join(__dirname, 'public')],
    historyApiFallback: true,
  },
};

你可以使用下面命令来安装所有必须的 npm 库

npm i -D webpack webpack-cli webpack-dev-server mini-css-extract-plugin css-loader file-loader babel-loader

定制 linaria/loader 的 options 属性

你可以这样来传递 options 属性

{
  loader: 'linaria/loader',
  options: {
    sourceMap: false, // 是否产生 CSS source map,默认 false
    cacheDirectory: '.linaria-cache', // 缓存所在文件见,默认 .linaria-cache
		extension: '.linaria.css', // CSS 文件处于中间态时的命名,默认 .linaria.css
		preprocessor: 'stylis', // 定义 css 的预处理器,默认为 stylis

  },
}

使用方式

Linaria 使用起来非常简单,只有一个核心方法 `css``` ,基本上可以覆盖所有场景了,同时为了方便开发也提供了一些语法糖性质的辅助函数

浏览器端 API

css```

css 是一个 标签函数 ,这意味着你可以通过模版字符串 ```` 而非 () 来调用这个函数,标签函数求值结果会被 Babel 插件转换成一个独一无二的 class 命名

import { css } from 'linaria';

const flower = css`
  display: inline;
  color: violet;
`;

// flower === flower__9o5awv –> with babel plugin

任何在模版字符串写的 CSS 样式都会局限在相应的 class 命名下,包括媒体查询和动画。我们可以这样声明动画:

import { css } from 'linaria';

const box = css`
  animation: rotate 1s linear infinite;

  @keyframes rotate {
    { from: 0deg; }
    { to: 360deg; }
  }
`;

cx(...classNames: Array<string | false | void | null | 0>) => string

cx() 会对传入的字符串进行拼接,但会忽略掉 Falsy 值,比如 ''nullundefined

import { css, cx } from 'linaria';

const cat = css`
  font-weight: bold;
`;

const yarn = css`
  color: violet;
`;

const fun = css`
  display: flex;
`;

function App({ isPlaying }) {
  return <Playground className={cx(cat, yarn, isPlaying && fun)} />;
}

cx() 这个函数看着很像一个流行库 classnames ,但还是有点区别的, cx() 不处理对象

styled

一个用于快速创建 React 组件的辅助对象,它的使用形式很像 styled-components

styled 的使用方式和 css 很相似,除此之外,你可以在模版字符串中插入函数来获取组件的 props ,并动态的设置样式

import { styled } from 'linaria/react';
import colors from './colors.json';

const Container = styled.div`
  background-color: ${colors.background};
  color: ${props => props.color};
  width: ${100 / 3}%;
  border: 1px solid red;

  &:hover {
    border-color: blue;
  }
`; 

同样的,所有的样式规则也是局部化的。为了避免重复的 CSS 样式代码,我们可以这样去引用别的样式

const Title = styled.h1`
  font-size: 36px;
`;

const Article = styled.article`
  font-size: 16px;

  /* this will evaluate to the selector that refers to `Title` */
  ${Title} {
    margin-bottom: 24px;
  }
`;

并且,我们可以通过 as 属性来指定实际渲染时的 html 标签是什么

// Here `Button` is defined as a `button` tag
const Button = styled.button`
  background-color: rebeccapurple;
`;

// You can switch it to use an `a` tag with the `as` prop
<Button as="a" href="/get-started">
  Click me
</Button>;

styled 也支持类似高阶组件形式的样式嵌套

const Button = styled.button`
  background-color: rebeccapurple;
`;

// The background-color in FancyButton will take precedence
const FancyButton = styled(Button)`
  background-color: black;
`;

服务端 API (linaria/server)

collect(html: string, css: string) => string

在做 SSR 时我们不仅需要将相应的 HTML 代码进行返回,也需要将 需要的 样式代码返回,这就那些 关键的 的 CSS 代码,我们可以通过利用 collect() 函数来抽离出关键的 CSS 代码

import { collect } from 'linaria/server';

const css = fs.readFileSync('./dist/styles.css', 'utf8');
const html = ReactDOMServer.renderToString(<App />);
const { critical, other } = collect(html, css);

// critical – returns critical CSS for given html
// other – returns the rest of styles

collect() 会根据元素的 class 属性,将用到的 CSS 代码抽离出来,这样就可以跟随 html 一起返回

需要注意的被抽离出来的 css 代码选择器的顺序会变乱掉,这使得如果你的样式依赖选择器顺序的权重,可能就会出现意料的之外的错误,不过由于 Linaria 生成的 class 命名都是唯一的,所以一般不会出现这个问题,但与其他的库协作时需要注意到这点

警告

Linaria 是基于 CSS 变量的,大部分现代浏览器支持这个特性,但是对于 IE 11 以及以下,是不支持的,所以如果你需要支持 IE 11 ,也许 Linaria 不是你最好的选择

后续

linaria/lader 已经改名为 @linaria/webpack4-loader