本文是Varlet组件库源码主题阅读系列的第三篇。阅读本文后,您可以了解如何将svg图标转换为字体图标文件,以及如何设计一个简单的Vue图标组件。Varlet提供了一些常用的图标,图标全部来自MaterialDesignIcon。ConvertSVGtoFontIcon图标的原文件是svg格式的,但是最后作为字体图标使用,所以需要进行转换操作。图标由单独的包处理,目录为/packages/varlet-icons/,提供可执行文件:打包命令为:接下来我们仔细看看lib/index.js文件做了什么。//lib/index.jsconstcommander=require('commander')commander.command('build').description('Buildvarleticonsfromsvg').action(build)commander.parse()使用命令行交互工具commander提供了构建命令,处理函数为build://lib/index.jsconstwebfont=require('webfont').defaultconst{resolve}=require('path')constCWD=process.cwd()constSVG_DIR=resolve(CWD,'svg')//svg图标目录constconfig=require(resolve(CWD,'varlet-icons.config.js'))//配置文件asyncfunctionbuild(){//取出与配置文件相关configconst{base64,publicPath,namespace,fontName,fileName,fontWeight='normal',fontStyle='normal'}=configconst{ttf,woff,woff2}=awaitwebfont({files:`${SVG_DIR}/*.svg`,//待转换的svg图标fontName,//字体名称,即css的font-familyformats:['ttf','woff','woff2'],//要生成的字体图标类型fontHeight:512,//ou的高度tputfont(默认为最高输入图标的高度)descent:64,//固定字体的基线})}varlet-icons的配置如下://varlet-icons.config.jsmodule.exports={namespace:'var-icon',//css类名的命名空间fileName:'varlet-icons',//生成的文件名fontName:'varlet-icons',//字体名base64:true,}核心是利用webfont包将多个svg文件转成字体文件,webfont的工作原理可以通过其文档上的依赖描述大致看出:使用svgicons2svgfont包将多个svg文件转换为一个svg字体文件,什么是svg字体,类似如下:每个单独Thesvgfile上面会转化为一个glyph元素,所以上面svg定义了一个名为geniconsfont的字体,里面包含两个字符图形,我们可以通过在glyph上定义的Unicode编码来使用这个glyphD详情要了解svg字体,请阅读SVG_fonts。前端html、css、js中同一个Unicode使用的格式是不一样的。在html/svg中,格式是dddd;或hhhh;,表示后面的四位十进制值,表示四位十六进制值;在css中,格式为\hhhh,以反斜杠开头;在js中,格式是\uhhhh,以\u开头。转换为svg字体后,使用几个字体转换库将其转换为各种类型的字体文件。至此,字体文件就生成了,但是事情还没有结束。//lib/index.jsconst{writeFile,ensureDir,removeSync,readdirSync}=require('fs-extra')constDIST_DIR=resolve(CWD,'dist')//打包输出目录constFONTS_DIR=resolve(DIST_DIR,'fonts')//输出字体文件目录constCSS_DIR=resolve(DIST_DIR,'css')//输出css文件目录//先删除输出目录removeSync(DIST_DIR)//创建输出目录awaitPromise.all([ensureDir(FONTS_DIR),ensureDir(CSS_DIR)])清除最后的结果,创建指定目录,继续://lib/index.jsconsticons=readdirSync(SVG_DIR).map((svgName)=>{consti=svgName.indexOf('-')constextIndex=svgName.lastIndexOf('.')return{name:svgName.slice(i+1,extIndex),//图标名称pointCode:svgName.slice(1,i),//图标代码}})consticonNames=icons.map((iconName)=>`"${iconName.name}"`)读取svg文件目录,遍历所有svg文件,从文件中提取图标名称和图标代码姓名。svg文件的名字有一个固定的格式:uFxxx是图标的Unicode码,后面是图标名,name是我们最终使用的时候的CSS类名,而这个Unicode实际上映射到某个图形中字体。一个字体其实就是一个“code-glyph”映射表,比如最终生成的css中的css类名:.var-icon-checkbox-marked-circle::before{content:"\F000";}var-icon是一个命名空间,通过伪元素防止与UnicodeF000冲突和显示字符。这个协议是svgicons2svgfont规定的:如果我们不自定义图标的Unicode,默认从E001开始。在Unicode中,E000-F8FF区间是没有定义字符的,这个区间是供我们使用private-use-area:接下来是生成css文件的内容://lib/index.js//commonjs格式:导出所有图标的css类名constindexTemplate=`\module.exports=[${iconNames.join(',\n')}]`//esm格式:导出所有图标的css类名constindexESMTemplate=`\exportdefault[${iconNames.join(',\n')}]`//css文件的内容constcssTemplate=`\@font-face{font-family:"${fontName}";src:url("${base64?`data:application/font-woff2;charset=utf-8;base64,${Buffer.from(woff2).toString('base64')}`:`${publicPath}${fileName}-webfont.woff2`}")format("woff2"),url("${base64?`data:application/font-woff;charset=utf-8;base64,${woff.toString('base64')}`:`${publicPath}${fileName}-webfont.woff`}")format("woff"),url("${base64?`data:font/truetype;charset=utf-8;base64,${ttf.toString('base64')}`:`${publicPath}${fileName}-webfont.ttf`}")format("truetype");font-weight:${fontWeight};font-style:${fontStyle};}.${namespace}--set,.${namespace}--set::before{position:relative;display:inline-block;font:normalnormalnormal14px/1"${fontName}";font-size:inherit;text-rendering:auto;-font-平滑:抗锯齿;}${icons.map((icon)=>{return`.${namespace}-${icon.name}::before{content:"\\${icon.pointCode}";}`}).join('\n\n')}`很简单,拼接生成并导出js文件和css文件的内容,最后写入文件://lib/index.jsawaitPromise.all([writeFile(resolve(FONTS_DIR,`${fileName}-webfont.ttf`),ttf),writeFile(resolve(FONTS_DIR,`${fileName}-webfont.woff`),woff),writeFile(resolve(FONTS_DIR,`${fileName}-webfont.woff2`),woff2),writeFile(resolve(CSS_DIR,`${fileName}.css`),cssTemplate),writeFile(resolve(CSS_DIR,`${fileName}.less`),cssTemplate),writeFile(resolve(DIST_DIR,'index.js'),indexTemplate),writeFile(resolve(DIST_DIR,'index.esm.js'),indexESMTemplate),])我们只需要导入varlet-icons.css或者less文件就可以直接在任意元素上使用icon图标组件字体icon通过对应的类名来使用,但是Varlet也提供了一个图标组件Icon,支持字体图标和传入图片:实现也很简单:viacomponent对于动态组件,根据传入的name属性判断渲染img标签还是i标签,如果是图片,nextName为图片url,否则nextName为图标类名。n方法用于拼接BEM风格的css类名,classes方法主要用于支持三元表达式,所以上面的:[isURL(name),n('image'),`${namespace}-${nextName}`]其实是一个三元表达式,为什么不直接用三元表达式呢,我不知道,也许这样更方便。const{n,classes}=createNamespace('icon')exportfunctioncreateNamespace(name:string){constnamespace=`var-${name}`//返回BEM样式类名constcreateBEM=(suffix?:string):string=>{if(!suffix)returnnamespacereturnsuffix.startsWith('--')?`${namespace}${suffix}`:`${namespace}__${suffix}`}//处理css类数组constclasses=(...classes:Classes):any[]=>{returnclasses.map((className)=>{if(isArray(className)){const[condition,truthy,falsy=null]=className返回条件?truthy:falsy}returnclassName})}return{n:createBEM,classes,}}支持设置图标大小:如果是图片,设置宽高,否则设置字体大小:支持颜色设置,当然只支持字体图标:支持图标切换动画。当设置transition(ms),通过图标名称切换图标时,可以触发切换动画:具体实现就是监听name属性的变化,然后添加一个改变元素属性的css类名。具体来说,这里是一个缩小为0的设置Classname--shrinking:.var-icon{&--shrinking{transform:scale(0);}}然后通过css的transition设置transition属性,使其以动画的方式收缩为0,动画结束后更新nextName为name属性的值,即成为一个新的图标,并且然后去掉css类名,通过动画恢复到原来的大小模板>constnextName:Ref=ref('')const收缩:Ref=ref(false)consthandleNameChange=async(newName:string|undefined,oldName:string|undefined)=>{const{transition}=props//在初始情况或者如果没有经过过渡时间,则不会有动画if(oldName==null||toNumber(transition)===0){nextName.value=newNamereturn}//添加收缩为0的那个cssclassnameshrinking.value=trueawaitnextTick()//移除类名并在收缩动画结束后更新图标setTimeout(()=>{oldName!=null&&(nextName.value=newName)//恢复到原尺寸缩水.value=false},toNumber(transition))}watch(()=>props.name,handleNameChange,{immediate:true})图标组件的实现比较简单,详解图标部分到此为止。下次见~