UnoCSS webpack插件原理

862 阅读4分钟

简介

最近团队推荐使用UnoCSS来进行CSS样式的书写,它有以下优势:

  • 配置简单:仅需简单的几步即可完成。
  • 原子CSS:你可以重复的书写很多相同的属性而不必担心其占用过多的体积。
  • 书写体验佳:我们开发时不必再绞尽脑汁先给元素想个名字,然后上下滑动来改变对应元素的样式。
  • 所见即所得:如 flex表示弹性布局p-10px表示10像素padding等。

但也有很多缺点:

  • 阅读体验差:一个元素会因为多个class变得很长很长,同时也失去了语义化不好排查bug
  • 编译缓存问题:Vue2中使用UnoCSS如果不删除cache-loader,会导致UnoCSS样式不生效,也是本文研究的重点。

UnoCSS 是一个具有多种一流构建工具集成的同构引擎(包括PostCSS 插件)。这意味着 UnoCSS 可以在不同的地方更加灵活地使用(例如,CDN 运行时,可以动态生成 CSS),并且可以与构建工具深度集成,提供更好的 HMR、性能和开发者体验(例如,Inspector)。

VSCode Debug Terminial使用

如果你现在还不会使用VSCode进行本地npm包(node环境)debug的话,那么赶快学习一下吧。VSCode Debug Terminal非常简单,很快就能学会,他能帮助我们快速定位问题

  1. 打开一个Debug Terminal:

image.png

  1. 运行指令:

image.png

在这里运行指令和我们在普通Terminal中运行指令一模一样,如:npm run serve,但这样运行是没有断点的,我们需要去关键路径上打断点,如:

image.png

打完断点之后再运行指令,我们就能愉快的进行调试了,如:

image.png

进入之后对源码进行分析,在你认为是关键路径的地方打上断点,一点一点摸索即可。

原理

image.png

画图工具:excalidraw

整体架构

首先UnoCSS主包入口位于:node_modules/@unocss/webpack中,主要流程为:

  1. 导出一个WebpackPlugin函数。
  2. WebpackPlugin函数调用位于node_modules/unplugin导出对象的createUnplugin方法。
  3. 返回满足一个接收用户自定义参数的函数,执行后返回符合webpack plugin协议的对象(包含一个apply方法)。

image.png

编译流程

任务收集

apply方法里,他会判断传递进来的plugin参数是否包含一个transform方法,包含的话就会加载其同级webpack目录下的transform loader

这个loader每次执行(rules匹配Vue文件等),都会调用plugin的transform方法,这个方法会收集task任务,任务传递一个提取模块儿中关键字的extract方法,执行后返回Promise对象,主要作用就是解析每个Vue文件中template模版中的原子CSS声明

async function extract(code, id) {
  if (id)
    modules.set(id, code);
  const len = tokens.size;
  await uno.applyExtractors(code.replace(SKIP_COMMENT_RE, ""), id, tokens);
  if (tokens.size > len)
    invalidate();
}

image.png

清空任务

unocss plugin订阅了webpack的optimizeAssets hook,在这个hook里,他清空了所有的任务,从而收集了模板里所有的token

image.png

compilation.hooks.optimizeAssets.tapPromise 钩子的执行时机是在 webpack 构建过程的资源优化阶段。具体来说,它在 compilation 对象的 optimizeAssets 阶段触发。

Webpack 的构建过程分为多个阶段,其中一个阶段是优化资源。在这个阶段,Webpack 会对生成的资源进行一些优化,例如压缩、混淆等。compilation.hooks.optimizeAssets 钩子就是在这个优化资源的阶段触发的。

主要代码如下:(有删改

compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  compilation.hooks.optimizeAssets.tapPromise(PLUGIN_NAME, async () => {
    await flushTasks();
    const result = await uno.generate(tokens, { minify: true });
    code = result.getLayer(layer)
    compilation.assets[file] = new WebpackSources__default.RawSource(code);
  });
});

cache-loader 缓存

了解了编译原理,那么就很好解释为何在Vue2中配置UnoCSS要加一条config:

config.module.rule('vue').uses.delete('cache-loader')

如果不加这一句,首次编译没有问题,因为所有的模块儿都没有缓存都能走完完整的loader链路,也就会触发token的收集,从而在最终的webpack hook:optimizeAssets中可以统一的收集到所有的原子CSS定义,从而通过unocss核心的工具方法generate生成最终的CSS代码,并替换掉原来的静态CSS资源。

那么再次编译的时候(重新启动等),因为所有文件都有缓存了,那么只有被修改的文件可以收集到token,从而出现所有的样式都不生效,而我们去改动某一个组件又发现仅仅这个组件的样式生效了(仅这个组件重新触发了loader链路,收集了token),而他依赖的组件的样式还是未生效的情况。

总结

  • loader中收集token
  • optimizeAssets钩子中清空所有task,收集全部token
  • 调用generate方法生成相关css规则。
  • 替换原有的CSS代码。
  • UnoCSS样式是运行时的,由输入(变动的模块)决定输出(调整的CSS)。