当前位置: 首页 > news >正文

一篇文章学会-图标组件库的搭建

1-1 构建源码bulid.ts

import path from 'node:path';
import chalk from 'chalk';
import consola from 'consola';
import { build, type BuildOptions, type Format } from 'esbuild';
import GlobalsPlugin from 'esbuild-plugin-globals';
import { emptyDir, copy } from 'fs-extra'; // 导入 copy 方法
import vue from 'unplugin-vue/esbuild';
import { version } from '../package.json';
import { pathOutput, pathSrc } from './paths';/*** 复制组件到 dist 目录*/
const copyComponents = async () => {const componentsSrcDir = path.resolve(pathSrc, 'components');const componentsDistDir = path.resolve(pathOutput, 'components');await emptyDir(componentsDistDir); // 清空目标目录await copy(componentsSrcDir, componentsDistDir); // 复制组件
};/*** 获取 esbuild 构建配置项* @param format 打包格式,分为 esm,iife,cjs*/
const getBuildOptions = (format: Format) => {const options: BuildOptions = {entryPoints: [path.resolve(pathSrc, 'index.js')],target: 'es2018',platform: 'neutral',plugins: [vue({isProduction: true,sourceMap: false,template: { compilerOptions: { hoistStatic: false } },}),],bundle: true,format,minifySyntax: true,banner: {js: `/*! maomao Icons v${version} */\n`,},outdir: pathOutput,};if (format === 'iife') {options.plugins!.push(GlobalsPlugin({vue: 'Vue',}));options.globalName = 'maomaoIcons';} else {options.external = ['vue'];}return options;
};/*** 执行构建* @param minify 是否需要压缩*/
const doBuild = async (minify: boolean = true) => {await Promise.all([build({...getBuildOptions('esm'),entryNames: `[name]${minify ? '.min' : ''}`,minify,}),build({...getBuildOptions('iife'),entryNames: `[name].iife${minify ? '.min' : ''}`,minify,}),build({...getBuildOptions('cjs'),entryNames: `[name]${minify ? '.min' : ''}`,outExtension: { '.js': '.cjs' },minify,}),]);
};/*** 开始构建入口,同时输出 压缩和未压缩 两个版本的结果*/
const buildBundle = () => {return Promise.all([doBuild(), doBuild(false)]);
};consola.log(chalk.blue('开始编译................................'));
consola.log(chalk.blue('清空 dist 目录................................'));
await emptyDir(pathOutput);
await copyComponents(); // 添加复制组件的逻辑
consola.log(chalk.blue('构建中................................'));
await buildBundle();
consola.log(chalk.green('构建完成。'));

1-2 代码生成模块generate.ts

import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import camelcase from 'camelcase';
import chalk from 'chalk';
import consola from 'consola';
import glob from 'fast-glob';
import { emptyDir, ensureDir } from 'fs-extra';
import { format, type BuiltInParserName } from 'prettier';
import { pathComponents, pathSvg } from './paths';/*** 从文件路径中获取文件名及组件名* @param file 文件路径* @returns*/
function getName(file: string) {const fileName = path.basename(file).replace('.svg', '');const componentName = camelcase(fileName, { pascalCase: true });return {fileName,componentName,};
}/*** 按照给定解析器格式化代码* @param code 待格式化代码* @param parser 解析器类型* @returns 格式化后的代码*/
async function formatCode(code: string, parser: BuiltInParserName = 'typescript') {return await format(code, {parser,semi: false,trailingComma: 'none',singleQuote: true,});
}/*** 将 file 转换为 vue 组件* @param file 待转换的 file 路径*/
async function transformToVueComponent(file: string) {const content = await readFile(file, 'utf-8');// 使用正则表达式提取 <svg> 标签及其内容const svgMatch = content.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);const svgContent = svgMatch ? svgMatch[0] : ''; // 如果没有匹配到,则返回空字符串const { fileName, componentName } = getName(file);// 获取相对路径const relativeDir = path.dirname(path.relative(pathSvg, file));const targetDir = path.resolve(pathComponents, relativeDir);// 确保目标目录存在await ensureDir(targetDir);const vue = await formatCode(`<template><div :style="iconStyle" class="icon"></div></template><script lang="ts" setup>import { defineProps,computed } from 'vue';import { parse } from 'svg-parser'; // 需要安装并导入svg-parser库const props = defineProps({fillColor: {type: String,default: 'currentColor',},size: {type: [String, Number],default: 32,},svgContent: {type: String,default: \`${svgContent}\`,},});const iconStyle = computed(() => {const parser = new DOMParser();const svgDoc = parser.parseFromString(props.svgContent, 'image/svg+xml');const svgElement = svgDoc.documentElement;const width = svgElement.getAttribute('width') || '100';const height = svgElement.getAttribute('height') || '100';const svgWidth = parseFloat(width);const svgHeight = parseFloat(height);const aspectRatio = svgWidth / svgHeight;const size = typeof props.size === 'string' ? parseFloat(props.size) : props.size;return {width: \`\${size}px\`,height: \`\${size / aspectRatio}px\`,backgroundImage: \`url('data:image/svg+xml;utf8,\${encodeURIComponent(props.svgContent)}')\`,backgroundSize: 'contain',backgroundRepeat: 'no-repeat',display: 'inline-block',};});</script><style scoped>.icon {/* 额外的样式可以在这里添加 */}</style>`,'vue');await writeFile(path.resolve(targetDir, `${fileName}.vue`), vue, 'utf-8');
}/*** 生成 components 入口文件*/
const generateEntry = async (files: string[]) => {// const elePlusIconsExport = `export * from '@element-plus/icons-vue'`;const entries: Record<string, string[]> = {};for (const file of files) {const { fileName, componentName } = getName(file);const relativeDir = path.dirname(path.relative(pathSvg, file));const entryPath = path.resolve(pathComponents, relativeDir, 'index.js');// 将每个组件名按目录分类if (!entries[relativeDir]) {entries[relativeDir] = [];}entries[relativeDir].push(`export { default as ${componentName} } from './${fileName}.vue'`);}// 为每个目录生成入口文件for (const [dir, componentEntries] of Object.entries(entries)) {// const code = await formatCode([...componentEntries, elePlusIconsExport].join('\n'));const code = await formatCode([...componentEntries].join('\n'));await writeFile(path.resolve(pathComponents, dir, 'index.js'), code, 'utf-8');}
};/*** 获取 svg 文件* 这里改为获取多个子目录下的svg文件*/
function getSvgFiles() {return glob('**/*.svg', { cwd: pathSvg, absolute: true });
}consola.log(chalk.blue('开始生成 Vue 图标组件................................'));
await ensureDir(pathComponents);
await emptyDir(pathComponents);
const files = await getSvgFiles();consola.log(chalk.blue('开始生成 Vue 文件................................'));
await Promise.all(files.map((file: string) => transformToVueComponent(file)));consola.log(chalk.blue('开始生成 Vue 组件入口文件................................'));
await generateEntry(files);
consola.log(chalk.green('Vue 图标组件已生成'));

1-3 路径变量 paths.ts

import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'// 目录相对路径
export const dirsRelativePath = {
root: '..',
src: 'src',
components: 'components',
svg: 'svg',
output: 'dist'
}// 当前程序执行的目录,即 build
const dir = dirname(fileURLToPath(import.meta.url))// 根目录
export const pathRoot = resolve(dir, dirsRelativePath.root)
// src 目录
export const pathSrc = resolve(pathRoot, dirsRelativePath.src)
// vue 组件目录
export const pathComponents = resolve(pathSrc, dirsRelativePath.components)
// svg 资源目录
export const pathSvg = resolve(pathRoot, dirsRelativePath.svg)
// 编译输出目录
export const pathOutput = resolve(pathRoot, dirsRelativePath.output)

2-1 构建说明

svg文件夹下是各项目所需要构建的svg图片文件

使用`npm run build`后,src目录下会构建出我们所需要的组件,和入口文件

 举例

project1下的asset-manage.vue,这是生成文件,由文件模板生成

<template><div :style="iconStyle" class="icon"></div>
</template>
<script lang="ts" setup>
import { defineProps, computed } from 'vue'
import { parse } from 'svg-parser' // 需要安装并导入svg-parser库const props = defineProps({fillColor: {type: String,default: 'currentColor'},size: {type: [String, Number],default: 32},svgContent: {type: String,default: `<svg t="1729500214124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4756" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M540.8 51.008l330.112 277.376 1.28 1.088 117.888 106.624a44.8 44.8 0 1 1-60.16 66.432l-53.12-48v484.16a44.8 44.8 0 0 1-44.8 44.8H192a44.8 44.8 0 0 1-44.8-44.8V454.4l-53.12 48.064a44.8 44.8 0 0 1-60.16-66.432l117.888-106.624 1.28-1.088L483.2 51.008a44.8 44.8 0 0 1 57.6 0zM512 143.808L236.8 375.04v518.848h550.4V375.04L512 143.872z m106.688 216.704a44.8 44.8 0 0 1 34.368 73.472l-45.44 54.528H640a44.8 44.8 0 1 1 0 89.6H556.8v38.4H640a44.8 44.8 0 1 1 0 89.6H556.8v83.2a44.8 44.8 0 1 1-89.6 0v-83.2H384a44.8 44.8 0 1 1 0-89.6h83.2v-38.4H384a44.8 44.8 0 1 1 0-89.6h32.32l-45.44-54.528a44.8 44.8 0 0 1 68.864-57.344L512 463.36l72.256-86.72a44.8 44.8 0 0 1 34.432-16.128z" p-id="4757"></path></svg>`}
})const iconStyle = computed(() => {const parser = new DOMParser()const svgDoc = parser.parseFromString(props.svgContent, 'image/svg+xml')const svgElement = svgDoc.documentElementconst width = svgElement.getAttribute('width') || '100'const height = svgElement.getAttribute('height') || '100'const svgWidth = parseFloat(width)const svgHeight = parseFloat(height)const aspectRatio = svgWidth / svgHeightconst size =typeof props.size === 'string' ? parseFloat(props.size) : props.sizereturn {width: `${size}px`,height: `${size / aspectRatio}px`,backgroundImage: `url('data:image/svg+xml;utf8,${encodeURIComponent(props.svgContent)}')`,backgroundSize: 'contain',backgroundRepeat: 'no-repeat',display: 'inline-block'}
})
</script>
<style scoped>
.icon {/* 额外的样式可以在这里添加 */
}
</style>

project1下的index.js

export { default as AssetManage } from './asset-manage.vue'
export { default as AvatarLine } from './avatar-line.vue'
export { default as Baodan12313 } from './baodan12313.vue'
export { default as ExitFullscreen } from './exit-fullscreen.vue'
export { default as PaperFrog } from './paper-frog.vue'
export { default as Registering } from './registering.vue'
export { default as StartFilled } from './start-filled.vue'

2-2 发包说明

重点解释:

此处代码可以根据项目图标目录中的文件,进行针对性导出,实现分包引入的效果,使得引入包大小不会太大。

.package.json
 

{"name": "icon-site-group","version": "1.0.2-beta.7","description": "项目分开目录管理(加回导出全部)","private": false,"type": "module","keywords": ["icons","图标"],"license": "ISC","author": "SuperYing","files": ["dist"],"main": "./dist/index.cjs","module": "./dist/index.js","types": "./dist/types/index.d.ts","exports": {".": {"types": "./dist/types/index.d.ts","require": "./dist/index.cjs","import": "./dist/index.js"},"./project1": "./dist/components/project1/index.js","./project2": "./dist/components/project2/index.js","./project3": "./dist/components/project3/index.js","./*": "./*"},"sideEffects": false,"scripts": {"build": "pnpm run build:generate && run-p build:build build:types","build:generate": "tsx build/generate.ts","build:build": "tsx build/build.ts","build:types": "vue-tsc --declaration --emitDeclarationOnly"},"peerDependencies": {"vue": "^3.4.21"},"dependencies": {"pnpm": "^9.12.2"},"devDependencies": {"@babel/types": "^7.22.4","@types/fs-extra": "^11.0.4","@types/node": "^20.11.25","camelcase": "^8.0.0","chalk": "^5.3.0","consola": "^3.2.3","console": "^0.7.2","esbuild": "^0.21.4","esbuild-plugin-globals": "^0.2.0","fast-glob": "^3.3.2","fs-extra": "^11.2.0","npm-run-all": "^4.1.5","prettier": "^3.2.5","tsx": "^4.7.1","typescript": "^5.4.2","unplugin-vue": "^5.0.4","vue": "^3.4.21","vue-tsc": "^2.0.6"}
}

3-1 图标库如何使用
 

npm install icon-site-group

单项目情况,需要进行组件注册

在main.js中引入

import * as MaoGroupIcons from 'icon-site-group/project1'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'tailwindcss/tailwind.css'
import { createApp } from 'vue'
import App from './App.vue'// console.log(MaoGroupIcons)/* 单项目展示以及调用的情况 */
const app = createApp(App);
Object.entries(MaoGroupIcons).forEach(([name, component]) => {app.component(name, component);
});
// console.log(MaoGroupIcons)
app.use(ElementPlus)// .use(MaoGroupIcons).mount('#app')

全量引入的情况下
 

import MaoGroupIcons from 'icon-site-group'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'tailwindcss/tailwind.css'
import { createApp } from 'vue'
import App from './App.vue'//此处传入project,用于注册指定项目下的图标组件
const app = createApp(App);
app.use(ElementPlus).use(MaoGroupIcons, { project: ['project1','project2','project3'] }).mount('#app')

4-1 打包大小
 

多项目引入占比大小

rollup-plugin-visualizer下的可视化打包页面📎stats.html

单项目引入占比大小

rollup-plugin-visualizer下的可视化打包页面📎stats.html

5-1 项目目录 

.
├── build
│   ├── build.ts
│   ├── generate.ts
│   └── paths.ts
├── src
│   ├── components
│   │   ├── project1│   │  │   ├──asset-manage.vue│   │   ├── project2│   │  │   ├──baodan.vue│   │  │   ├──bianji.vue│   │   └── project3│   │  │   ├── Check-Circle-Fill.vue│   └── index.js
├── svg
│   ├── project1
│   │   ├── asset-manage.svg
│   ├── project2
│   │   ├── baodan.svg
│   │   ├── bianji.svg
│   └── project3
│   ├── Check-Circle-Fill.svg├── tree.md
├── tsconfig.build.json
└── tsconfig.json


http://www.mrgr.cn/news/71302.html

相关文章:

  • 软考之面向服务架构SOA-通信方法
  • 使用最小花费爬楼梯(DP)
  • “乐鑫组件注册表”简介
  • 【C++习题】10.反转字符串中的单词 lll
  • 通过socket设置版本更新提示
  • Unity类银河战士恶魔城学习总结(P130 SkillTree UI 技能树)
  • 前端监控与埋点 全总结
  • 使用R语言survminer获取生存分析高风险和低风险的最佳截断值cut-off
  • python基础概念
  • 论分布式事务及其解决方案
  • Linux(CentOS 7) yum一键安装mysql8
  • 【Linux】注释和配置文件的介绍
  • 丹摩征文活动|智谱AI引领是实现文本可视化 - CogVideoX-2b 部署与使用
  • 访问网页的全过程(知识串联)
  • linux本地磁盘分区
  • IO作业5
  • 使用YOLOv9进行图像与视频检测
  • C++根据特定字符截取字符串
  • 蓝队基础知识和网络七层杀伤链
  • 中阳智能交易模型的进阶探索与实战应用
  • Webots控制器编程
  • 最高提升20倍吞吐量!豆包大模型团队发布全新 RLHF 框架,现已开源!
  • 根据日志和指标构建更好的服务水平目标 (SLOs)
  • 006配置DHCP服务器
  • RAT 无线接入技术
  • Servlet生命周期