CRA引入unocss报错原因探究
😝

CRA引入unocss报错原因探究

Tags
unocss
create-react-app
React.js
Published
January 15, 2023
Author
wangfengyuan

背景

前端项目大多都离不开使用图标,目前最常见的是直接找开源的已有的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>
但是事实上不会如此顺利,启动项目后报错了,如下
notion image
第一直觉,这个工具很多人使用,肯定特么有人遇到过一样的问题吧,然后去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 插件来处理这种虚拟模块,将这个/_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; }, ), };
解决完这个问题后就能正确使用了
notion image
但是发现在停止并重启服务时,这个转换又失效了
notion image
原因在于,cra内部开启了filesystem的持久化缓存,上面提到的webpack-virtual-modules插件的原因导致缓存了不完整的中间态的文件内容,暂时通过关闭cra的缓存来解决,添加
config.cache = false;
为避免其他人也遇到这个问题,我把这个create-react-app使用示例提交了PR

总结

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