@Tailwindcss 源码解析

2,546 阅读4分钟

下面是解析 Tailwindcss 背后的原理,知道背后的原理,能方便我们更加好的使用 Tailwindcss,同时我们也就能具备驾驭 ACSS 原理。

前置知识

  1. 了解并熟悉使用 postcss, Tailwimdcss 本质是 postcss 的一个插件
  2. 了解 postcss api
  3. 学会书写一个 postcss 插件

目录结构分析

源码分析的版本:

// Tailwindcss 版本
"version": "1.2.0",
// 所需的 Node 版本
"engines": {
    "node": ">=8.9.0"
 }

整体目录结构

.
├── LICENSE
├── README.md
├── __tests__
├── base.css
├── components.css
├── defaultConfig.js
├── defaultTheme.js
├── dist
├── jest
├── package.json
├── plugin.js
├── resolveConfig.js
├── screens.css
├── scripts
├── src
├── stubs
├── tailwind.css
├── utilities.css
└── yarn.lock

主源码目录结构 - // src 目录

$ tree -L 2
.
├── cli
│   ├── colors.js
│   ├── commands
│   ├── compile.js
│   ├── emoji.js
│   ├── main.js
│   └── utils.js
├── cli.js
├── constants.js
├── corePlugins.js
├── index.js
├── lib
│   ├── evaluateTailwindFunctions.js
│   ├── formatCSS.js
│   ├── getModuleDependencies.js
│   ├── registerConfigAsDependency.js
│   ├── substituteClassApplyAtRules.js
│   ├── substituteResponsiveAtRules.js
│   ├── substituteScreenAtRules.js
│   ├── substituteTailwindAtRules.js
│   └── substituteVariantsAtRules.js
├── plugins
│   ├── accessibility.js
│   ├── alignContent.js
│   ├── alignItems.js
│   ├── alignSelf.js
│   ├── appearance.js
│   ├── backgroundAttachment.js
│   ├── backgroundColor.js
│   ├── backgroundPosition.js
│   ├── backgroundRepeat.js
│   ├── backgroundSize.js
│   ├── borderCollapse.js
│   ├── borderColor.js
│   ├── borderRadius.js
│   ├── borderStyle.js
│   ├── borderWidth.js
│   ├── boxShadow.js
│   ├── boxSizing.js
│   ├── clear.js
│   ├── container.js
│   ├── css
│   ├── cursor.js
│   ├── display.js
│   ├── fill.js
│   ├── flex.js
│   ├── flexDirection.js
│   ├── flexGrow.js
│   ├── flexShrink.js
│   ├── flexWrap.js
│   ├── float.js
│   ├── fontFamily.js
│   ├── fontSize.js
│   ├── fontSmoothing.js
│   ├── fontStyle.js
│   ├── fontWeight.js
│   ├── gap.js
│   ├── gridAutoFlow.js
│   ├── gridColumn.js
│   ├── gridColumnEnd.js
│   ├── gridColumnStart.js
│   ├── gridRow.js
│   ├── gridRowEnd.js
│   ├── gridRowStart.js
│   ├── gridTemplateColumns.js
│   ├── gridTemplateRows.js
│   ├── height.js
│   ├── inset.js
│   ├── justifyContent.js
│   ├── letterSpacing.js
│   ├── lineHeight.js
│   ├── listStylePosition.js
│   ├── listStyleType.js
│   ├── margin.js
│   ├── maxHeight.js
│   ├── maxWidth.js
│   ├── minHeight.js
│   ├── minWidth.js
│   ├── objectFit.js
│   ├── objectPosition.js
│   ├── opacity.js
│   ├── order.js
│   ├── outline.js
│   ├── overflow.js
│   ├── padding.js
│   ├── placeholderColor.js
│   ├── pointerEvents.js
│   ├── position.js
│   ├── preflight.js
│   ├── resize.js
│   ├── rotate.js
│   ├── scale.js
│   ├── skew.js
│   ├── stroke.js
│   ├── strokeWidth.js
│   ├── tableLayout.js
│   ├── textAlign.js
│   ├── textColor.js
│   ├── textDecoration.js
│   ├── textTransform.js
│   ├── transform.js
│   ├── transformOrigin.js
│   ├── transitionDuration.js
│   ├── transitionProperty.js
│   ├── transitionTimingFunction.js
│   ├── translate.js
│   ├── userSelect.js
│   ├── verticalAlign.js
│   ├── visibility.js
│   ├── whitespace.js
│   ├── width.js
│   ├── wordBreak.js
│   └── zIndex.js
├── processTailwindFeatures.js
└── util
    ├── buildMediaQuery.js
    ├── buildSelectorVariant.js
    ├── cloneNodes.js
    ├── configurePlugins.js
    ├── createPlugin.js
    ├── createUtilityPlugin.js
    ├── escapeClassName.js
    ├── flattenColorPalette.js
    ├── generateVariantFunction.js
    ├── increaseSpecificity.js
    ├── negateValue.js
    ├── parseObjectStyles.js
    ├── prefixNegativeModifiers.js
    ├── prefixSelector.js
    ├── processPlugins.js
    ├── resolveConfig.js
    ├── responsive.js
    └── wrapWithVariants.js

构建分析

构建结构输出,外部加载入口:

"main": "lib/index.js",

命令:

"bin": {
    "tailwind": "lib/cli.js",
    "tailwindcss": "lib/cli.js"
  },

脚本:

"scripts": {
    "prebabelify": "rimraf lib",
    "babelify": "babel src --out-dir lib --copy-files",
    "rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js",
    "prepare": "npm run babelify && babel-node scripts/build.js",
    "style": "eslint .",
    "test": "jest && eslint ."
  },

依赖:

// 开发环境依赖
"devDependencies": {
    "@babel/cli": "^7.0.0",
    "@babel/core": "^7.0.0",
    "@babel/node": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "babel-jest": "^25.1.0",
    "clean-css": "^4.1.9",
    "eslint": "^6.0.1",
    "eslint-config-postcss": "^2.0.2",
    "eslint-config-prettier": "^6.0.0",
    "eslint-plugin-prettier": "^3.0.1",
    "jest": "^25.1.0",
    "prettier": "^1.7.4",
    "rimraf": "^3.0.0"
 },
// 生成环境依赖
"dependencies": {
    "autoprefixer": "^9.4.5",
    "bytes": "^3.0.0",
    "chalk": "^3.0.0",
    "detective": "^5.2.0",
    "fs-extra": "^8.0.0",
    "lodash": "^4.17.15",
    "node-emoji": "^1.8.1",
    "normalize.css": "^8.0.1",
    "postcss": "^7.0.11",
    "postcss-functions": "^3.0.0",
    "postcss-js": "^2.0.0",
    "postcss-nested": "^4.1.1",
    "postcss-selector-parser": "^6.0.0",
    "pretty-hrtime": "^1.0.3",
    "reduce-css-calc": "^2.1.6",
    "resolve": "^1.14.2"
 },

暴露在 src 之外的文件或文件

  • css 文件
// base.css
@tailwind base;
// components.css
@tailwind components;
// screen.css
@tailwind screens;
// utilities.css
@tailwind utilities;
// tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
  • js 文件
// tailwindcss/defaultConfig.js
const cloneDeep = require('lodash/cloneDeep')
const defaultConfig = require('./stubs/defaultConfig.stub.js')
module.exports = cloneDeep(defaultConfig)

// tailwindcss/defaultTheme.js
const cloneDeep = require('lodash/cloneDeep')
const defaultConfig = require('./stubs/defaultConfig.stub.js')
module.exports = cloneDeep(defaultConfig.theme)

// tailwindcss/plugin.js
const createPlugin = require('./lib/util/createPlugin').default
module.exports = createPlugin

// tailwindcss/resolveConfig.js
const resolveConfigObjects = require('./lib/util/resolveConfig').default
const defaultConfig = require('./stubs/defaultConfig.stub.js')
module.exports = function resolveConfig(...configs) {
  return resolveConfigObjects([...configs, defaultConfig])
}
  • stubs 目录文件(本质就是 tailwindcss 的配置文件)
  1. tailwindcss/stubs/simpleConfig.stub.js
module.exports = {
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}
  1. tailwindcss/stubs/defaultConfig.stub.js
  • script 脚本
// build
import fs from 'fs'
import postcss from 'postcss'
import tailwind from '..'
import CleanCSS from 'clean-css'

function buildDistFile(filename) {
  return new Promise((resolve, reject) => {
    console.log(`Processing ./${filename}.css...`)

    fs.readFile(`./${filename}.css`, (err, css) => {
      if (err) throw err

      return postcss([tailwind(), require('autoprefixer')])
        .process(css, {
          from: `./${filename}.css`,
          to: `./dist/${filename}.css`,
          map: { inline: false },
        })
        .then(result => {
          fs.writeFileSync(`./dist/${filename}.css`, result.css)
          if (result.map) {
            fs.writeFileSync(`./dist/${filename}.css.map`, result.map)
          }
          return result
        })
        .then(result => {
          const minified = new CleanCSS().minify(result.css)
          fs.writeFileSync(`./dist/${filename}.min.css`, minified.styles)
        })
        .then(resolve)
        .catch(error => {
          console.log(error)
          reject()
        })
    })
  })
}

console.info('Building Tailwind!')

Promise.all([
  buildDistFile('base'),
  buildDistFile('components'),
  buildDistFile('utilities'),
  buildDistFile('tailwind'),
]).then(() => {
  console.log('Finished Building Tailwind!')
})
// rebuildFixtures.js
import fs from 'fs'
import postcss from 'postcss'
import tailwind from '..'

function build({ from, to, config }) {
  return new Promise((resolve, reject) => {
    console.log(`Processing ./${from}...`)

    fs.readFile(`./${from}`, (err, css) => {
      if (err) throw err

      return postcss([tailwind(config)])
        .process(css, {
          from: undefined,
        })
        .then(result => {
          fs.writeFileSync(`./${to}`, result.css)
          return result
        })
        .then(resolve)
        .catch(error => {
          console.log(error)
          reject()
        })
    })
  })
}

console.info('\nRebuilding fixtures...\n')

Promise.all([
  build({
    from: '__tests__/fixtures/tailwind-input.css',
    to: '__tests__/fixtures/tailwind-output.css',
    config: {},
  }),
  build({
    from: '__tests__/fixtures/tailwind-input.css',
    to: '__tests__/fixtures/tailwind-output-important.css',
    config: { important: true },
  }),
]).then(() => {
  console.log('\nFinished rebuilding fixtures.')
  console.log(
    '\nPlease triple check that the fixture output matches what you expect before committing this change.'
  )
})

主入口分析

index.js, 是一个标准的 postcss plugin 的写法

const plugin = postcss.plugin('tailwind', config => {
  const plugins = []
  const resolvedConfigPath = resolveConfigPath(config)

  if (!_.isUndefined(resolvedConfigPath)) {
    plugins.push(registerConfigAsDependency(resolvedConfigPath))
  }

  return postcss([
    ...plugins,
    processTailwindFeatures(getConfigFunction(resolvedConfigPath || config)),
    formatCSS,
  ])
})

module.exports = plugin

constants 常量

import path from 'path'
export const cli = 'tailwind'
export const defaultConfigFile = './tailwind.config.js'
export const defaultConfigStubFile = path.resolve(__dirname, '../stubs/defaultConfig.stub.js')
export const simpleConfigStubFile = path.resolve(__dirname, '../stubs/simpleConfig.stub.js')

cli 命令行入口

#!/usr/bin/env node
import main from './cli/main'
import * as utils from './cli/utils'

main(process.argv.slice(2)).catch(error => utils.die(error.stack))

核心插件的引入和导出

import preflight from './plugins/preflight'
import container from './plugins/container'
import accessibility from './plugins/accessibility'
import appearance from './plugins/appearance'
import backgroundAttachment from './plugins/backgroundAttachment'
import backgroundColor from './plugins/backgroundColor'
import backgroundPosition from './plugins/backgroundPosition'
import backgroundRepeat from './plugins/backgroundRepeat'
import backgroundSize from './plugins/backgroundSize'
import borderCollapse from './plugins/borderCollapse'
import borderColor from './plugins/borderColor'
import borderRadius from './plugins/borderRadius'
import borderStyle from './plugins/borderStyle'
import borderWidth from './plugins/borderWidth'
import boxSizing from './plugins/boxSizing'
import cursor from './plugins/cursor'
import display from './plugins/display'
import flexDirection from './plugins/flexDirection'
import flexWrap from './plugins/flexWrap'
import alignItems from './plugins/alignItems'
import alignSelf from './plugins/alignSelf'
import justifyContent from './plugins/justifyContent'
import alignContent from './plugins/alignContent'
import flex from './plugins/flex'
import flexGrow from './plugins/flexGrow'
import flexShrink from './plugins/flexShrink'
import order from './plugins/order'
import float from './plugins/float'
import clear from './plugins/clear'
import fontFamily from './plugins/fontFamily'
import fontWeight from './plugins/fontWeight'
import height from './plugins/height'
import lineHeight from './plugins/lineHeight'
import listStylePosition from './plugins/listStylePosition'
import listStyleType from './plugins/listStyleType'
import margin from './plugins/margin'
import maxHeight from './plugins/maxHeight'
import maxWidth from './plugins/maxWidth'
import minHeight from './plugins/minHeight'
import minWidth from './plugins/minWidth'
import objectFit from './plugins/objectFit'
import objectPosition from './plugins/objectPosition'
import opacity from './plugins/opacity'
import outline from './plugins/outline'
import overflow from './plugins/overflow'
import padding from './plugins/padding'
import placeholderColor from './plugins/placeholderColor'
import pointerEvents from './plugins/pointerEvents'
import position from './plugins/position'
import inset from './plugins/inset'
import resize from './plugins/resize'
import boxShadow from './plugins/boxShadow'
import fill from './plugins/fill'
import stroke from './plugins/stroke'
import strokeWidth from './plugins/strokeWidth'
import tableLayout from './plugins/tableLayout'
import textAlign from './plugins/textAlign'
import textColor from './plugins/textColor'
import fontSize from './plugins/fontSize'
import fontStyle from './plugins/fontStyle'
import textTransform from './plugins/textTransform'
import textDecoration from './plugins/textDecoration'
import fontSmoothing from './plugins/fontSmoothing'
import letterSpacing from './plugins/letterSpacing'
import userSelect from './plugins/userSelect'
import verticalAlign from './plugins/verticalAlign'
import visibility from './plugins/visibility'
import whitespace from './plugins/whitespace'
import wordBreak from './plugins/wordBreak'
import width from './plugins/width'
import zIndex from './plugins/zIndex'
import gap from './plugins/gap'
import gridAutoFlow from './plugins/gridAutoFlow'
import gridTemplateColumns from './plugins/gridTemplateColumns'
import gridColumn from './plugins/gridColumn'
import gridColumnStart from './plugins/gridColumnStart'
import gridColumnEnd from './plugins/gridColumnEnd'
import gridTemplateRows from './plugins/gridTemplateRows'
import gridRow from './plugins/gridRow'
import gridRowStart from './plugins/gridRowStart'
import gridRowEnd from './plugins/gridRowEnd'
import transform from './plugins/transform'
import transformOrigin from './plugins/transformOrigin'
import scale from './plugins/scale'
import rotate from './plugins/rotate'
import translate from './plugins/translate'
import skew from './plugins/skew'
import transitionProperty from './plugins/transitionProperty'
import transitionTimingFunction from './plugins/transitionTimingFunction'
import transitionDuration from './plugins/transitionDuration'

import configurePlugins from './util/configurePlugins'

export default function({ corePlugins: corePluginConfig }) {
  return configurePlugins(corePluginConfig, {
    preflight,
    container,
    accessibility,
    appearance,
    backgroundAttachment,
    backgroundColor,
    backgroundPosition,
    backgroundRepeat,
    backgroundSize,
    borderCollapse,
    borderColor,
    borderRadius,
    borderStyle,
    borderWidth,
    boxSizing,
    cursor,
    display,
    flexDirection,
    flexWrap,
    alignItems,
    alignSelf,
    justifyContent,
    alignContent,
    flex,
    flexGrow,
    flexShrink,
    order,
    float,
    clear,
    fontFamily,
    fontWeight,
    height,
    lineHeight,
    listStylePosition,
    listStyleType,
    margin,
    maxHeight,
    maxWidth,
    minHeight,
    minWidth,
    objectFit,
    objectPosition,
    opacity,
    outline,
    overflow,
    padding,
    placeholderColor,
    pointerEvents,
    position,
    inset,
    resize,
    boxShadow,
    fill,
    stroke,
    strokeWidth,
    tableLayout,
    textAlign,
    textColor,
    fontSize,
    fontStyle,
    textTransform,
    textDecoration,
    fontSmoothing,
    letterSpacing,
    userSelect,
    verticalAlign,
    visibility,
    whitespace,
    wordBreak,
    width,
    zIndex,
    gap,
    gridAutoFlow,
    gridTemplateColumns,
    gridColumn,
    gridColumnStart,
    gridColumnEnd,
    gridTemplateRows,
    gridRow,
    gridRowStart,
    gridRowEnd,
    transform,
    transformOrigin,
    scale,
    rotate,
    translate,
    skew,
    transitionProperty,
    transitionTimingFunction,
    transitionDuration,
  })
}

特性

import _ from 'lodash'
import postcss from 'postcss'

import substituteTailwindAtRules from './lib/substituteTailwindAtRules'
import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
import substituteVariantsAtRules from './lib/substituteVariantsAtRules'
import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules'
import substituteScreenAtRules from './lib/substituteScreenAtRules'
import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules'

import corePlugins from './corePlugins'
import processPlugins from './util/processPlugins'

export default function(getConfig) {
  return function(css) {
    const config = getConfig()
    const processedPlugins = processPlugins([...corePlugins(config), ...config.plugins], config)

    return postcss([
      substituteTailwindAtRules(config, processedPlugins),
      evaluateTailwindFunctions(config),
      substituteVariantsAtRules(config, processedPlugins),
      substituteResponsiveAtRules(config),
      substituteScreenAtRules(config),
      substituteClassApplyAtRules(config, processedPlugins.utilities),
    ]).process(css, { from: _.get(css, 'source.input.file') })
  }
}

其他

todo

  • 深入学习 postcss
  • tailwindcss 工作流程分析
  • tailwindcss cli
  • 插件
  • 负值

参考

  1. postcss 官方网站
  2. postcss api
  3. 书写你的第一个postcss 插件
  4. 创建一个自己的postcss插件
  5. gulp 快速开始一个postcss插件