【译】继续icon的旅程

339 阅读7分钟

前言: 为什么想搬运并且翻译这篇文章,是因为我在soybean-admin项目中,发现soybean-admin作者的icon不需要引入,并且似乎是不同的库,当时开发任务比较急,没有深究原因。今天偶然发现这篇文章,本文作者详细的讲述了他对 icon 的一个优化过程。英语一般,见谅。

原文出处

以下是译文:

一年以前,我写了一篇博文,icon之旅,分享了我在前端项目中解决需求的一些工具。在这段时期,vite的核心思想激励了很多项目去思考更高效和具有创新意识的解决方法。

在这篇文章中,我将继续分享我的icon旅程,以及目前为止遇到的工具。

PurgeIcons和它的局限性

PurgeIcons是我第一次尝试去提高Iconify (一个统一的icon库,允许你在任意框架使用各种icon)的加载速度。PurgeIcons的主要是问题是它是纯客户端的。即使它可以跟任意框架灵活搭配,但是不可避免会导致icon闪烁的现象。为了解决这个问题,我利用PurgeIcons通过静态扫描icon,与程序绑定在一起的方式,避免了发送请求但是能够加载icon。

这个方法生效了,但是只解决了一小部分问题。因为这些icon脱离框架只与JavaScript和函数方法绑定在一起。对于客户端渲染或者生成的页面,属性的传递等都是不友好的。我们需要找到一个更好的解决方式

新的解决方式

vite的核心思想是万事皆可按需导入。模块只有在被请求后才会编译。通过这种方法,vite服务可以迅速启动而不是加载整个app。除此之外Vite’s plugin APIRollup’s plugin system的上层延申,允许你对模块做一些custom transformations

所以,我们思考通过vite的这种方法,可以在编译时而不是客户端去解决这个问题。通过使用virtual modules,动态生成图标组件并即时提供,也就是vite-plugin-icons(后来的unplugin-icons


// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    IconsPlugin()
  ]
})

function IconsPlugin() {
  return {
    name: 'vite-plugin-icons',
    // tell Vite that ids start with `~icons/` are virtual files
    resolveId(id) {
      if (id.startsWith('~icons/'))
        return id
      return null
    },
    // custom logic to load the module
    load(id) {
      if (!id.startsWith('~icons/'))
        return
      const [prefix, collection, name] = id.split('/')
      // get icon data from Iconify
      const svg = getIconSVG(collection, name)
      // we compile the SVG as a Vue component
      return Vue3Compiler(svg)
    }
  }
}

使用方式如下:

<script setup>
import MdiAlarm from '~icons/mdi/alarm'
import FaBeer from '~icons/fa/beer'
import TearsOfJoy from '~/icons/twemoji/face-with-tears-of-joy'
</script>

<template>
  <MdiAlarm />
  <FaBeer style="color: orange"/>
  <TearsOfJoy/>
</template>

你可能觉得这个很像早就有的React Icons的处理方式。然而他们中的大部分是通过把所有的icon编译到各种文件中,然后作为npm包分发出来。但这样不仅因为每个icon增加了额外的字节而增加了编译器解析的时间,而且意味着你被限制在了他们提供的单一的环境中。

在unplugin-icons中,你可以用以下方式使用Iconify(超过100套的icon,10000多个icon并且还在增长)

import Icon from '~icons/[collection]/[name]'

可以在这里unplugin-icons学习到更多的使用方法

通用性

icon的通用性

Iconify同时提供了icon的统一性,通常是JSON format,所以我们能否让它更通用,根据我们喜欢的工具来选择。

iconify/collections-json

框架的通用性

刚开始,我是为用vite构建出来的vue3设计的插件。但是因为我们在处理更复杂的需求,我想我们可以为使用不同框架的用户申请不同的编译器。因为这个想法,现在icon可以作为组件在Vue3,Vue2,React和Solid中使用。(欢迎更多的框架来加入!)

function Vue3Compiler(svg) { /* ... */ }
function Vue2Compiler(svg) { /* ... */ }
function JSXCompiler(svg) { /* ... */ }
function SolidCompiler(svg) { /* ... */ }
// ...add more!

function IconsPlugin({ compiler }) {
  return {
    name: 'vite-plugin-icons',
    resolveId(id) { /* ... */ },
    load(id) {
      /* ... */
      // we could apply different compilers here as needed
      return compiler(SVG)
    }
  }
}

所以,你可以在react中这样使用

import MdiAlarm from '~icons/mdi/alarm'
import FaBeer from '~icons/fa/beer'
import TearsOfJoy from '~/icons/twemoji/face-with-tears-of-joy'

export function MyComponent() {
  return (<>
    <MdiAlarm />
    <FaBeer style="color: orange"/>
    <TearsOfJoy/>
  </>)
}

构建工具的通用性

在过去的几周,我加入了NuxtLabs,为我们的捆绑工具开发通用插件unjs/unplugin。它允许你使用通用性api为Vite, Webpack, Rollup, Nuxt, Vue CLI等等开发插件。为了实现这一想法,我们将代码调整成这样

export function VitePluginIcons() {
  return {
    name: 'vite-plugin-icons',
    resolveId(id) { /* ... */ },
    load(id) { /* ... */ }
  }
}
import { createUnplugin } from 'unplugin'

const unplugin = createUnplugin(() => {
  return {
    name: 'unplugin-icons',
    resolveId(id) { /* ... */ },
    load(id) { /* ... */ }
  }
})

// Use unplugin to generate plugins for different build tools
export const VitePluginIcons = unplugin.vite
export const WebpackPluginIcons = unplugin.webpack
export const RollupPluginIcons = unplugin.rollup

这很酷。现在你不再需要学习每个框架的api然后把他们打包成不同的包。你只需要一个包就能为所有框架服务。

unjs/unplugin

通用的解决方案

根据上面的成果,我把我的vite-plugin-icons,一个为vite和vue3开发的插件,转化成了unplugin-icons,通用的icon解决方案。

我说的通用,字面意思上,是你可以使用

image.png

现在你把它们结合起来

就是它👇!

unplugin-icons

还有一件事

你还在这儿,那我猜测你可能在寻找更深层次的东西。

你应该注意到了,不论你使用哪种icon,第一步你首先需要引入它,定义它,然后使用它。这样的话,icon这个名字就被重复至少三次。例如:

VUE

<script setup>
import MdiAlarm from '~icons/mdi/alarm'
</script>

<template>
  <MdiAlarm />
</template>

REACT

import MdiAlarm from '~icons/mdi/alarm'

export function MyComponent() {
  return (
    <div>
      <MdiAlarm />
    <div/>
  )
}

所以,我们需要一个更好的方式去实现它

自动导入

根据nuxt/components自动注入./components目录下的组件的启发,我开发了unplugin-vue-components(是的,又一个组件)在编译时自动导入需要的组件。多么完美的一个组件解决方案!

unplugin-vue-components提供了选项resolvers去提供定制化的函数解决组件应该在哪里引入的问题。

这里是一份Vite的配置(因为它们都是非插件化的,你可以在Webpack和Rollup中使用它们):

// vite.config.js
import { defineConfig } from 'vite'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'

export default defineConfig({
  plugins: [
    /* ... */
    Icons(),
    Components({
      resolvers: [
        IconsResolver({
          // to avoid naming conflicts
          // a prefix can be specified for icons
          prefix: 'i'
        })
      ]
    })
  ]
})

然后我们就能在模板中直接使用他们,没有多余的引入和重复(并且你可以方便的改变icons,不需要在三个地方更新)

<template>
  <!-- both PascalCase and dash-case are supported by Vue -->
  <IMdiAlarm />
  <i-fa-beer style="color: orange"/>
</template>

这不是很完美吗

更多学习unplugin-vue-components

@nuxt/components集成自动导入正在进行中

jsx的自动导入

哦,我几乎忘记它了。因为jsx在某种程度上就像是普通的JavaScript,JSX实际上就是函数或者方法。那事情就变得容易了。比如可以使用我开发的另一个插件-unplugin-auto-import

解释下这里的背景,unplugin-auto-importvue-global-api编译时的继承者,旨在改进 Vue Composition API 的开发体验(直接使用 ref、computed 等)

随着对任意api集的通用自动导入解决方案的扩展,对JSX组件进行自动导入也是有可能的。在unplugin-auto-import中,我们提供了相同的解决接口。

// vite.config.js
import { defineConfig } from 'vite'
import Icons from 'unplugin-icons/vite'
import AutoImport from 'unplugin-auto-import/vite'
import IconsResolver from 'unplugin-icons/resolver'

export default defineConfig({
  plugins: [
    /* ... */
    Icons({
      compiler: 'jsx'
    }),
    AutoImport({
      imports: [
        'react' // preset for react
      ],
      resolvers: [
        IconsResolver({
          prefix: 'Icon',
          extension: 'jsx'
        })
      ]
    })
  ]
})

对于react组件,也是受欢迎的

export function MyComponent() {
  return (
    <>
      <IconMdiAlarm />
      <IconFaBeer style="color: orange"/>
    </>
  )
}

总结

总结一下,这是文章中提到的解决方案

同时,你也能在我的上篇笔记中发现一些有帮助的事情:

  • icones - Icon Explorer for Iconify with Instant searching and exporting.
  • vscode-iconify - Iconify IntelliSense for VS Code.

如果你喜欢他们,你可能也想看下我的配置的Vue+Vite开发模板

Anthony Fu

Opens profile photo

Anthony Fu 大大的文章就这样结束了,里面有很多我暂时没有去深入了解的东西,所以翻译起来特别表面化。但是我已经会用了,粘贴一下我的代码

pnpm add @iconify/json unplugin-icons unplugin-vue-components

在vite中配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Icons from "unplugin-icons/vite";
import Components from 'unplugin-vue-components/vite';
import IconsResolver from 'unplugin-icons/resolver';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(),
    Icons({
      compiler: "vue3",
      scale: 1,
      defaultClass: "inline-block",
    }),
    Components({
      dts: "src/typings/components.d.ts",
      resolvers: [IconsResolver({ componentPrefix: "icon" })],
    }),
  ],
})

在 模板中 直接只用就好了,不需引入

<icon-akar-icons:github-fill @click="handleGithubLogin" />

效果:

image.png

后续会深入了解!!!

更新:unplugin-vue-components/vite 这个组件是不是会自动注册 components 文件夹下面的公共组件啊