likes
comments
collection
share

create-react-app引入unocss报错原因探究

作者站长头像
站长
· 阅读数 50

背景

前端项目大多都离不开使用图标,目前最常见的是直接找开源的已有的icon图标引入项目使用,但是如果有单独的设计团队,会用专门的工具来设计图标,然后导出svg图片给到前端。这里为了方便,决定直接使用强大的unocss开源原子化css引擎工具,结合纯CSS样式图标来实现。原文地址

使用图标

当前项目是使用create-react-app (后面简称cra)脚手架初始化的react项目,构建是用的webpack,这里结合unocss使用文档和preset-icon使用说明,文档提到了可以使用FileSystemIconLoader 来实现自定义图标,首先安装依赖

npm i -D unocss @unocss/webpack

cra封装了webpack配置,因此修改的话需要使用customize-cra,svg格式的图标放在src/assets/icons文件夹下,新增config-overrides.js文件

const path = require('path');
const { override, addWebpackPlugin } = require('customize-cra');
const UnoCss = require("@unocss/webpack").default;
const { presetIcons } = require("unocss");

module.exports = {
    webpack: override(
        addWebpackPlugin(
            UnoCss({
                presets: [
                    presetIcons({
                        collections: {
                            'midas': FileSystemIconLoader(
                            './src/assets/icons',
                            (svg) => svg.replace(/fill="none"/, 'fill="currentColor"')
                            )
                        },
                    }),
                    presetUno()
                ],
            }),
        ),
				(config) => {
            config.optimization.realContentHash = true;
            return config;
        },
    ),

};

然后在项目入口文件引入

import 'uno.css'

假设有一个向下的箭头图标叫down.svg,那么项目中可以直接这样使用。

<i className='i-midas:down'></i>

但是事实上不会如此顺利,启动项目后报错了,如下

create-react-app引入unocss报错原因探究

第一直觉,这个工具很多人使用,肯定特么有人遇到过一样的问题吧,然后去github上搜索,然而并没有搜到同样的问题,官方示例给的大部分是vite的使用场景,webpack相关的示例主要有nextjsvuecli,并没有cra的示例,在unocss仓库讨论中留言后也是推荐我使用vite,于是决定找出这个报错原因。

第一步,找了个最简单的react webapck脚手架,然后按照同样配置,结果是没报错,因此上面报错只在cra中存在,然后按照报错信息到cra仓库源代码搜索,果然有线索。在这里使用了ModuleScopePlugin插件检查项目中引入的文件,只允许引入指定路径的文件,cra中定义允许的路径在这可以看到,只包括srcnode_modulespackage.json等指定路径,那么报错提示找不到的路径/_virtual_%2F__uno.css 又是哪里来的咧,这就需要探究下背后的原因

原因

unocss依赖了unpluginunplugin旨在为不同构建工具提供通用的插件系统,只需要开发一次插件就可用于不同构建工具,包括RollupViteWebpackesbuild。

首先查看unplugin如何处理resolveId这个hook,在代码这里可以看到,其内部是定义一个resolverPlugin插件来实现这个hook, 其中关键代码如下

// call hook
const resolveIdResult = await plugin.resolveId!(id, importer, { isEntry })

if (resolveIdResult == null)
  return callback()

let resolved = typeof resolveIdResult === 'string' ? resolveIdResult : resolveIdResult.id
// If the resolved module does not exist,
// we treat it as a virtual module
if (!fs.existsSync(resolved)) {
  resolved = normalizeAbsolutePath(
    plugin.__virtualModulePrefix
    + encodeURIComponent(resolved), // URI encode id so webpack doesn't think it's part of the path
  )

  // webpack virtual module should pass in the correct path
  // https://github.com/unjs/unplugin/pull/155
  if (!plugin.__vfsModules!.has(resolved)) {
    plugin.__vfs!.writeModule(resolved, '')
    plugin.__vfsModules!.add(resolved)
  }
}

可以看到,这里先调用unocss定义的resolveId hook,然后我们查看unocss定义的hook可以看到,在resolveId解析模块id时,会将

import 'uno.css'

转变为

import '/__uno.css'

在这里执行完后unpluginresolveIdResult/__uno.css ,接下来判断是否存在这个文件,当然这个文件不存在,因此将其作为虚拟模块,并且拼接上plugin.__virtualModulePrefix 前缀,这时模块id就成了

/_virtual_%2F__uno.css

这里就是报错文件找不到对应的报错路径,接下来调用

plugin.__vfs!.writeModule(resolved, '')

这里使用了[webpack-virtual-modules](https://github.com/sysgears/webpack-virtual-modules) 插件来处理这种虚拟模块,将这个/_virtual_%2F__uno.css 虚拟模块的初始内容定义为空字符串,接着unocssload这个hook将内容转换成特定的占位符字符串,然后再transform hook中找到要处理文件数组, 接下来在webpackCompilation编译钩子里,扫描上一步要处理的文件,利用正则匹配生成tokens数组,接下来根据定义的样式规则生成在最终的样式代码,然后替换掉占位符。同时也会调用

plugin.__vfs.writeModule(id, code)

将代码写入这个虚拟模块。

解决办法

定位到原因是ModuleScopePlugin插件不能识别/_virtual_%2F__uno.css 虚拟模块报错后,解决办法就是将这个模块添加到插件的allowedPaths允许引入的文件路径列表,在config-overrides.js 中补充如下

module.exports = {
    webpack: override(
        // ...,
        (config) => {
            const ModuleScopePlugin = config.resolve.plugins.find(plugin => plugin.constructor.name === 'ModuleScopePlugin');
            ModuleScopePlugin.allowedPaths.push(path.join(__dirname, '_virtual_%2F__uno.css'));
            config.optimization.realContentHash = true;
            return config;
        },
    ),

};

解决完这个问题后就能正确使用了

create-react-app引入unocss报错原因探究

但是发现在停止并重启服务时,这个转换又失效了

create-react-app引入unocss报错原因探究

原因在于,cra内部开启了filesystem的持久化缓存,上面提到的webpack-virtual-modules插件的原因导致缓存了不完整的中间态的文件内容,暂时通过关闭cra的缓存来解决,添加

config.cache = false;

为避免其他人也遇到这个问题,我把这个create-react-app使用示例提交了PR

总结

unocss作为一个很好用的原子化引擎工具,也提供了很多好用的预设,比如本文的纯CSS图标,在cra项目中直接使用时会有报错,原因是cra内部配置拦截了这种虚拟模块路径,因此需要特殊处理修改默配置来解决