如何搭建一个图标库或者图片库?

188 阅读8分钟

在繁忙的都市中,有一家名为“智慧星”的科技公司,老板李明是个对效率有着极高追求的人。有一天,李明发现设计师小赵和前端工程师小王在为一个项目的图片资源争吵,原因是图片管理混乱,导致工作重复和延误。
李明决定,是时候建立一个前端图片库了。
他召集了团队,提出了这个想法。大家一听,纷纷表示赞同。小赵说:“这样一来,我们找图就方便多了。”小王也补充道:“而且还能避免图片重复使用,提高项目质量。”
说干就干,李明亲自督战,小赵和小王联手,经过一周的紧张筹备,前端图片库终于建成。这个库包含了公司所有项目的图片资源,分类清晰,检索方便。
投入使用后,效果立竿见影。设计师们不再为找图而头疼,工程师们也能迅速找到所需资源。项目进度因此加快,客户满意度也随之提升。
李明看着大家忙碌而有序的身影,心中暗自得意。一天,他路过小赵的工位,听到小赵感慨地说:“这个图片库真是太棒了,以前找一张图得半天,现在几分钟就搞定了。”
小王也在旁边附和:“是啊,而且图片质量也有了保障,项目出错率明显降低。”
李明微笑着离开了,他知道,这个前端图片库不仅提高了工作效率,还增强了团队的凝聚力。从此,“智慧星”在行业内声名鹊起,成为了高效与品质的代名词。而这一切,都源于那个小小的前端图片库。

为什么需要搭建一个图标库?

先说一下我们这边的业务背景,我们这边的前端工程非常多,通过类微前端的形式整合到一起。所以会出现多个项目都引用一个图片资源,那么会浪费重复消耗资源存储空间和用户访问也会访问2个资源路径,无法实现资源整合和复用。(虽然说我觉得都放oss就可以了,但是老板喜欢...)

我们要做什么?

搞一个前端npm包,按照业务模块划分,整合项目中所用到的全部图片资源(svg,png...),并提供一个可视化界面,支持更新图片内容。

开搞,图标库!

先说一下思路,和哪些需要去做的?

  • 搞个图标库Npm包
  • 把项目的图片全部转成vue组件,每个组件对应一个图片
  • 不仅支持组件方式调用,也可以在当成css背景
  • 支持更多的文件类型,比如png,如何把png转成单个vue组件
  • 线上oss的图片资源,也可以按照组件的形式调用
  • 搞个图标库站点,方便查询图标和更新oss图片

工程搭建

这里选择使用pnpm搭建项目,方便管理多个工程,分别起3个目录:

  • sources - 图片资源
  • lib - 对图片资源进行打包相关代码
  • docs - 图片库文档 image.png

sources 图标资源

图标库,有一个很重要的问题,如何管理规划众多图片?推荐大家这里可以按照业务模块来创建目录,一个目录代表一个业务,如果有通用的图片可以统一规划到一个common或者base目录下。

lib 核心内容

lib工程主要做的事情就是读sources下面的图片,并把图片转成单vue组件,那么我们需要对不同的图片格式(svg,png,oss)进行分开处理。

svg

svg转成vue组件算是比较容易了,我们这边只需要把svg文件内容放到.vue文件的template里就可以了。
所以我们需要先找到我们的全部图片资源,根据图片类型进行分别处理。

import { emptyDir, ensureDir } from 'fs-extra'
import { findWorkspaceDir } from '@pnpm/find-workspace-dir'
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'

// 获取项目项目根目录
const pathRoot = resolve(dir, '..')
// 定义生成的vue组件生成目录
const pathComponents = resolve(pathRoot, 'components')
// 确定文件夹确实存在
await ensureDir(pathComponents)
// 清空文件夹
await emptyDir(pathComponents)
// 获取到全部的图片资源
async function getSvgFiles() {
   // 找到工程中所有的项目包
  const pkgs = await findWorkspacePackages(
    (await findWorkspaceDir(process.cwd()))!,
  )
  // 找到项目包名sources的图标资源工程
  const pkg = pkgs.find(
    (pkg) => pkg.manifest.name === 'sources',
  )!
  // 返回二级目录下所有的图片,比如 /sources/element-plus/apple.svg
  return glob('*/**.(svg|png|oss)', { cwd: pkg.dir, absolute: true })
}
// 获取到了全部图片资源
const files = await getSvgFiles()

获取到全部图片资源后,就要把图片资源转成vue文件。

import { type BuiltInParserName, format } from 'prettier'

async function transformToVueComponent(file: string) {
   // 在转vue文件之前,我们需要对文件进行处理,根据文件名称生成组件名,具体看下面
  const { componentName, extName, fileName } = await getName(file)
  // 图片资源内容
  let content = ''
  // 对svg进行处理
  if (extName === FILE_TYPE_SVG) {
     // 读取文件内容
    content = await readFile(file, 'utf-8')
  } else {
     // 其他逻辑代码...
  }

// 格式化代码
function formatCode(code: string, parser: BuiltInParserName = 'typescript') {
  return format(code, {
    parser,
    semi: false,
    singleQuote: true,
  })
}

// 根据代码模板生成.vue源码
  const code = await formatCode(
    `
    <template>
    ${content}
    </template>
    <script lang="ts" setup>
    defineOptions({
      name: ${JSON.stringify(componentName)},
    })

</script>`,
    'vue',
  )
  // 将代码写入到对应的vue文件内
  writeFile(path.resolve(pathComponents, `${componentName}.vue`), code, 'utf-8')
}
// 遍历每个文件进行转换
import camelcase from 'camelcase'

await Promise.all(files.map((file) => transformToVueComponent(file)))
// 对文件加工,获取我们需要的文件名,文件后缀
async function getName(file: string) {
  // 对文件路径进行拆分 
  const fileFragments = file.split('/')
  // 模块名称
  const mpName = file.split('/')[fileFragments.length - 2]
  // 图标类型
  const extName = path.extname(file)
  // 文件名称
  const fileName = path.basename(file).replace(extName, '')
  // 组件名称
  const componentName = camelcase(COMPONENT_SUFFIX + '-' + mpName + '-' + fileName, { pascalCase: true })
  return {
    fileName,
    componentName,
    mpName,
    extName
  }
}

这样每个svg图片,都会转个单个vue文件,具体效果如下。

1731576299095.png

png

对于png直接转成vue组件可能有点费劲,但是我们可以把png转成base64,再把base64嵌入到svg的image中。这种方法其实是不推荐的,因为转成base64会导致包体积成倍增加,推荐使用小体积的svg或者直接线上oss。

async function transformToVueComponent(file: string) {
   // ...
  if (extName === FILE_TYPE_SVG) {
    // svg处理...
  } else {
      // 处理其他图片格式
       const transformIcon = async (fileUrl, { extName, fileName }) => {
        return new Promise<any>((resolve, reject) => {
            fs.readFile(fileUrl, (err, data) => {
                if (err) {
                    reject(err)
                } else {
                    let code = ''
                    // 处理图片内容
                    if (extName === FILE_TYPE_PNG) {
                        // 转换为 Base64
                        const base64Image = `data:image/png;base64,${data.toString('base64')}`;
                        code = `<svg xmlns="http://www.w3.org/2000/svg">  <image 
                        width="100%" height="100%"
                         href="${base64Image}" ></image>
                         </svg>`
                        iconStr = base64Image
                    } else if (extName === FILE_TYPE_OSS) {
                         // ...
                    }
                    resolve({
                        code
                        })

                }
            },);
        })
}
// ... 其他操作不变

oss

对于oss,可以把oss的线上服务地址放到一个单文件中。在转化.vue过程中,读取文件内的线上服务地址,并赋值给一个image标签。对于svg和png有天然的文件后缀类型,那oss类型的文件,我们这边都起一个.oss后缀的文件来存储线上图片资源地址。 image.png

async function transformToVueComponent(file: string) {
   // ...
  if (extName === FILE_TYPE_SVG) {
    // svg处理...
  } else {
      // 处理其他图片格式
       const transformIcon = async (fileUrl, { extName, fileName }) => {
        return new Promise<any>((resolve, reject) => {
            fs.readFile(fileUrl, (err, data) => {
                if (err) {
                    reject(err)
                } else {
                    let code = ''
                    // 处理图片内容
                    if (extName === FILE_TYPE_PNG) {
                         // 处理png图片
                    } else if (extName === FILE_TYPE_OSS) {
                         // 处理oss
                         const ossUrl = data.toString('utf-8')
                         code = `<img src="${ossUrl}" />`
                    }
                    resolve({
                        code
                        })

                }
            },);
        })
}
// ... 其他操作不变

生成组件库入口

async function generatedEntryItemCode(file) {
  const { componentName } = await getName(file)
  // 根据componentName生成对应export语句
  return `export { default as ${componentName} } from './${componentName}.vue'`
}

async function generateEntry(files: string[]) {
  let code: any = await Promise.all(files.map(file => generatedEntryItemCode(file)))
  code = await formatCode(
    code.join('\n')
  )
  // 生成入口index.ts文件
  await writeFile(path.resolve(pathComponents, 'index.ts'), code, 'utf-8')
}

三种格式转成vue效果

  • oss

image.png

  • png

image.png

  • svg

1731637008863.jpg

  • 组件列表 1731642426370.png

添加声明内容或配置文件

后期图片越来越多的话,我们希望对每个图片添加一些描述内容,用于定义图片信息。或者我们需要对某些图片进行定制化,那我们也可以添加一些字段来控制。所以在添加图片的时候,我们可以先创建文件同名的json文件,在生成.vue文件的同时将json写入到vue文件中。

1731637944678.png

// 在原本的getName函数中加工一下,读取同名文件.json内部的配置
async function getName(file: string) {
  const fileFragments = file.split('/')
  // 模块名称
  const mpName = file.split('/')[fileFragments.length - 2]
  // 图标类型
  const extName = path.extname(file)
  // 文件名称
  const fileName = path.basename(file).replace(extName, '')
  // 组件名称
  const componentName = camelcase(COMPONENT_SUFFIX + '-' + mpName + '-' + fileName, { pascalCase: true })

  let description = {
    "mpName": mpName, // 模块名称
    "fileName": fileName,
    "label": '',
    'extName': extName.replace('.', ''),
    'componentName': componentName,
    "author": "", // 维护人
    "desc": "", // 该图标的相关描述
    "updateTime": "", // 最近一次更新时间
    "autoIconStr": false // 是否自动生成源内容(base64 ? http), 只能由false改为true,true改false请通知相关应用
  }
  const descriptionJsonPath = `${path.dirname(file)}/${fileName}.json`
  try {
    const result = await readFile(descriptionJsonPath, 'utf-8')
    description = {
      ...description,
      ...(JSON.parse(result))
    }
  } catch (error) {
    // consola.warn(`${mpName}/${fileName} 未定义说明文件!`)
  }
  return {
    fileName,
    componentName,
    mpName,
    extName,
    description
  }
}

重新打包下,看下效果:

image.png

如何当作背景图?

对应oss类型的图片资源,本身就是一个线上地址,我们可以直接使用当作背景url。那么svg和png呢,我们该怎么处理?其实可以把这2种类型都转成base64,然后在vue组件内暴露一个属性,专门存放base64或线上oss地址,这里其实更推荐线上oss地址,因为背景图之类的一般都是大图,如果转成base64会导致包体积越来越大。
每个vue文件定义一个iconStr属性存储base64或者oss地址。对于oss线上资源生成的vue文件体积不会太大,所以这里默认生成一个iconStr,而svg或者png如果转成base64的话文件会大很多,所以不是提倡默认都提供这个属性,而是通过上面提高的json配置文件添加一个autoIconStr属性。

// svg转base64
export function svgToBase64(svg) {
    const utf8Bytes = new TextEncoder().encode(svg);
    return 'data:image/svg+xml;base64,' + btoa(String.fromCharCode.apply(null, utf8Bytes));
}

// 对png和oss进行加工,返回vue源码和iconStr
export const transformIcon = async (fileUrl, { extName, fileName }) => {
    return new Promise<any>((resolve, reject) => {
        fs.readFile(fileUrl, (err, data) => {
            if (err) {
                reject(err)
            } else {
                let code = ''
                let iconStr = ''
                if (extName === FILE_TYPE_PNG) {
                    // 转换为Base64
                    const base64Image = `data:image/png;base64,${data.toString('base64')}`;
                    code = `<svg xmlns="http://www.w3.org/2000/svg">  <image 
                    width="100%" height="100%"
                     href="${base64Image}" ></image>
                     </svg>`
                     // png直接返回base64
                    iconStr = base64Image
                } else if (extName === FILE_TYPE_OSS) {
                    const ossUrl = data.toString('utf-8')
                    code = `<img src="${ossUrl}" />`
                    // oss直接返回线上地址
                    iconStr = ossUrl
                }
                resolve({
                    code,
                    iconStr
                })

            }
        },);
    })
}

// 转根据不同文件类型转成vue文件
async function transformToVueComponent(file: string) {
  const { componentName, extName, fileName, description } = await getName(file)
  let content = ''
  // 定义iconStr,默认为空
  let iconStr = ''
  // 读取json配置,是否自动生成iconStr或者图片类型为oss
  const addIconStr = description.autoIconStr || extName === FILE_TYPE_OSS
  if (extName === FILE_TYPE_SVG) {
    content = await readFile(file, 'utf-8')
    //将svg转成base64
    iconStr = addIconStr ? svgToBase64(content) : ""
  } else {
    const data = await transformIcon(file, {
      extName,
      fileName
    })
    content = data.code
    iconStr = data.iconStr
  }

  const code = await formatCode(
    `
<template>
${content}
</template>
<script lang="ts" setup>
defineOptions({
  name: ${JSON.stringify(componentName)},
  iconStr:${addIconStr ? `"${iconStr}"` : "''"},
  customOptions:${JSON.stringify({
      ...description,
    })}
})

</script>`,
    'vue',
  )
  writeFile(path.resolve(pathComponents, `${componentName}.vue`), code, 'utf-8')
}

图标库查询界面

这部分大家可以根据自己的业务需求,遍历上面的全部的vue组件,进行名称搜索或者展示。

image.png

image.png

end

这里搭建一个图标库,是为了方便团队内整合多个项目图片资源,方便进行复用和管理。大家如果有类似的需求,可以在上面的基础上进行加工,完成属于你自己的图标库。

coding库在这里,欢迎大家star: github.com/waltiu/npc-…