如何在快应用开发中使用 Tailwind CSS 提升开发效率?

Tailwind CSS,在 Web 开发领域,已经成为各种框架、开发人员标配;已经有大量实例证明,基于 Tailwind CSS 来编写样式,可以为开发者节省诸多时间;而且,能够优化减小代码体积、提升运行性能,是非常棒的技术套件。 快应用 开发,同样基于 Web 技术栈,理论上也完全可以使用 Tailwind CSS;在实际开发过程中,该如何使用 Tailwind CSS 呢?请详细说说。


Tailwind CSS 的工作原理是扫描所有 HTML 文件、JavaScript 组件和任何其他模板的类名,生成相应的样式,然后将它们写入静态 CSS 文件。它快速、灵活、可靠,且运行时间为零。官方提供 Tailwind CLI 、PostCSS、Play CDN 等使用方式;下面主要讲下如何基于 Tailwind CLI 方式,在应用开发中使用 Tailwind CSS:

Tailwind CSS 可为快应用开发带来哪些好处?

Tailwind CSS 是一种现代的 CSS 框架,它提供了一组预定义的 CSS 类,用于快速开发现代 Web 应用程序。Tailwind 的核心理念是为开发者提供一种灵活的方法来构建自定义的 UI 组件,而不需要编写大量的 CSS 代码。

Tailwind CSS 的工作原理是扫描所有 HTML 文件、JavaScript 组件和任何其他模板的类名,生成相应的样式,然后将它们写入静态 CSS 文件。它快速、灵活、可靠,且运行时间为零。

在快应用开发中引入,可以为项目带来四方面的好处:更快的编写效率更小的代码总体积更高的运行效率更易于项目维护。更详细说明如下:

  • 预定义的 CSS 类可以减少样式的冗余,从而减小 CSS 文件的体积
  • 大量配套设施,可以提高开发效率,不必手动编写大量的 CSS 代码。
  • 提供了自动优化工具,可以删除未使用的 CSS 代码
  • 不用耗时去为 CSS class 取语义化类名,不再需要关注 style 标签;
  • 代码体积减小,可以减少下载、加载、解析等步骤耗时。
  • 使 CSS 样式更规范化,避免冗余的样式和复杂的选择器,优化运行性能。
  • Tailwind CSS 拥有活跃的社区和文档,可以帮助开发者快速上手和解决问题。

如何在项目中使用 Tailwind CSS?

安装 Tailwind CSS 依赖

通过 npm 安装 tailwindcss 并创建 tailwind.config.js 文件,具体命令如下:

pnpm install -D tailwindcss 
# OR 
# npm install -D tailwindcss 
npx tailwindcss init

配置您的模板路径

在文件中添加所有模板文件的路径 tailwind.config.js,参考性配置如下:

const colors = require('tailwindcss/colors')

const selfCustomColors = {
  brand: {
    DEFAULT: '#1e293b',
  },
  warn: {
    DEFAULT: '#f59e0b',
  },
  link: {
    DEFAULT: '#0ea5e9',
  },
  mark: {
    DEFAULT: '#ff4582',
  },
}

module.exports = {
  mode: 'jit',
  content: [
    './src/**/*.{ux,html,js,svelte,vue,ts}',
    './node_modules/flowbite/**/*.js',
  ],
  purge: {
    enabled: true,
    content: [
      './src/**/*.{ux,html,js,svelte,vue,ts}',
      './node_modules/flowbite/**/*.js',
    ],
  },
  // https://tailwindcss.com/docs/configuration#core-plugins
  corePlugins: {
    preflight: false, // disable base/reset styles
    container: false, // disable container component
    content: false, // disable `content` utility
    accentColor: false, // disable `accent-color` utility
    accessibility: false, // disable `appearance` utility
    appearance: false, // disable `appearance` utility 
    aspectRatio: false, // disable `aspect-ratio` utility
    backgroundOpacity: false, // disable `background-opacity` utility
    backdropBlur: false, // disable `backdrop-blur` utility
    backdropBrightness: false, // disable `backdrop-brightness` utility
    backdropContrast: false, // disable `backdrop-contrast` utility
    backdropGrayscale: false, // disable `backdrop-grayscale` utility
    backdropHueRotate: false, // disable `backdrop-hue-rotate` utility
    backdropInvert: false, // disable `backdrop-invert` utility
    backdropOpacity: false, // disable `backdrop-opacity` utility
    backdropSaturate: false, // disable `backdrop-saturate` utility
    backdropSepia: false, // disable `backdrop-sepia` utility
    blur: false, // disable `blur` utility
    borderCollapse: false, // disable `border-collapse` utility
    borderOpacity: false, // disable `border-opacity` utility
    borderSpacing: false, // disable `border-spacing` utility
    boxShadow: false, // disable `box-shadow` utility
    boxShadowColor: false, // disable `box-shadow-color` utility
    boxDecorationBreak: false, // disable `box-decoration-break` utility
    boxSizing: false, // disable `box-sizing` utility
    breakAfter: false, // disable `break-after` utility
    breakBefore: false, // disable `break-before` utility
    breakInside: false, // disable `break-inside` utility
    brightness: false, // disable `brightness` utility
    captionSide: false, // disable `caption-side` utility
    caretColor: false, // disable `caret-color` utility
    clear: false, // disable `clear` utility
    contrast: false, // disable `contrast` utility
    divideColor: false, // disable `divide-color` utility
    divideOpacity: false, // disable `divide-opacity` utility
    divideStyle: false, // disable `divide-style` utility
    divideWidth: false, // disable `divide-width` utility
    float: false, // disable `float` utility
    fontVariantNumeric: false, // disable `font-variant-numeric` utility
    hyphens: false, // disable `hyphens` utility
    isolation: false, // disable `isolation` utility
    lineClamp: false, // disable `line-clamp` utility
    mixBlendMode: false, // diable `mix-blend-mode` utility
    listStyleImage: false, // disable `list-style-image` utility
    listStylePosition: false, // disable `list-style-position` utility
    listStyleType: false, // disable `list-style-type` utility
    objectPosition: false, // disable `object-position` utility
    opacity: false, // disable `opacity` utility
    outlineColor: false, // disable `outline-color` utility
    outlineOffset: false, // disable `outline-offset` utility
    outlineStyle: false, // disable `outline-style` utility
    outlineWidth: false, // disable `outline-width` utility
    overscrollBehavior: false, // disable `overscroll-behavior` utility
    placeContent: false, // disable `place-content` utility
    placeItems: false, // disable `place-items` utility
    placeSelf: false, // disable `place-self` utility
    placeholderOpacity: false, // disable `placeholder-opacity` utility
    resize: false, // disable `resize` utility
    ringColor: false, // disable `ring-color` utility
    ringOffsetColor: false, // disable `ring-offset-color` utility
    ringOffsetWidth: false, // disable `ring-offset-width` utility
    ringOpacity: false, // disable `ring-opacity` utility
    space: false, // disable `space-between` utility
    textDecorationThickness: false, // disable `text-decoration-thickness` utility
    textOpacity: false, // disable `text-opacity` utility
    textTransform: false, // disable `text-transform` utility
    textUnderlineOffset: false, // disable `text-underline-offset` utility
    touchAction: false, // disable `touch-action` utility
    userSelect: false, // disable `user-select` utility
    verticalAlign: false, // disable `vertical-align` utility
    whitespace: false, // disable `whitespace` utility
    wordBreak: false, // disable `word-break` utility
    willChange: false, // disable `will-change` utility
  },
  darkMode: false,
  theme: {
    colors: { ...colors, ...selfCustomColors },
    extend: {
      width: ({ theme }) => ({
        auto: 'auto',
        ...theme('spacing'),
      }),
      height: ({ theme }) => ({
        auto: 'auto',
        ...theme('spacing'),
      }),
      spacing: {
        '1/2': '2px',
        '1': '4px',
        '3/2': '6px',
        '2': '8px',
        '5/2': '10px',
        '3': '12px',
        '7/2': '14px',
        '4': '16px',
        '5': '20px',
        '6': '24px',
        '7': '28px',
        '8': '32px',
        '9': '36px',
        '10': '40px',
        '11': '44px',
        '12': '48px',
        '14': '56px',
        '16': '64px',
        '20': '80px',
        '24': '96px',
        '28': '112px',
        '32': '128px',
        '36': '144px',
        '40': '160px',
        '44': '176px',
        '48': '192px',
        '52': '208px',
        '56': '224px',
        '60': '240px',
        '72': '288px',
        '80': '320px',
        '96': '384px',
      },
      borderWidth: {
        'DEFAULT': '1px',
        '0': '0px',
        '2': '2px',
        '4': '4px',
        '8': '8px',
      },
      borderRadius: {
        'none': '0',
        '': '1px',
        'sm': '2px',
        'DEFAULT': '4px',
        'md': '6px',
        'lg': '8px',
        'xl': '12px',
        '2xl': '16px',
        '3xl': '24px',
      },
      fontSize: {
        'xm': ['12px', { lineHeight: '16px' }],
        'sm': ['14px', { lineHeight: '20px' }],
        'base': ['16px', { lineHeight: '24px' }],
        'lg': ['18px', { lineHeight: '28px' }],
        'xl': ['20px', { lineHeight: '28px' }],
        '2xl': ['24px', { lineHeight: '32px' }],
        '3xl': ['30px', { lineHeight: '36px' }],
        '4xl': ['36px', { lineHeight: '40px' }],
        '5xl': ['48px', { lineHeight: '60px' }],
        '6xl': ['60px', { lineHeight: '60px' }],
        '7xl': ['72px', { lineHeight: '60px' }],
        '8xl': ['96px', { lineHeight: '60px' }],
        '9xl': ['128px', { lineHeight: '60px' }],
      }
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

将 Tailwind 指令添加到您的 CSS 中

@tailwind 将 Tailwind 每个层的指令添加到您的主 CSS 文件中。

/* input.css */

/* @tailwind base; */
@tailwind components;
@tailwind utilities;

启动 Tailwind CLI 构建过程

运行 CLI 工具来扫描模板文件中的类并构建 CSS,参考性命令如下:

npx tailwindcss -i ./src/input.css -o ./src/output.css --watch

这是看起来路径简短的方式,实际上这个 input.cssoutput.css,无论是名字,或是它存在的路径,皆可以自行指定,您可以按照自己喜欢的方式、或是团队约定俗成的形式;譬如统一存放在:src/assets/styles 目录下。

需要补充说明的是:在团队开发中,输入上述命令,略显繁琐,可以在 package.json 注入命令(如下示例);如此以下,您只需在「终端」运行 pnpm tailwindcssnpm run tailwindcss 命令即可。

"scripts": {
  "tailwindcss": "npx tailwindcss -i ./src/input.css -o ./src/output.css --watch",
}

开始在 UX 中使用 Tailwind CSS

将已编译的 CSS 文件,在 .ux (非 app.ux)文件中,通过 @import 方式引入;

<style>
  @import './../../output.css';
</style>

现在,即可在 .uxtemplate 中,开始使用 Tailwind 的实用程序类来设置内容的样式,如下面这段代码示例:

<template>
    <div class="bg-blue-300 w-full flex flex-col justify-center items-center">
      <text class="text-red-600">TailwindCSS Area</text>
      <text class="text-red-600">Awesome TailwindCSS</text>
    </div>
</template>

当您写完保存之后,打开 output.css 即可如下的 CSS 代码:

/* @tailwind base; */

.flex {
  display: flex
}

.w-full {
  width: 100%
}

.flex-col {
  flex-direction: column
}

.items-center {
  align-items: center
}

.justify-center {
  justify-content: center
}

.bg-blue-300 {
  background-color: #93c5fd
}

.text-red-600 {
  color: #dc2626
}

至此,在 快应用 开发中引入 Tailwind CSS 的准备工作,已经初步完成,您可以与 Tailwind CSS 开启愉快合作之旅,从而促使高效编码、提前完工。假如,您对 Tailwind CSS 尚不熟悉,可通过查阅 Tailwind CSS Doc ,亦可在线体验—— Tailwind Playground

需要补充说明的是,使用 Tailwind CSS 并不破坏您原来的 CSS 书写✍🏻方式;您完全可以结合喜欢的预处理器(如 Sass、Less、Stylus),来共同工作,而无需做更多设置(如下代码示例);但我以为当您熟悉 Tailwind CSS 之后,大有可能也会“移情而别恋”。

<style lang="scss">
  @import './../../assets/styles/style.scss';
  @import './../../assets/styles/output.css';

  .primary-btn {
    width: 90 * $size-factor;
    height: 16 * $size-factor;
    background-color: $brand;
    border-radius: 8 * $size-factor;
    color: $white;
  }
</style>

如何基于 Tailwind CSS 提升开发效率?

安装 Tailwind CSS IntelliSense 扩展

您可以在应用开发工具的「扩展市场」,通过关键字 Tailwind 检索”Tailwind CSS IntelliSense“扩展——适用于 Visual Studio Code 的智能 Tailwind CSS 工具,用户提供自动完成、语法突出显示和 linting 等高级功能来增强 Tailwind 开发体验;略作适配即可支持 *.ux;详细设置,可参见: Tailwind CSS IntelliSense

默认情况下,编辑“字符串”内容时(例如在 JSX 属性值中),开发工具中不会触发完成。更新设置 editor.quickSuggestions 可能会改善您的体验:

"editor.quickSuggestions": {
  "strings": "on"
}

参考基于 Tailwind CSS 组件的开源库

Tailwind CSS 生态发展繁荣超乎想象,就连其衍生产品也不可胜数;如 Flowbite ——包含 600 多个 UI 组件、部分和页面的开源库进行开发,该库使用 Tailwind CSS 的实用程序类构建并在 Figma 中设计。它不仅能充当 Tailwind CSS 插件使用,也可以拷贝到项目直接使用,还可以在线查阅效果(支持多种屏幕设备类型),挑选匹配实现方案;在仍是基于组件树构建页面的时代,极大提升页面搭建效率。如: pagination 组件:


常见问题及说明

如果您熟悉 Web 开发,针对上述教程,您可能会有诸多疑惑;实际上,笔者在实践之时,也有发现,只不过因为各种机制限制,目前只能如此。下面就与诸君分享,期可解惑:

不能在 script 标签中,引入输出 CSS 文件么**?

按 Web 开发习惯,在 app.ux 文件中可以直接 import 输出 CSS 文件(如下代码所示),那为何未建议如此呢?

<script>
import './output.css'
</script>

因为这样做会报如下错误;具体是缺乏与之匹配的 loader;当然,可以使用 css-loader 和 style-loader 来处理 CSS 文件,详见 ChatGPT | You may need an appropriate loader....

You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

| /* @tailwind base; */

但在快应用开发过程中,却不能生效;因为 style-loader 的作用是将 CSS 注入 DOM,其中用到了诸多浏览器 API(document),而快应用的开发虽然是基于前端技术栈,但其运行环境止于 v8,实际的渲染是由 Android 底层完成,并未走浏览器这条路,因此无法工作。在构建工具优化、兼容之前,通过 style 标签中引入 CSS,成了主要选择。

为何在 tailwind.config.js 禁用如此多样式?

Tailwind corePlugins 部分允许您完全禁用 Tailwind 通常默认生成的类(如果您的项目不需要它们);这个设计对于快应用实在必要的紧,在上述示例中,对 text-opacity 等样式进行禁用,也是无奈之举。具体原因在于以下两点:

其一:快应用标准虽然基于 Web 技术栈,但只是 Web 标准子集,如蛮多 CSS 属性并不能很好的支持;其二:默认情况下,TailwindCSS 会将样式属性的值存储为 CSS 变量(例如 --tw-text-opacity),以便进行动态计算和响应式设计。

如此一来,在 template 中使用类 text-red-300border-spacing-1,输出的便是如下 CSS;这样的写法,在快应用中并不能得到正确解析,也就无法起到期待效果,于是禁用便成了必须要做之事。当然,这样禁用也好处:利于输出更小提及的 CSS 文件。其他属性亦复如是。

.text-red-300 {
  --tw-text-opacity: 1;
  color: rgb(252 165 165 / var(--tw-text-opacity))
}
.border-spacing-1 {
  --tw-border-spacing-x: 4px;
  --tw-border-spacing-y: 4px;
  border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y)
}

为何要在 input.css 中移除 @tailwind base;

这么做主要未解决问题: How disable default styles for : before and : after

/* input.css */

/* @tailwind base; */
@tailwind components;
@tailwind utilities;

在一般 Web 开发过程中,会将 base、components、utilities 等指令,都添加到 CSS 文件项目中;但,在快应用开发过程中,这无疑是累赘;因为它的存在,会使得 output.css 输出较多对项目没有裨益额外内容(如下代码所示),故而移除;这与通过 corePlugins 禁用基础样式(preflight)、禁用容器组件(container)作用类似。

*, ::before, ::after {
  --tw-border-spacing-x: 0;
  ......
}
::backdrop {
  --tw-border-spacing-x: 0;
  ......
}

为什么要在配置中通过 theme 自定义长度单位?

修改长度单位从 rempx,这也是不得已而为之。 快应用 标准对 长度单位 的支持,在 1070 及以下版本仅支持长度单位 px%。从 1080 版本开始,新增了长度单位 dp。它无法支持 rem,就只能自定义设置为 px;在通过 theme 自定义长度单位,是 Tailwind CSS 官方提供的功能,以此能够实现更精细的样式控制;虽然操作不难,但属性较多、约定繁杂。后续时间如果允许,将探索 rem to px 批量转换插件来实现,以达到快速修改。如果您感兴趣,可以查阅 Tailwind CSS 完整版本配置—— config.full.js

其他暂未解决问题

  • 少部分类名写法,快应用构建不支持

在 Tailwind CSS 机制中,支持使用类名诸如 h-[100px] w-1/3 (对应生成 CSS 如下),然而,在快应用构建工具暂不支持这类名,于是乎这样写便不会生效,当然也不会造成什么问题;后续亟待打包工具优化。

.h-\[100px\] {
  height: 100px
}
.w-1\/3 {
  width: 33.333333%
}

在打包(Toolkit)未解决该问题之前,您可以手动写一个 class,或者使用内联样式(style)来兼容;在这种情况下,更推荐后者;因为引入 Tailwind CSS 的开发模式中,基本不用手写 class,即无需滚动到 style 标签或跳转 less、(s)css、文件,那么直接在 template 中使用内联更为便捷。

<div class="training-list-width" style="width: 326px;"></div>

<style>
.training-list-width {
  width: 326px;
}
</style>
  • 快应用构建,资源存在重复引用问题

前文中提到,在 script 标签中 import CSS 资源,暂时未得到支持;但,在 style 标签中 @import 的 CSS 文件,被 A、B 两个页面(*.ux)引入,那么对应的 css 内容,就会被打入对应页面(即便有的内容没有被使用),从而导致代码体积略有增加(即便参考 如何优化「快应用」rpk 包体积? 一文中的方法,也不可避免)。这是快应用构建本身存在的问题,使用 Tailwind CSS 也不例外。后续需要打包工具优化。

以上,就是在 快应用 开发过程中,引入 Tailwind CSS 所需的操作、以及常见问题说明。肯定还有异常更多情况没有考虑到,后续会逐步补充;如果您在实际运用过程中,欢请留言交流、反馈、分享。

2023 年 07 月 05 日写于〔深圳福田〕