tailwindcss
是一个原子类优先的 css 框架,现如今非常的流行。它语义化的 类名
能够让前端开发人员直观地对元素的样式进行编写和维护。
然而这种直观性,有时候也会带来一定的困扰。有时候我们不想让用户或者外界的开发人员也能直观的观测到所有元素的样式,
比如我们访问 https://tailwindcss.com , 然后打开开发者工具,检查元素。瞬间,页面上那些元素的排版和样式都能猜测出来,甚至都不需要看右边的 style
面板。
所以,出于让其他人在生产环境中无法直观的看出一个元素的样式,我们就需要对 tailwindcss
生成的原子类进行混淆。
起初我在网络上寻找解决方案时,我找到了 mangle-css-class-webpack-plugin
,这是一个 webpack
插件,专门用来压缩和混淆在 html
,js
,css
里的类名。
然而我在尝试使用它去混淆 tailwindcss
的类名的时候遇到了困难,一开始我无法准确的传入 classNameRegExp
和其他的参数,这导致我 webpack build
之后的结果,要不就完全是错的,要不就完全达不到预期,后来找了一些 issue
看似解决了这个问题,但是可维护性极差。
另外直接通过正则去修改 js
里的字面量的方式,很容易造成小范围误伤,导致整个项目运行不起来报错。
于是我开始思考,有没有什么方式能够精准的实现混淆 tailwindcss
的类名,以下是我的解决方案。
首先为了更加精确的进行匹配,我决定从以下角度去实现
tailwindcss
生成了多少的类,我需要在运行时,得到 tailwindcss
的上下文。prase5
来解析转化 html
, 使用 babel
来解析转化 js
, 使用 postcss
来解析转化 css
为了达到第一点,我设计编写了一个 npm 包: tailwindcss-patch 使用它来获取到所有生成的 class
集合。
为了达到第二点,我编写了一个unplugin
插件: unplugin-tailwindcss-mangle, 这是一个 webpack/vite
插件,用来在打包的时候,修改html
,js
,css
混淆所有的类名。
那么如何使用它们呢?很简单只要以下几步:
<npm|yarn|pnpm> i -D unplugin-tailwindcss-mangle tailwindcss-patch
npx tw-patch
prepare
script 在你的 package.json
里 "scripts": {
"prepare": "tw-patch"
},
这里以 webpack
和 vite
为例
// esm
import { webpackPlugin as utwm } from 'unplugin-tailwindcss-mangle'
// or cjs
const { webpackPlugin: utwm } = require('unplugin-tailwindcss-mangle')
// use this webpack plugin
// for example next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack: (config) => {
config.plugins.push(utwm())
return config
}
}
module.exports = nextConfig
这里我以 nextjs
为例,当然 vue.config.js
那些也都是类似的,把 utwm()
注册进 webpack
然后打包构建,预览即可看到效果。
// for example: vue vite project
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { vitePlugin as utwm } from 'unplugin-tailwindcss-mangle'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), utwm()]
})
然后执行脚本:
# generate bundle
<npm|yarn|pnpm> run build
# preview
<npm|yarn|pnpm> run preview
<!-- before -->
<div class="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex"></div>
<!-- after -->
<div class="tw-g tw-h tw-i tw-d tw-e tw-j tw-k tw-l"></div>
当然,你也可以自定义生成的类名,查看文档获取更多的配置项。
上面简单的介绍了一下使用方式,下面才是正文,当然只是用就没必要看这一段了。
思路上,首先我们仿照 tailwindcss
的文件提取方式,先从源文件 (content
配置的 glob
表达式 覆盖到的文件),比如 html
,js
,ts
,jsx
,tsx
,vue
,svelte
... 这类的文件中,读取内容生成 class
, 然后,利用 html ast
和 babel ast
转义,把转义结果保存起来,最后交给 postcss
对所有的 css
选择器进行转义。
一图以蔽之:
当然有些聪明的小伙伴,可能会产生疑问,为什么图上的 css
被处理了 2
次呢?
因为 css
有可能是变成一段 inline
的 js
,这种情况下,没有 css
文件产生,只有一个 js
块,里面包含着样式内容,这时候使用插入 loader
的方案可以解决。而最后获取生成器内的map
进行转化和比较,算是一道双保险。
然而上述方案还有一些缺陷,尤其是在 ssr
场景下
我们都知道 react
/vue
ssr
模式下面会打 2
类包,一类是服务端,一类是客户端的。
这意味着我们在运行构建任务时,我们的插件会被跑 2
次。
然而这里面存在一个严重的问题,我们能得到 tailwindcss
上下文的时机,是在客户端的 postcss
被触发后,然而服务端打包时,并不会触发 postcss
的处理。假如 server
的包,在 client
的包之前打好了,这时候运行项目。由于 clinet
包被混淆了,server
包没有被混淆,这时候就会出现 2
边的dom
属性不对等的问题,从而造成客户端水合激活报错。遇到这种情况,我们应该怎么办呢?
实际上很简单,既然插件在同一个 nodejs
进程里,被运行了 2
次,那么我们大可创建一块缓存区域,当发现 server
端的打包先被激活的时候,我们就缓存住它的运行结果,就是那些 Webpack Source
或者 Vite Chunk/Asset
对象。等到客户端获取上下文,处理好了之后,再把它们去取出来,使用上下文对 server bundle
进行处理混淆,然后再对之前 server
已经输出的结果进行覆写,这样不就可以了吗?
所以这个方案,我用在了 nextjs
/nuxtjs
里。
如果你在使用过程中,遇到了问题或者疑问,或者想进行交流,欢迎提 issue 给我.