背景
前端项目大多都离不开使用图标,目前最常见的是直接找开源的已有的
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>
但是事实上不会如此顺利,启动项目后报错了,如下
第一直觉,这个工具很多人使用,肯定特么有人遇到过一样的问题吧,然后去
github
上搜索,然而并没有搜到同样的问题,官方示例给的大部分是vite
的使用场景,webpack
相关的示例主要有nextjs
、vuecli
,并没有cra
的示例,在unocss
仓库讨论中留言后也是推荐我使用vite
,于是决定找出这个报错原因。第一步,找了个最简单的
react webapck
脚手架,然后按照同样配置,结果是没报错,因此上面报错只在cra
中存在,然后按照报错信息到cra
仓库源代码搜索,果然有线索。在这里使用了ModuleScopePlugin
插件检查项目中引入的文件,只允许引入指定路径的文件,cra
中定义允许的路径在这可以看到,只包括src
、node_modules
、package.json
等指定路径,那么报错提示找不到的路径/_
virtual_
%2F__uno.css
又是哪里来的咧,这就需要探究下背后的原因原因
unocss
依赖了unplugin
,unplugin
旨在为不同构建工具提供通用的插件系统,只需要开发一次插件就可用于不同构建工具,包括Rollup
、Vite
、Webpack
、esbuild。
// 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) } }
import 'uno.css'
转变为
import '/__uno.css'
在这里执行完后
unplugin
的resolveIdResult
为/__uno.css
,接下来判断是否存在这个文件,当然这个文件不存在,因此将其作为虚拟模块,并且拼接上plugin.__virtualModulePrefix
前缀,这时模块id就成了/_virtual_%2F__uno.css
这里就是报错文件找不到对应的报错路径,接下来调用
plugin.__vfs!.writeModule(resolved, '')
这里使用了
webpack-virtual-modules
插件来处理这种虚拟模块,将这个/_
virtual_
%2F__uno.css
虚拟模块的初始内容定义为空字符串,接着unocss
在load
这个hook
将内容转换成特定的占位符字符串,然后再transform
hook
中找到要处理文件数组, 接下来在webpack
的Compilation
编译钩子里,扫描上一步要处理的文件,利用正则匹配生成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; }, ), };
解决完这个问题后就能正确使用了
但是发现在停止并重启服务时,这个转换又失效了
config.cache = false;
为避免其他人也遇到这个问题,我把这个create-react-app使用示例提交了PR
总结
unocss
作为一个很好用的原子化引擎工具,也提供了很多好用的预设,比如本文的纯CSS
图标,在cra
项目中直接使用时会有报错,原因是cra
内部配置拦截了这种虚拟模块路径,因此需要特殊处理修改默配置来解决