背景
- 页面中存在大量重复冗余的css样式
- 组件中难以维护、优化
实现 Tailwind CSS 的任意值语法
webpack 自定义插件
const chalk = require('chalk')
class AtomicCSSPlugin {
constructor(options = {}) {
this.options = {
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
include: /src/,
...options,
}
}
apply(compiler) {
const { test, exclude } = this.options
const { rules } = compiler.options.module
rules.push({
test,
exclude,
use: [
{
loader: require.resolve('./loader'),
},
],
})
compiler.hooks.done.tap('AtomicCSSPlugin', (stats) => {
if (stats.hasErrors()) {
return
}
console.log(`${chalk.green('[AtomicCSSPlugin]')}: 任意值语法处理完成`)
})
}
}
module.exports = AtomicCSSPlugin
const fs = require('fs')
const path = require('path')
const { parseContent } = require('./parser')
module.exports = function (source) {
const { cssRules, transformedContent } = parseContent(source)
if (cssRules.length === 0) {
return source
}
const cssContent = cssRules.join('\n')
const styleId = `atomic-styles-${Math.random().toString(36).substring(2, 9)}`
if (process.env.TARO_ENV === 'h5') {
const styleTag = `
const style = document.createElement('style');
style.id = '${styleId}';
style.textContent = \`${cssContent}\`;
const existingStyle = document.getElementById('${styleId}');
if (existingStyle) {
existingStyle.parentNode.removeChild(existingStyle);
}
if (document.head) {
document.head.appendChild(style);
}
`
return `${styleTag}\n${transformedContent}`
}
const atomicCssPath = path.resolve(__dirname, '../../../packages/atomic/index.css')
const atomicCssDir = path.dirname(atomicCssPath)
if (!fs.existsSync(atomicCssDir)) {
fs.mkdirSync(atomicCssDir, { recursive: true })
}
fs.writeFileSync(
atomicCssPath,
`${cssContent}`
)
return transformedContent
}
const ARBITRARY_VALUE_REGEX = /([a-zA-Z0-9_-]+)-\[(.*?)\]/g
const PROPERTY_MAP = {
p: 'padding',
pt: 'padding-top',
pr: 'padding-right',
pb: 'padding-bottom',
pl: 'padding-left',
px: ['padding-left', 'padding-right'],
py: ['padding-top', 'padding-bottom'],
m: 'margin',
mt: 'margin-top',
mr: 'margin-right',
mb: 'margin-bottom',
ml: 'margin-left',
mx: ['margin-left', 'margin-right'],
my: ['margin-top', 'margin-bottom'],
w: 'width',
h: 'height',
'min-w': 'min-width',
'min-h': 'min-height',
'max-w': 'max-width',
'max-h': 'max-height',
size: ['width', 'height'],
text: 'font-size',
leading: 'line-height',
tracking: 'letter-spacing',
indent: 'text-indent',
bg: 'background-color',
color: 'color',
border: 'border-color',
'bg-size': 'background-size',
rounded: 'border-radius',
'rounded-t': ['border-top-left-radius', 'border-top-right-radius'],
'rounded-r': ['border-top-right-radius', 'border-bottom-right-radius'],
'rounded-b': ['border-bottom-left-radius', 'border-bottom-right-radius'],
'rounded-l': ['border-top-left-radius', 'border-bottom-left-radius'],
opacity: 'opacity',
z: 'z-index',
top: 'top',
right: 'right',
bottom: 'bottom',
left: 'left',
gap: 'gap',
}
function generateCSSRule(selector, properties, value) {
const values = value.includes('_') ? value.split('_') : [value]
if (Array.isArray(properties)) {
let declarations
if (values.length > 1 && properties.length === values.length) {
declarations = properties.map((prop, index) => `${prop}: ${values[index]};`).join(' ')
} else {
declarations = properties.map((prop) => `${prop}: ${values[0]};`).join(' ')
}
return `.${selector} { ${declarations} }`
}
const cssValue = values.length > 1 ? values.join(' ') : value
return `.${selector} { ${properties}: ${cssValue}; }`
}
function transformClassName(className) {
return className.replace(/\[(.*?)\]/g, (match, p1) => {
const specialCharsMap = {
'#': 'hex_',
'(': '_lp_',
')': '_rp_',
',': '_c_',
'.': '_d_',
'%': '_pct_',
}
let value = p1
Object.entries(specialCharsMap).forEach(([char, replacement]) => {
if (value.includes(char)) {
value = value.replace(new RegExp(`\\${char}`, 'g'), replacement)
}
})
value = value.replace(/\s+/g, '_s_')
return `_${value}_`
})
}
function parseContent(content) {
const matches = []
const cssRules = []
const transformedClasses = {}
let match
ARBITRARY_VALUE_REGEX.lastIndex = 0
while ((match = ARBITRARY_VALUE_REGEX.exec(content)) !== null) {
const [fullMatch, property, value] = match
if (PROPERTY_MAP[property]) {
const transformedClassName = transformClassName(fullMatch)
transformedClasses[fullMatch] = transformedClassName
const cssRule = generateCSSRule(
transformedClassName,
PROPERTY_MAP[property],
value
)
if (!cssRules.includes(cssRule)) {
cssRules.push(cssRule)
}
matches.push({
original: fullMatch,
transformed: transformedClassName,
})
}
}
let transformedContent = content
Object.entries(transformedClasses).forEach(([original, transformed]) => {
transformedContent = transformedContent.replaceAll(original, transformed)
})
return {
matches,
cssRules: [...new Set(cssRules)],
transformedContent,
}
}
module.exports = {
parseContent,
ARBITRARY_VALUE_REGEX,
PROPERTY_MAP,
transformClassName,
}
注册webpack插件
mini:{
webpackChain(chain) {
chain.plugin('AtomicCSSPlugin').use(AtomicCSSPlugin, [{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
include: /src/,
}])
},
}
注意点
- 小程序中class会过滤掉一些特殊字符变成空格([ =>' ')
- 小程序中不支持动态创建css文件所以要将动态生成css写入到产物的common.wxss
- 注意css权重问题 原子式css权重很低容易被覆盖
页面使用
<View className="fixed top-[0] left-[0] p-[24px] m-[16px] w-[200px] bg-[#ff0000]">
内容
</View>
