写一个unocss的vite插件

670 阅读2分钟

背景

前几天逛unocssissue, 发现个有意思的提案

由于unocss是按需生成对应的规则样式的,因此如果你想用浏览器上的开发者模式给元素添加上符合规则的class时,是无法生效的。但我有时可能我就是想这么干,不然来回切换编辑器和浏览器查看效果就是很烦。

实现

原理上很简单,我们只需通过MutationObserver来监听所有元素的class变化,然后将新增的class传给服务。这里我们可以引入一个虚拟文件将监听脚本返回给客户端:

{
  load(id) {
    if (id === DEVTOOLS_PATH) {
      // 生产环境下, 返回空
      return config.command === 'build'
            ? '' : `

function post(data: any) {
  return fetch('__POST_PATH__', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  })
}

function schedule() {
  ...
  post({ type: 'add-classes', data: Array.from(pendingClasses) })
}

const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === 'class' && mutation.target) {
      Array.from((mutation.target as Element).classList || [])
        .forEach((i) => {
          if (!visitedClasses.has(i))
            pendingClasses.add(i)
        })
      schedule()
    }
  })
})

`
    }
  }
}

由于需要将增加的class发送给服务端,故还需要创建一个接口用来接收新增的class:

{
  ...
  configureServer(server) {
    server.middlewares.use(async(req, res, next) => {
      // POST_PATH 为新增类的url路径
      if (req.url !== POST_PATH)
        return next()

      try {
        const data = await getBodyJson(req)
        const type = data?.type
        let changed = false
        switch (type) {
          case 'add-classes':
            (data.data as string[]).forEach((key) => {
              if (!devtoolCss.has(key)) {
                devtoolCss.add(key)
                changed = true
              }
            })
            if (changed)
            // 更新类
              updateDevtoolClass()
        }
        res.statusCode = 200
      }
      catch (e) {
        console.error(e)
        res.statusCode = 500
      }

      res.end()
    })
  }
}

这里服务端接收到新增的class后, 存到一个Set中,如果有之前没有的,就重新更新css文件

通过DevTool增加的class对应的css代码也是一个虚拟文件, 可以在前面提到的DEVTOOLS_PATH返回的代码中引入这个虚拟文件:

...
const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === 'class' && mutation.target) {
      Array.from((mutation.target as Element).classList || [])
        .forEach((i) => {
          if (!visitedClasses.has(i))
            pendingClasses.add(i)
        })
      schedule()
    }
  })
})
+ import('${DEVTOOLS_CSS_PATH}')

该模块用于返回所有监听到新增的类对应的css规则生成:

{
  load (id) {
    if (id === DEVTOOLS_CSS_PATH) {
      // 根据所有devtools新增的类, 生成对应的css返回
      const { css } = await uno.generate(devtoolCss)
      return css
    }
  }
}

接下来就很简单了,只要收到客户端有更新新的class的时候,通知该模块更新即可:

function updateDevtoolClass() {
  const mod = server.moduleGraph.getModuleById(DEVTOOLS_CSS_PATH)
  if (!mod)
    return
  server.moduleGraph.invalidateModule(mod)
  server.ws.send({
    type: 'update',
    updates: [{
      acceptedPath: DEVTOOLS_CSS_PATH,
      path: DEVTOOLS_CSS_PATH,
      timestamp: lastUpdate,
      type: 'js-update',
    }],
  })
}

以上就是在插件的大致实现思路,详细的代码参见该PR

缺陷

由于MutationObserver监听是并不知道类的通过DevTools添加的还是通过脚本添加的,因此如果你使用类似如下的代码:


let value = getValue() // 假设为2
const el = document.querySelector('#app')
el.classList.push(`p-${value}`)

那么此时p-2这个规则会有效果,但是再生产环境下是没有这个规则的(编译阶段没有扫描到这个规则p-2), 因此可能会出现开发环境和生产环境不一致的情况。

所有应当尽量避免这种以拼接的形式组成class或者使用safelist

参考