基于postCss的TaiWindCss源码解析

前言

从最初看到TaiWindCss这个库的时候,单纯从css上看,这块跟自前接触到的bootstrap差不多,就是纯粹的css 样式库而已,但是深入了解之后,发现这个比bootstrap使用的更加方便,也很细,唯一感觉麻烦就是,每次的样式都需要自己去DIV组装,包括一系列hover,active等效果。后来深入查看了解TaiWindCss,发现这css 样式库,竟然是用js代码写出来,然后编译出来的CSS文件样式库。当时就把我给震惊了,js还能这样的操作。有点厉害!!!于是我这边就去细细的去了解并查看TaiWindCss源码。去深究这块的实现逻辑。

了解 postCss

什么 是 postCss?

通俗的讲法:postCss 就是一个开发工具,是一个用 JavaScript 工具和插件转换 CSS 代码的工具。支持变量,混入,未来 CSS 语法,内联图像等等。

它具备以下特性与常见的功能:

  1. 增强代码的可读性:Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
  2. 将未来的 CSS 特性带到今天!:帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills
  3. 终结全局 CSS:CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。
  4. 避免 CSS 代码中的错误:通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS
  5. 可以作为预处理器使用,类似:Sass, Less 和 Stylus。但是 PostCSS 是模块化的工具,比之前那些快3-30 倍,而且功能更强大。并演化出了一系列的插件来使用。

postCss的核心原理/工作流

PostCSS 包括 CSS 解析器,CSS 节点树 API,一个源映射生成器和一个节点树 stringifier。

PostCSS 主要的原理核心工作流:

  1. 通过 fs 读取CSS文件
  2. 通过 parser 将CSS解析成抽象语法树(AST树)
  3. 将AST树”传递”给任意数量的插件处理
  4. 诸多插件进行数据处理。插件间传递的数据就是AST树
  5. 通过 stringifier 将处理完毕的AST树重新转换成字符串

这一系列的工作流,讲简单一点就是对数据进行一系列的操作。为此PostCSS提供了一系列的数据操作API。比如:walkAtRules ,walkComments,walkDecls,walkRules等相关API, 具体相关文档可查看:
postcss官方API文档

TaiWindCss 源码解析

TaiWindCss 是什么?

从TaiWindCss时间的使用场景上来看,我们以 PostCSS 插件的形式安装TaiWindCss,而本质上讲TaiWindCss是一个postCss的插件,我们目前在实际项目开发的角度上看,我们目前都已经普遍使用PostCSS,目前大环境下的大多数的流行/主流的框架基本都默认使用了 PostCSS,例如 autoprefixer. 这个我们就基本上都会使用。

对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:

  1. PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
  2. TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
  3. 项目 引入的 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') })
  1. substituteTailwindAtRules 转换AST数据操作
  2. evaluateTailwindFunctions 主题配置操作
  3. substituteVariantsAtRules 变量递归规则操作
  4. substituteResponsiveAtRules 常规Responsive规则逻辑操作
  5. convertLayerAtRulesToControlComments 内容编辑描述操作
  6. substituteScreenAtRules 样式 Screen 规则操作
  7. substituteClassApplyAtRules 标识 @apply 逻辑处理
  8. applyImportantConfiguration 传参 important 是否添加逻辑处理
  9. 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的开发实践。

参考资料

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值