当前位置: 首页 > 工具软件 > linaria > 使用案例 >

编译型 JSS 框架 Linaria 的原理

张嘉
2023-12-01

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 进行后续的处理
 类似资料: