1

背景

在微信小程序开发过程中难免会遇到需要使用多色icon的场景,项目中的icon一般存放在iconfont上。

iconfont有三种引用方式(参考https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.d8cf4382a&helptype=code)

  • unicode引用:因为是字体,所以不支持多色
  • font-class引用:本质上还是使用的字体,所以多色图标还是不支持的
  • symbol引用:支持多色,使用svg渲染

但是微信小程序iconfont并不支持<svg>标签,只支持以background形式渲染.svg文件。基于此,可以使用Node编写脚本,将iconfont项目里的多色icon下载到项目中,生成

.arrow-down--colorful {
    background: url('./arrow-down.[hash].svg');
}

// 或者有CDN的可以
.arrow-down--colorful {
    background: url('//[cdn]/imgs/arrow-down.[hash].svg');
}

步骤

步骤一、 上传icon至iconfont

注意在上传时,带颜色的图标需要有固定后缀来区分。例如普通icon为icon-name,则带颜色的icon需要为icon-name--colorful

步骤二、脚本下载icon_**.css

脚本下载icon_**.css

使用Node下载项目对应的css脚本,格式为:

@font-face {font-family: "iconfont";
  src: url('//at.alicdn.com/t/font_**.eot?t=1571134856668');
  src: url('//at.alicdn.com/t/font_385238_**.eot?t=1571134856668#iefix') format('embedded-opentype'),
  url('data:application/x-font-woff2;**') format('woff2'),
  url('//at.alicdn.com/t/font_**.woff?t=**') format('woff'),
  url('//at.alicdn.com/t/font_**.ttf?t=**') format('truetype'),
  url('//at.alicdn.com/t/font_385238_**.svg?t=**#iconfont') format('svg');
}

.iconfont {
  font-family: "iconfont" !important;
  ...
}

.icon-name:before {
  content: "\e600";
}

.icon-name--colorful:before {
  content: "\e653";
}

删除其中无用的font格式,以及带--colorful的css定义。使用脚本将其格式化为

@font-face {font-family: "iconfont";
  src: url('data:application/x-font-woff2;**') format('woff2');
}

.iconfont {
  font-family: "iconfont" !important;
  ...
}

.icon-name:before {
  content: "\e600";
}

步骤三、脚本下载icon_**.js

脚本下载icon_**.js
使用Node下载项目对应的js脚本,格式为:

!function(o){var t,l='<svg><symbol id="icon-name” viewBox="0 0 1024 1024"><path …>…</path></symbol>symbol id="icon-name—colorful” viewBox="0 0 1024 1024"><path …>…</path></symbol></svg>’,…}(window);

提取出其中带颜色(id以--colorful结尾)的<symbol></symbol>标签,使用xml解析工具,得到

<path d="..." fill="..." ></path><path d="..." fill="..." ></path>
注意:如果步骤一不能实现按照--colorful区分是否是多色icon,则需要分析各个path的颜色是否一致来区分是否是多色。

拼接上<svg></svg>标签

function getHash(cont: string) {
  return cryptoNode
    .createHash('md5')
    .update(cont)
    .digest('hex')
    .substr(0, 8);
}

步骤四、生成hash值及图片文件

最终得到的svgStr的hash值

function getHash(cont: string) {
  return cryptoNode
    .createHash('md5')
    .update(cont)
    .digest('hex')
    .substr(0, 8);
}
使用hash值是为了避免更换icon但是不换名字的场景

生成本地图片文件

const hash = getHash(svgStr);
const fileName = `${iconId}.${hash}.svg`;
const filePath = `${stylePath}${fileName}`;
fs.writeFileSync(filePath, svgStr);

步骤五、将图片上传至CDN并删除本地文件(可选)

如果有CDN资源可以将图片上传至CDN,可以节约打包出的项目体积

uploadFileToCDN(filePath);

/** 将pathWithHash文件上传到CDN */
async function uploadFileToCDN(pathWithHash: string) {
  return new Promise((resolve, reject) => {
    exec(
      `${此处为上传至CDN的命令行}`,
      (error: any, stdout: any, stderr: any) => {
        if (error) {
          console.error(`exec error: ${error}`);
          reject(error);
          return;
        }
        resolve('');
        /** 删除文件 */
        fs.unlinkSync(pathWithHash);
      }
    );
  });
}

步骤六、生成完整的css内容并写入本地

给步骤二的css文件拼接上带颜色图片的css内容

@font-face {font-family: "iconfont";
  src: url('data:application/x-font-woff2;**') format('woff2');
}

.iconfont {
  font-family: "iconfont" !important;
  ...
}

.icon-name:before {
  content: "\e600";
}

.icon-name--colorful:before {
  background: url('cdnUrl');
}

并写入本地

fs.writeFileSync(cssPath, fileContent.replace(/"/g, "'"), 'utf8');

完整的脚本文件示例

const http = require('http');
const cryptoNode = require('crypto');
const { exec } = require('child_process');
const xml2js = require('xml2js');
const fs = require('fs');
const config = {
  url: '//at.alicdn.com/t/font_***.js'
};
// 上传至CDN后的前缀
const cdnPath = '***';
/** 生成的css文件path */
const cssPath = 'src/styles/iconfont.scss';
const stylePath = 'src/styles/';

const { parseString } = xml2js;

// 替换iconfont.scss
const iconfontUrl = config.url.replace(/.*\/\//, 'http://');
const iconfontUrl_css = iconfontUrl.replace('.js', '.css');

let fileContent = '';

type IPath = {
  $: {
    [key: string]: string;
  };
};

type ISymbol = {
  $: {
    id: string;
    viewBox: string;
  };
  path: IPath[];
};

/** 读取css文件,去掉远程连接 */
async function generateFile() {
  const cssData = await readRemoteFile(iconfontUrl_css);
  fileContent = cssData.replace(/[^,;]*at.alicdn.com.*\*\//g, '');
  // 替换掉woff
  fileContent = fileContent.replace(/[^)]*at.alicdn.com.*\),/g, ';');
  // 替换掉colorful
  fileContent = fileContent.replace(/[^}]*colorful[^}]*}/g, '');
  // 替换src
  fileContent = fileContent.replace('url(', 'src: url(');
  // 加换行
  fileContent = fileContent.replace('{font-family', '{\n  font-family');
  // 加换行
  fileContent = fileContent.replace(') format(', ')\n    format(');
  // 去除最后一个换行
  fileContent = fileContent.replace(/\n$/, '');
  // 有颜色的图标生成background: url('cdnUrl')
  const fontXml = await fetchXml(iconfontUrl);
  fontXml.svg.symbol.forEach((item: ISymbol) => {
    const iconId = item.$.id;
    if (/colorful/.test(iconId)) {
      let svgStr = '';
      item.path.forEach((path: IPath) => {
        const keys = Object.keys(path.$);
        let attrStr = '';
        keys.forEach(k => {
          attrStr += `${k}="${path.$[k]}" `;
        });
        svgStr = `${svgStr}<path ${attrStr}></path>`;
      });
      svgStr = `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">${svgStr}</svg>`;
      const hash = getHash(svgStr);
      const fileName = `${iconId}.${hash}.svg`;
      const filePath = `${stylePath}${fileName}`;
      fs.writeFileSync(filePath, svgStr);
      uploadFileToCDN(filePath);
      svgStr = `\n.${iconId} {\n  background: url('${cdnPath}${fileName}');\n}\n`;
      fileContent += svgStr;
    }
  });
}

function getHash(cont: string) {
  return cryptoNode
    .createHash('md5')
    .update(cont)
    .digest('hex')
    .substr(0, 8);
}

async function uploadAndUpdate() {
  // 写文件
  fs.writeFileSync(cssPath, fileContent.replace(/"/g, "'"), 'utf8');
}

generateFile().then(() => {
  uploadAndUpdate();
});

function readRemoteFile(remoteUrl: string): Promise<string> {
  let _data = '';
  return new Promise(resolve => {
    http.get(remoteUrl, (res: any) => {
      //数据
      res.on('data', function(chunk: string) {
        _data += chunk;
      });

      res.on('end', function() {
        resolve(_data);
      });
    });
  });
}

async function fetchXml(
  url: string
): Promise<{
  svg: {
    symbol: ISymbol[];
  };
}> {
  const data = await readRemoteFile(url);
  const matches = String(data).match(/'<svg>(.+?)<\/svg>'/);
  return new Promise((resolve, reject) => {
    parseString(`<svg>${matches ? matches[1] : ''}</svg>`, (err: any, result: any) => {
      if (err) {
        reject(err);
      } else {
        resolve(result);
      }
    });
  });
}

/** 将pathWithHash文件上传到CDN */
async function uploadFileToCDN(pathWithHash: string) {
  return new Promise((resolve, reject) => {
    exec(`${此处为上传至CDN的命令行}`, (error: any, stdout: any, stderr: any) => {
      if (error) {
        console.error(`exec error: ${error}`);
        reject(error);
        return;
      }
      resolve('');
      /** 删除文件 */
      fs.unlinkSync(pathWithHash);
      console.log(`stdout: ${stdout}`);
      console.error(`stderr: ${stderr}`);
    });
  });
}

再在package.json中的scripts中配置

"icon": "ts-node scripts/iconfont.ts"

则每次更新iconfont后,更新对应的url,再运行npm run icon即可


深蓝一人
1.6k 声望65 粉丝

暂时没有个人简介