前言
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 的上下文。
放弃正则匹配,全面使用AST,即使用 prase5 来解析转化 html ,使用 babel 来解析转化 js,使用 postcss 来解析转化 css
为了达到第一点,我设计编写了一个npm包: tailwindcss-patch 使用它来获取到所有生成的 class 集合。
为了达到第二点,我编写了一个unplugin插件: unplugin-tailwindcss-mangle, 这是一个 webpack/vite 插件,用来在打包的时候,修改html,js,css混淆所有的类名。
如何使用
那么如何使用它们呢?很简单只要以下几步:
1. 安装这2个包
<npm|yarn|pnpm> i -D unplugin-tailwindcss-mangle tailwindcss-patch
2. 执行一下脚本
npx tw-patch
3. 添加
"scripts": { "prepare": "tw-patch" },
4. 注册这个插件
这里以 webpack 和 vite 为例
webpack
// esmimport { webpackPlugin as utwm } from 'unplugin-tailwindcss-mangle'// or cjsconst { 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 然后打包构建,预览即可看到效果。
vite
// for example: vue vite projectimport { 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 场景思考和解决方案
然而上述方案还有一些缺陷,尤其是在 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 给我.