Linaria 是一个近似于 styled-components
和 emotion
JSS 框架,不同点在于, styled-components
和 emotion
是一个 运行时 方案,而 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;}
通过编译后的代码我们可以看出:
.css
文件中了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 方案就没法做到这点。
@linaria/webpack4-loader
@linaria/webpack4-loader
内部使用 @linaria/babel
解析、执行 JS 代码@linaria/babel
将 JSS 编译成根据文件路径生成的类名,将 css 代码写入到生成的 metadata
,将其交付到 @linaria/webpack4-loader
@linaria/webpack4-loader
将 metadata
的 css 代码解压到 /.linaria-cache
文件中@linaria/webpack4-loader
在解析后代码末尾处加上 require("./.linaria-cache/index.linaria.css")
,交由 webpack 进行后续的处理