基于postCss的TaiWindCss源码解析
前言
从最初看到TaiWindCss这个库的时候,单纯从css上看,这块跟自前接触到的bootstrap差不多,就是纯粹的css 样式库而已,但是深入了解之后,发现这个比bootstrap使用的更加方便,也很细,唯一感觉麻烦就是,每次的样式都需要自己去DIV组装,包括一系列hover,active等效果。后来深入查看了解TaiWindCss,发现这css 样式库,竟然是用js代码写出来,然后编译出来的CSS文件样式库。当时就把我给震惊了,js还能这样的操作。有点厉害!!!于是我这边就去细细的去了解并查看TaiWindCss源码。去深究这块的实现逻辑。
了解 postCss
什么 是 postCss?
通俗的讲法:postCss 就是一个开发工具,是一个用 JavaScript 工具和插件转换 CSS 代码的工具。支持变量,混入,未来 CSS 语法,内联图像等等。
它具备以下特性与常见的功能:
- 增强代码的可读性:Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
- 将未来的 CSS 特性带到今天!:帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills
- 终结全局 CSS:CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。
- 避免 CSS 代码中的错误:通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS
- 可以作为预处理器使用,类似:Sass, Less 和 Stylus。但是 PostCSS 是模块化的工具,比之前那些快3-30 倍,而且功能更强大。并演化出了一系列的插件来使用。
postCss的核心原理/工作流
PostCSS 包括 CSS 解析器,CSS 节点树 API,一个源映射生成器和一个节点树 stringifier。
PostCSS 主要的原理核心工作流:
- 通过 fs 读取CSS文件
- 通过 parser 将CSS解析成抽象语法树(AST树)
- 将AST树”传递”给任意数量的插件处理
- 诸多插件进行数据处理。插件间传递的数据就是AST树
- 通过 stringifier 将处理完毕的AST树重新转换成字符串
这一系列的工作流,讲简单一点就是对数据进行一系列的操作。为此PostCSS提供了一系列的数据操作API。比如:walkAtRules ,walkComments,walkDecls,walkRules等相关API, 具体相关文档可查看:
postcss官方API文档
TaiWindCss 源码解析
TaiWindCss 是什么?
从TaiWindCss时间的使用场景上来看,我们以 PostCSS 插件的形式安装TaiWindCss,而本质上讲TaiWindCss是一个postCss的插件,我们目前在实际项目开发的角度上看,我们目前都已经普遍使用PostCSS,目前大环境下的大多数的流行/主流的框架基本都默认使用了 PostCSS,例如 autoprefixer. 这个我们就基本上都会使用。
对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:
- PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
- TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
- 项目 引入的 less,sass,css 文件中注入 @tailwind 标识,并引入 base,components,utilities,是否全部引入取决你自己。
相关配置完成之后,在项目打包或者热更新的过程中,执行 PostCSS一系列插件, 自动把我们页面上引用的相关css,进行打包成我们需要的css文件,然后加载到我们的页面中。
因为我们使用了碎片化的样式文件布局,比如页面上的代码
class="col-start-1 row-start-1 flex sm:col-start-2 sm:row-span-3"
这些 class 类型,高度颗粒化,高度复用,而 TailwindCss 在构建生产文件时会自动删除所有未使用的 CSS,这意味着您最后的 CSS 文件可能会是最小的。
实现基本原理
首先我们明确清楚postCss的工作流:
大致步骤:
- 将CSS解析成抽象语法树(AST树)
- 将AST树”传递”给任意数量的插件处理
- 将处理完毕的AST树重新转换成字符串
在PostCSS中有几个关键的处理机制:
Source string → Tokenizer → Parser → AST → Processor → Stringifier
那么TaiWindCss也是遵循这样的类似的一个工作流:
基本的步骤:
- 将CSS解析成抽象语法树(AST树)
- 读取插件配置,根据配置文件,生成新的抽象语法树
- 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
- 清除一系列操作留下的数据痕迹
- 将处理完毕的AST树重新转换成字符串
举例说明:
当前代码块如下:
@layer components{
@variants responsive{
.container{
width: 100%
}
}
}
转换后的AST代码块如下:
{
"raws": {
"semicolon": false,
"after": "\n\n"
},
"type": "root",
"nodes": [
{
"raws": {
"before": "",
"between": "",
"afterName": " ",
"semicolon": false,
"after": "\n"
},
"type": "atrule",
"name": "layer",
"source": {
"start": {
"line": 1,
"column": 1
},
"input": {
"css": "@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n }\n}\n\n",
"hasBOM": false,
"id": "<input css 17>"
},
"end": {
"line": 7,
"column": 1
}
},
"params": "components",
"nodes": [
{
"raws": {
"before": "\n ",
"between": "",
"afterName": " ",
"semicolon": false,
"after": "\n "
},
"type": "atrule",
"name": "variants",
"source": {
"start": {
"line": 2,
"column": 3
},
"input": {
"css": "@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n }\n}\n\n",
"hasBOM": false,
"id": "<input css 17>"
},
"end": {
"line": 6,
"column": 3
}
},
"params": "responsive",
"nodes": [
{
"raws": {
"before": "\n ",
"between": "",
"semicolon": false,
"after": "\n "
},
"type": "rule",
"nodes": [
{
"raws": {
"before": "\n ",
"between": ": "
},
"type": "decl",
"source": {
"start": {
"line": 4,
"column": 7
},
"input": {
"css": "@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n }\n}\n\n",
"hasBOM": false,
"id": "<input css 17>"
},
"end": {
"line": 4,
"column": 17
}
},
"prop": "width",
"value": "100%"
}
],
"source": {
"start": {
"line": 3,
"column": 5
},
"input": {
"css": "@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n }\n}\n\n",
"hasBOM": false,
"id": "<input css 17>"
},
"end": {
"line": 5,
"column": 5
}
},
"selector": ".container"
}
]
}
]
}
],
"source": {
"input": {
"css": "@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n }\n}\n\n",
"hasBOM": false,
"id": "<input css 17>"
},
"start": {
"line": 1,
"column": 1
}
}
}
树形结构图:
反复的数据操作,新增删除添加,都是对root -> nodes -> atrule / rule / Comment / Container / Declaration -> nodes 数组,往里面新增数据。这一系列的操作用的postCss文档提供的一系列方法。
如果我们想Css文件变成如下代码:
@layer components{
@variants responsive{
.container{
width: 100%
}
.c {
color: red
}
}
}
那么我们只需把下面的数据:
{
"raws":{
"before":"\n ",
"between":" ",
"semicolon":false,
"after":"\n "
},
"type":"rule",
"nodes":[
{
"raws":{
"before":"\n \t",
"between":": "
},
"type":"decl",
"source":{
"start":{
"line":7,
"column":6
},
"input":{
"css":"@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n .c {\n \tcolor: red\n }\n \n }\n}\n\n",
"hasBOM":false,
"id":"<input css 37>"
},
"end":{
"line":7,
"column":15
}
},
"prop":"color",
"value":"red"
}
],
"source":{
"start":{
"line":6,
"column":5
},
"input":{
"css":"@layer components{\n @variants responsive{\n .container{\n width: 100%\n }\n .c {\n \tcolor: red\n }\n \n }\n}\n\n",
"hasBOM":false,
"id":"<input css 37>"
},
"end":{
"line":8,
"column":5
}
},
"selector":".c"
}
插入到 root -> nodes -> atrule / rule / Comment / Container / Declaration -> nodes 下面的数据即可。
得到新的树形图如下:
图中,新增的目标就是我们上次出入数据之后,反馈出来的AST树形图机构。
从上述内容,我们基本上就了解TaiWindCss的实现基本原理了。其实就是一个对数据流的一系列操作过程,得到最终我们想要的 CSS 模块,然后再剔除掉多余的代码,转换成我们想要的CSS文件。
源码文件解读
TaiWindCss的github地址: TaiWindCss github 地址
package.json
首先查看package.json文件,我们找到scripts,代码如下:
"scripts": {
"prebabelify": "rimraf lib",
"babelify": "babel src --out-dir lib --copy-files",
"rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js",
"prepublishOnly": "npm run babelify && babel-node scripts/build.js",
"style": "eslint .",
"test": "jest",
"posttest": "npm run style",
"compat": "node scripts/compat.js --prepare",
"compat:restore": "node scripts/compat.js --restore"
},
我们 查看prepublishOnly 这一行,就是TaiWindCss源码构建的入口点。
找到项目文件 scripts/build.js,核心代码如下:
import tailwind from '..'
function buildDistFile(filename, config = {}, outFilename = filename) {
return new Promise((resolve, reject) => {
fs.readFile(`./${filename}.css`, (err, css) => {
if (err) throw err
return postcss([tailwind(config), require('autoprefixer')])
.process(css, {
from: `./${filename}.css`,
to: `./dist/${outFilename}.css`,
})
.then((result) => {
fs.writeFileSync(`./dist/${outFilename}.css`, result.css)
return result
})
.then((result) => {
const minified = new CleanCSS().minify(result.css)
fs.writeFileSync(`./dist/${outFilename}.min.css`, minified.styles)
})
.then(resolve)
.catch((error) => {
console.log(error)
reject()
})
})
})
}
这个核心代码描述就是传递文件名(base),读取项目中的css文件,经过postcss插件tailwindcss进行转换的css文件。然后到处Css文件。
值得注意的是
import tailwind from '..'
这里的tailwind,其实就是package.json里面的对应的
"main": "lib/index.js",
读取相关配置文件
文件目录 src -> index.js
核心代码如下:
const plugin = postcss.plugin('tailwindcss', (config) => {
const plugins = []
const resolvedConfigPath = resolveConfigPath(config)
if (!_.isUndefined(resolvedConfigPath)) {
plugins.push(registerConfigAsDependency(resolvedConfigPath))
}
console.log('plugins:', plugins)
return postcss([
...plugins,
processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),
formatCSS,
])
})
定义tailwindcss命名的postCss插件,去解析css文件。
页面上引用的:
import getAllConfigs from './util/getAllConfigs'
import { defaultConfigFile } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'
就是项目初始化的一系列配置文件。其中核心的读取配置文件就是 tailwind.config.js , 而这个就是我们再使用tailwindcss的时候,需要去对我们的引用进行配置化管理的文件。
读取配置的文件,主要是对AST的数据结构进行初始化与替换操作。这个文件再后续的逻辑处理中都是作为参数的形式调用的。
主要入口处理逻辑
src 文件下面的 processTailwindFeatures.js,这个文件是所有操作业务逻辑的入口文件,还有因为PostCss版本不同的引用,核心的入口都是这个文件,一系列逻辑操作代码如下:
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions(config),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),
applyImportantConfiguration(config),
purgeUnusedStyles(config, configChanged),
]).process(css, { from: _.get(css, 'source.input.file') })
- substituteTailwindAtRules 转换AST数据操作
- evaluateTailwindFunctions 主题配置操作
- substituteVariantsAtRules 变量递归规则操作
- substituteResponsiveAtRules 常规Responsive规则逻辑操作
- convertLayerAtRulesToControlComments 内容编辑描述操作
- substituteScreenAtRules 样式 Screen 规则操作
- substituteClassApplyAtRules 标识 @apply 逻辑处理
- applyImportantConfiguration 传参 important 是否添加逻辑处理
- purgeUnusedStyles 删除多余的代码,添加purgecss插件,读取配置删除多余的未引用的css样式代码
10.最好导出我们项目开发所需的Css文件
核心代码
src 文件下面的 processTailwindFeatures.js,中有这样一行代码:
processedPlugins = processPlugins(
[...corePlugins(config), ..._.get(config, 'plugins', [])],
config
)
getProcessedPlugins = function () {
return {
// ...jumpUrl,
base: cloneNodes(processedPlugins.base),
components: cloneNodes(processedPlugins.components),
utilities: cloneNodes(processedPlugins.utilities),
}
}
其中核心的功能就是遍历src/plugins下面的 一些列配置文件:
'preflight',
'container',
'space',
'divideWidth',
'divideColor',
'divideStyle',
'divideOpacity',
'accessibility',
'appearance',
'backgroundAttachment',
'backgroundClip',
'backgroundColor',
'backgroundImage',
'gradientColorStops',
'backgroundOpacity',
'backgroundPosition',
'backgroundRepeat',
'backgroundSize',
'borderCollapse',
'borderColor',
'borderOpacity',
'borderRadius',
'borderStyle',
'borderWidth',
'boxSizing',
'cursor',
'display',
'flexDirection',
'flexWrap',
'placeItems',
'placeContent',
'placeSelf',
'alignItems',
'alignContent',
'alignSelf',
'justifyItems',
'justifyContent',
.....
遍历这些配置生成代码就是util -> processPlugins.js 里面的代码:
handler({
postcss,
config: getConfigValue,
theme: (path, defaultValue) => {
const [pathRoot, ...subPaths] = _.toPath(path)
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
return transformThemeValue(pathRoot)(value)
},
corePlugins: (path) => {
if (Array.isArray(config.corePlugins)) {
return config.corePlugins.includes(path)
}
return getConfigValue(`corePlugins.${path}`, true)
},
variants: (path, defaultValue) => {
if (Array.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
e: escapeClassName,
prefix: applyConfiguredPrefix,
addUtilities: (utilities, options) => {
const defaultOptions = { variants: [], respectPrefix: true, respectImportant: true }
options = Array.isArray(options)
? Object.assign({}, defaultOptions, { variants: options })
: _.defaults(options, defaultOptions)
const styles = postcss.root({ nodes: parseStyles(utilities) })
styles.walkRules((rule) => {
if (options.respectPrefix && !isKeyframeRule(rule)) {
rule.selector = applyConfiguredPrefix(rule.selector)
}
if (options.respectImportant && config.important) {
rule.__tailwind = {
...rule.__tailwind,
important: config.important,
}
}
})
pluginUtilities.push(
wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'utilities')
)
},
addComponents: (components, options) => {
const defaultOptions = { variants: [], respectPrefix: true }
options = Array.isArray(options)
? Object.assign({}, defaultOptions, { variants: options })
: _.defaults(options, defaultOptions)
const styles = postcss.root({ nodes: parseStyles(components) })
styles.walkRules((rule) => {
if (options.respectPrefix && !isKeyframeRule(rule)) {
rule.selector = applyConfiguredPrefix(rule.selector)
}
})
pluginComponents.push(
wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'components')
)
},
addBase: (baseStyles) => {
pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))
},
addVariant: (name, generator, options = {}) => {
pluginVariantGenerators[name] = generateVariantFunction(generator, options)
},
})
})
循环一些列根据配置所需要的元素,进行遍历逻辑操作,从而生成生成base,components ,utilities 文件。为数据逻辑操作生成原始数据。
base 核心代码
base的核心操作代码如下:
export default function () {
return function ({ addBase }) {
const normalizeStyles = postcss.parse(
fs.readFileSync(require.resolve('modern-normalize'), 'utf8')
)
const preflightStyles = postcss.parse(fs.readFileSync(`${__dirname}/css/preflight.css`, 'utf8'))
addBase([...normalizeStyles.nodes, ...preflightStyles.nodes])
}
}
base就是一些基础样式配置,这里面引用了 modern-normalize 这个基础样式库来作为 TaiWindCss 的基础样式库,我们也可以自定义引用,比如代码中的 preflightStyles.css 文件,如果后续需要扩展 TaiWindCss的base基础库,写法类似上传代码中preflight.css代码引用即可。
utilities 核心代码
utilities 的核心操作
(1)我们已经明确css是如何的,比如我们明确字体样式向左,向右,居中等,那么代码如下:
export default function () {
return function ({ addUtilities, variants }) {
addUtilities(
{
'.text-left': { 'text-align': 'left' },
'.text-center': { 'text-align': 'center' },
'.text-right': { 'text-align': 'right' },
'.text-justify': { 'text-align': 'justify' },
},
variants('textAlign')
)
}
}
(2) 我们需要根据配置文件去生成的样式文件,比如z-index 后面的值是配置化的,那么代码如下:
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return createUtilityPlugin('zIndex', [['z', ['zIndex']]])
}
components 核心代码
components 这个模板,我的理解,就是提供一系列类似组件化的代码逻辑,TaiWindCss 目前它只对 布局样式 container 做了这样的操作。主要代码如下:
onst atRules = _(minWidths)
.sortBy((minWidth) => parseInt(minWidth))
.sortedUniq()
.map((minWidth) => {
return {
[`@media (min-width: ${minWidth})`]: {
'.container': {
'max-width': minWidth,
...generatePaddingFor(minWidth),
},
},
}
})
.value()
addComponents(
[
{
'.container': Object.assign(
{ width: '100%' },
theme('container.center', false) ? { marginRight: 'auto', marginLeft: 'auto' } : {},
generatePaddingFor(0)
),
},
...atRules,
],
variants('container')
)
}
从代码逻辑上看,读取配置 screens 的值,遍历生成不同 screens 下面的 container的样式数据。然后进行归类并输出components这个样式库。当然我们也可以对这块进行扩展,比如:
addComponents({
'.btn-blue': {
backgroundColor: 'blue',
color: 'white',
padding: '.5rem 1rem',
borderRadius: '.25rem',
},
'.btn-blue:hover': {
backgroundColor: 'darkblue',
},
})
//或者
addComponents(
{
'.btn-blue': {
backgroundColor: 'blue',
},
},
['responsive', 'hover']
)
如果新增这样的插件配置,那么这些板块就是打包到components这个库里面。
这三个模板的配置化生产CSS文件,就是TaiWindCss的主要功能
插件配置
插件的功能, 给我们开发了一个入口函数,让我们可以进行配置化开发,插件的方法如下:
module.exports = {
plugins: [
plugin(function({ addUtilities, addComponents, e, prefix, config }) {
// Add your custom styles here
}),
]
}
传递的一系列参数,就是util -> processPlugins.js 里面的一些核心代码应用。
主要的核心代码:
入口文件src -> index.js 读取插件配置:
processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),
}
getConfigFunction方法,读取tailwind.config.js配置里面的插件,进行数据初始化操作,然后把这些插件写入进来,从原理上js下写的方法就是类似 plugins 文件夹下的哪些插件js文件。只是调用的方式不同而已。
具体的插件使用文档查看官网 TaiWindCss插件使用文档
脚手架
我们再使用TaiWindCss过程中,需要提供一个npm包,于是TaiWindCss内部也集成了一个TaiWindCss的使用手册(脚手架)。
查看package.json :
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
根据目录我们知道了,它主要提供三个 init (初始化),build(构建生成css ),help (帮助文档)
核心的代码在 src -> cli -> commands -> init.js help.js,build.js
具体代码很简单,就是简单的脚手架配置js, 这里就不写了,大家有兴趣可以去看源码。
总结
通过这段时间的对TaiWindCss源码的解析,阅读源码,从中学到了代码的一些好的写法,系统架构的设计思想与理念。主要还是有以下的学习成果:
- AST结构与转换原理
- 从0到1熟悉postCss,具体文档查看postCss官网
- postCss 插件的开发流程
- TaiWindCss框架的设计理念
- JS插件化/可配置化架构的设计与实现
通过这段时间的阅读与相关代码查看,让我真正熟悉并可以熟练使用postCss这个工具库,还有我觉得很重要的收获就是代码的系统设计。
以前我们的代码设计可能都是正向的,从if else 一直下去,该调用就调用,该传参就传参,设计理念都是至上而下的。
而阅读TaiWindCss的源码之后,让我觉得很受用的就是通过遍历plugins下面的一系列文件,函数传参中传递函数方法,从何获取数据,最终得到原始数据。而这样的设计就是这个架构最核心的设计理念。
这样的反向操作逻辑,代码归类很棒,收集数据通过一个循环就搞定了。函数调用传递参数,每个不同的插件集按需去使用。
这样的设计给与我通过阅读源码来获取到。从而让我感受到了阅读源码带来了一下好处,可以丰富并增长我们的开发实践与开发经验。
后续我会提供一个mini TaiWindCss的开发实践。