编译型 JSS 框架 Linaria 的原理

Linaria是一个编译期+运行时的JSS框架,与传统运行时方案不同,它在编译阶段解析CSS,生成单独的CSS文件并用哈希字符串替换JSS代码。本文探讨了Linaria的编译流程,包括如何通过webpack和babel配置,以及如何动态执行JS代码来解析和提取CSS。最后,总结了Linaria如何将CSS拆分到单独文件并利用webpack生态进行后续处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linaria 是一个近似于 styled-componentsemotion JSS 框架,不同点在于, styled-componentsemotion 是一个 运行时 方案,而 Linaria 是一个 编译期 + 运行时 方案。

运行时的 JSS 方案必须内置一个 CSS 处理器,并且在运行时去解析,分别增加了体积和性能上的成本。 Linaria 创造性的在编译期将相应的 JSS 解析出来,抽出解压到一个 CSS 文件中,并将相应的 JSS 代码替换成一个指向某个 css 类名的字符串,避免了运行时方案的问题。

本文只做原理探讨,使用介绍相关请看 《Linaria 也许是现在 React 最佳的 JSS 方案》

总览

对于以下代码

import { css } from '@linaria/core'

let size = 5;
size = (function() { return 3; }());

const headerClassName = css`
  text-align: center;
  color: #fff;
`;

const headerTitleClassName = css`
  .${headerClassName} {
    font-size: ${size}px;
  }
`;

console.log(headerClassName, headerTitleClassName);

Linaria 会将其编译为

// index.bundle.js
var size = 5;

size = function () {
  return 3;
}();

var headerClassName = "hf71da1";
var headerTitleClassName = "hi1y09m";
console.log(headerClassName, headerTitleClassName);
/* index.css */
.hf71da1{text-align:center;color:#fff;}
.hi1y09m .hf71da1{font-size:3px;}

通过编译后的代码我们可以看出:

  • 相关 JSS 代码在编译后被抽离解压到一个 .css 文件中了
  • 对应 JSS 代码被替换成了一个哈希字符串,这个字符串代表某个样式表的类名
  • 编译时解压并非是静态解析,而是动态执行,可以看到样式表 font-size 的值为 3 而非 size 变量的初始值

原理

Linaria 的实现依赖于 wbepack(rollup) 和 babel 是必然的,使用 Linaria 必须在 webpack 和 babel 分别设置 @linaria/webpack4-loader@linaria/babel 才可以。

流程

通过 webpack 使用 Linaria 需要 .rules 上进行如下配置

{
  test: /.(js|ts)$/,
  use: [
    'babel-loader',
    '@linaria/webpack4-loader''
  ],
},

所有的 JS 代码都会经过 @linaria/webpack4-loader ,其核心代码如下

export default function index(
  this: LoaderContext,
  sourceCodes: string,
  inputSourceMap: RawSourceMap | null
) {

  result = transform(sourceCodes, {
    filename: path.relative(process.cwd(), this.resourcePath),
    inputSourceMap: inputSourceMap ?? undefined,
    outputFilename,
    pluginOptions: rest,
    preprocessor,
  });

  if (result.cssText) {
    let { cssText } = result;

    if (sourceMap) {
      cssText += `/*# sourceMappingURL=data:application/json;base64,${Buffer.from(
        result.cssSourceMapText || ''
      ).toString('base64')}*/`;
    }

    if (result.dependencies?.length) {
      result.dependencies.forEach((dep) => {
        try {
          const f = resolveSync(path.dirname(this.resourcePath), dep);

          this.addDependency(f);
        } catch (e) {
          // eslint-disable-next-line no-console
          console.warn(`[linaria] failed to add dependency for: ${dep}`, e);
        }
      });
    }

    this.callback(
      null,
      `${result.code}\n\nrequire(${loaderUtils.stringifyRequest(
        this,
        outputFilename
      )});`,
      result.sourceMap ?? undefined
    );
    return;
  }

是一个标准的 webpack 异步 loader ,@linaria/webpack4-loader 做的事情就是把传入的源代码当做参数传入、调用 transform 函数,这个函数来自 @linaria/babel@linaria/babel 是 Linaria 维护的解析 JSS 代码的 babel-preset

export default function transform(code: string, options: Options): Result {
  // Check if the file contains `css` or `styled` words first
  // Otherwise we should skip transforming
  if (!/\b(styled|css)/.test(code)) {
    return {
      code,
      sourceMap: options.inputSourceMap,
    };
  }
  // ...  
  const ast = parseSync(code, {
    ...babelOptions,
    filename: options.filename,
    caller: { name: 'linaria' },
  });

  const { metadata, code: transformedCode, map } = transformFromAstSync(
    ast!,
    code,
    //.....
  )!;

  const {
    rules,
    replacements,
    dependencies,
  } = (metadata as babel.BabelFileMetadata & {
    linaria: LinariaMetadata;
  }).linaria;
  const mappings: Mapping[] = [];

  let cssText = '';

  Object.keys(rules).forEach((selector, index) => {
    mappings.push({
      generated: {
        line: index + 1,
        column: 0,
      },
      original: rules[selector].start!,
      name: selector,
      source: '',
    });

    // Run each rule through stylis to support nesting
    cssText += `${preprocessor(selector, rules[selector].cssText)}\n`;
  });

  return {
    code: transformedCode || '',
    cssText,
    rules,
    replacements,
    dependencies,
    sourceMap: map
  };
}

transform 内部首先会判断代码是否使用 Linaria 的 API ,如果使用了则解析相应的 JSS 代码,**并执行相关的 JS 代码,**这也是为什么本文开头例子处编译后样式表的 font-size 的值为 3 而非 5 的原因。解析的时候,@linaria/babel 会将 JSS 中的 css 代码写到 metadata 里,而非转化后的源代码处,随后交付给 @linaria/webpack4-loader 处理。

metadata 是 babel 解析、转译代码时用于储存一些辅助信息的地方,其内容适用于辅助转译流程的,转译完成后就不存在了

也就是说,@linaria/babel 只负责将 JSS 代码根据文件路径计算出一个哈希名,作为类名,替换相应的JSS 代码。 css 解压相关的操作由 @linaria/webpack4-loader 负责。

@linaria/webpack4-loader 会将 metadata 里 css 解压到 .linaria-cache/index.linaria.css 里,然后在转化后的源代码末尾处加上 require("./.linaria-cache/index.linaria.css") ,随后 webpack 解析时就会将解析 .css 文件相关操作委托到处理 .css 文件的 loader ,此时我们可以自由选择使用 mini-css-extract-plugin 还是 style-loader ,或者其他的 .css loader ,重点在于我们拥有了 webpack.css 文件相关的生态,而其他的 JSS 方案就没法做到这点。

总结

  1. JS 代码经由 @linaria/webpack4-loader
  2. @linaria/webpack4-loader 内部使用 @linaria/babel 解析、执行 JS 代码
  3. @linaria/babel 将 JSS 编译成根据文件路径生成的类名,将 css 代码写入到生成的 metadata ,将其交付到 @linaria/webpack4-loader
  4. @linaria/webpack4-loadermetadata 的 css 代码解压到 /.linaria-cache 文件中
  5. @linaria/webpack4-loader 在解析后代码末尾处加上 require("./.linaria-cache/index.linaria.css") ,交由 webpack 进行后续的处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值