背景:接受二手项目会开发周期较长得项目,会忘掉自己引证了哪些iconfont的icon内容。了解引进的icon需求到iconfont官网登陆查看,不同开发者可能还不知道对方引证了哪些icon。新增iconfont时,又会影响到原有的icon内容。

问题:

1.iconfont.js是紧缩后的代码,不够直观化,运用icon的名字进程太过繁琐,甚至还无从查找。

2.对已有iconfont.js保护体验不友好,不同开发人员运用下载多份iconfont.js容易抵触。

上一篇文章:IconView——在项目中可视化iconfont.js中初始化了插件和进行了初步的初始化展现,接下来持续改造插件IconView,增加一些保护功用:新增、删去和去色。

前置常识

iconfont.js引进后进程

iconfont.js是一个立即履行函数,作用是将svg连同icon定义symbol刺进document的body中。文章称为svgContainer,首要格局如下:

<svg >
  <symbol id=“icon1”>
    <!--具体的内容-->
  </symbol>
</svg>

在实际运用中,引证symbol对应的id

<svg >
    <use xlinkHref="#icon1"></use>
</svg>

这儿能够学习这个思路用于后续的icon新增。

svg元素新增

svg运用XML规范,创立svg元素需求运用createElementNS,传入命名空间http://www.w3.org/2000/svg

const ns = 'http://www.w3.org/2000/svg'; // 元素命名空间           
let el = document.createElementNS(ns, 'svg');

svg的currentColor

svg的fill特点或者path的fill特点设置为currentColor,会继承父元素的色彩,在运用icon需求设置自定义色彩很有必要。iconfont官网的的去色功用就是去除原有的色彩。

新增icon

这儿选用导入文件的来新增,文件支持两种格局:iconfont.js文件和简单格局的svg文件。

index.html模板中新增一个翻开本地文件夹的input:

<div>
  新增icons:<input type="file" accept=".js,.svg" aceept @change="importLocalFile" ref="refFile" id="file">
</div>

调用importLocalFile读取文件内容,然后调用后台getIconfontList来解析文本内容

importLocalFile: function () {
  const selectedFile = this.$refs.refFile.files[0];
  const fr = new FileReader()
  const that = this;
  fr.onload = function (e) {
    const result = e.target.result;
    const name = selectedFile?.name;
    if (name?.indexOf('.js') > -1) { // 处理.js文件
      callVscode({ cmd: 'vscode:getIconfontList', fileOrigin: 'content', content: result }, (data) => {
        if (data && data.icons) {
          that.addIconsToView(data.icons)
        }
      });
    } else if (name?.indexOf('.svg') > -1) { // 处理.svg文件
      callVscode({ cmd: 'vscode:getIconfontBySvg', fileOrigin: 'content', content: result, name: name?.substring(0, name.length - 4) }, (data) => {
        if (data && data.icons) {
          if (data && data.icons) {
            that.addIconsToView(data.icons)
          }
        }
      });
    }
  }
  fr.readAsText(selectedFile)
},

在前面提到的messageHandler新增一个getIconfontList办法:

  /** 获取一切的icon */
  async getIconfontList(global: any, message: any) {
    if (message?.fileOrigin === 'currentFile') {
      const currentFile = global.currentFile;
      if (fs.existsSync(currentFile)) {
        const resources = await getLocalFileFs(currentFile)
        const icons = parseIconfontJs(resources)
        global.icons = icons;
        invokeCallback(global.panel, message, { icons });
      }
    } else if (message?.fileOrigin === 'content' && message?.content) {
      const icons = parseIconfontJs(message?.content)
      invokeCallback(global.panel, message, { icons });
    }
  },

这儿趁便改造了webview初始化获取本地的iconfont.js操作,前面选用的是引进.js文件刺进symbol元素,这次统一选用解析文件中的字符串动态新增,便于后续的其他操作。fileOrigin区别是项目文件途径仍是文本内容。最终都是调用parseIconfontJs解析文本内容。

parseIconfontJs函数

/* 解析iconfont读取的文本 */
export function parseIconfontJs(content: string) {
  // 运用正则匹配解析svg,symbol等元素,获取id和name
  const res = content.match(new RegExp('<svg>.*</svg>'))
  let icons: any = [];
  if (res) {
    // 解分出一切的symbol标签
    const symbolStrList = res[0].match(new RegExp('<symbol.+?</symbol>', 'g'));
    symbolStrList?.forEach((v) => {
      // 提取id
      const id = v.match(new RegExp(/(?<=id=['"]).+?(?=['"])/));
      if (id && id[0]) {
        icons.push({
          id: id[0],
          name: id[0],
          symbolStr: v,
        })
      }
    })
  }
  return icons;
}

解析svg的getIconfontBySvg,提取相关特点构建symbol格局。

async getIconfontBySvg(global: any, message: any) {
  const icons = parseIconfontSvg(message?.content, message?.name)
  invokeCallback(global.panel, message, { icons });
},
/* 解析.svg文件的文本 */
function parseIconfontSvg(content: string,name:string) {
  const svgRes = content.match(new RegExp('<svg.+?</svg>'))
  let icons: any = [];
  if (svgRes) {
    const svgContent = svgRes[0].match(new RegExp('(?<=<svg.+?>).+?(?=</svg>)', 'g'));
    // 提取viewBox,不然icon展现反常
    const viewBoxStr = svgRes[0].match(new RegExp(`viewBox=['"].+?["']`, 'g'));
    if (svgContent) {
      const id =  name || randomStrs();
      icons.push({
        id:id,
        name: id,
        symbolStr: `<symbol id="${id}" ${viewBoxStr}>${svgContent}</symbol>`,
      })
    }
  }
  return icons;
}

webview拿到后台解分出来的icons,就需求动态新增svgContainer,先看importLocalFile调用的回调,这儿先是解析新增icons,区别出id抵触和未抵触的icons。然后将icon注入svgContainer。

 addIconsToView: function (icons) {
            if (document) {
                // 解析抵触和非抵触,用于接下来的新增
                const {
                    repeatIcons,
                    noRepeatIcons,
                } = checkRepeatTypeIcon(this.icons, icons);
                this.addIconsToSvgContainer([...repeatIcons, ...noRepeatIcons])
                setTimeout(() => {
                    this.addIcons = {
                        repeatIcons,
                        noRepeatIcons
                    }
                    this.modalOpen = true;
                }, 1000)
            }
        },

addIconsToSvgContainer函数,包括新建svgcontainer和刺进icon的symbol元素。

/* 讲icon以symbol方式刺进svg容器中 */
        addIconsToSvgContainer: function (icons, isEmpty) {
            if (document) {
                // 获取第svg节点,不存在则新增一个svgContainer
                const svgContainerId = 'iconview-svg'
                let svgContainer = document.getElementById(svgContainerId);
                const ns = 'http://www.w3.org/2000/svg'; // 元素命名空间
                if (!svgContainer) {
                    let el = document.createElementNS(ns, 'svg');
                    el.setAttribute('id', svgContainerId);
                    el.setAttribute('style', 'position: absolute; width: 0px; height: 0px; overflow: hidden;');
                    el.setAttribute('aria-hidden', 'true');
                    let rootDom = document.getElementById('root');
                    document.body.insertBefore(el, rootDom);
                }
                svgContainer = document.getElementById(svgContainerId);
                // 在svg容器中刺进symbol标识
                if (svgContainer) {
                    // 清空原有的icon
                    if (isEmpty) {
                        while (svgContainer.hasChildNodes()) {
                            svgContainer.removeChild(svgContainer.firstChild)
                        }
                    }
                    icons.forEach((v) => {
                        let div = document.createElementNS(ns, "div");
                        div.innerHTML = v.symbolStr;
                        svgContainer.appendChild(div.children[0])
                    })
                }
            }
        },

icon抵触处理

前面解析新增的addIcons的时候,进行了icon的抵触解析checkRepeatTypeIcon

/** 检查新增icons 是否重复 */
function checkRepeatTypeIcon(icons, newIcons) {
    const repeatIcons = [];
    const noRepeatIcons = [];
    const allIconsTemp = icons.slice();
    newIcons.forEach((v) => {
        const icon = allIconsTemp.find((c) => c.id === v.id);
        // 重复的type, icon保留原id,履行兼并战略会运用到
        if (icon) {
            const newId = creatUniqNamefromList(allIconsTemp.filter((v) => v.id === icon.id).map((v) => v.id), v.id);
            repeatIcons.push({
                oldId: v.id,
                name: `${newId}`,
                oldName: v.name,
                id: newId,
                symbolStr: v.symbolStr.replace(icon.id, newId)
            })
        } else {
            allIconsTemp.push(v);
            noRepeatIcons.push(v)
        }
    })
    return {
        repeatIcons,
        noRepeatIcons,
    }
}
/** 从序列中创立新的id名 */
function creatUniqNamefromList(originName, repeatNames) {
    let index = 2;
    let newName = `${originName}${index}`
    while (repeatNames.includes(newName)) {
        index += 1;
        newName = `${originName}${index}`
    }
    return newName;
}

以上进程是从导入文件到解分出icon是数据并刺进document中。数据准备好了,接下来需求在页面上进行展现了,新建一个弹窗组件来展现新增的icons。

Vue.component('icon-modal', {
    props: {
        open: {
            type: Boolean,
            default: false,
        },
        title: {
            type: String,
            default: '',
        },
        onOk: {
            type: Function,
            default: null,
        },
        onCancel: {
            type: Function,
            default: null,
        },
        okText: {
            type: String,
            default: '确定',
        }
    },
    data() {
        return {}
    },
    computed: {
        useId: function () {
            // 用于展现的id需求加上#
            return `#${this.id}`
        }
    },
    methods: {
        onCancelHandler() {
            if(this.onCancel){
                this.onCancel();
            }
        },
        onOkHandler(){
            if(this.onOk){
                this.onOk();
            }
        }
    },
    template: `<div>
        <div  v-if="open" class="--ch-icon-modal-back" v-on:click="onCancelHandler()"></div>
        <div  v-if="open" class="--ch-icon-modal">
            <h3>{{title}}</h3>
            <div class="--ch-icon-modal-content">
                <slot></slot>
            </div>
            <div class="--ch-icon-modal-footer">
                <button class="--ch-default-btn --ch-btn --ch-icon-modal-cancel-btn" v-on:click="onCancelHandler()">撤销</button>
                <button class="--ch-primary-btn  --ch-btn" v-on:click="onOkHandler()">{{okText}}</button>
            <div>
            <div class="--ch-icon-modal-cancel-icon"  v-on:click="onCancelHandler()">x</div>
        </div>
    <div>
    `
})

在index.html中新增

<icon-modal
  title="新增icons"
  ok-text="确定"
  v-bind:open="modalOpen"
  v-bind:on-cancel="onCancel"
  v-bind:on-ok="onOk"
  >
  <div v-if="hasRepeatIcons">
    <div
      v-for="item in conflictSolveMode"
      :key="item.key"
      >
      <input type='radio' :id="item.key" :value='item.key' v-model='selectedconflictMode'/>
      <label :for='item.key'>{{item.label}}</label>
    </div>
  </div>
  <h4 v-if="addIcons.repeatIcons.length > 0">抵触的icons</h4>
  <div v-if="addIcons.repeatIcons.length > 0" class="--ch-icon-list" >
    <div
      v-for="item in addIcons.repeatIcons"
      :key="item.id"
      class="--ch-icon-item-wrapper"
      >
      <icon-item
        v-bind:id="item.id"
        v-bind:name="item.name"
        v-bind:item="item"
        ></icon-item>
    </div>
  </div>
  <h4 v-if="addIcons.noRepeatIcons.length > 0">未抵触icons</h4>
  <div v-if="addIcons.noRepeatIcons.length > 0" class="--ch-icon-list" >
    <div
      v-for="item in addIcons.noRepeatIcons"
      :key="item.id"
      class="--ch-icon-item-wrapper"
      >
      <icon-item
        v-bind:id="item.id"
        v-bind:name="item.name"
        ></icon-item>
    </div>
  </div>
</icon-modal>

作用如图

IconView——在项目中维护iconfont.js

这儿提供了三种兼并的战略:

(1)仅兼并未抵触:仅新增下方id未抵触的icon

(2)兼并抵触并重命名:运用抵触中的新id兼并抵触和未抵触的icon

(3)兼并抵触并掩盖:抵触icon运用原icon来掩盖原有的icon以及兼并未抵触icon

以下是对应战略的处理代码

onOk: function () {
  const mode = this.selectedconflictMode;
  const newIcons = [];
  this.addIcons.noRepeatIcons = this.addIcons.noRepeatIcons.map((v) => ({ ...v, isNew: true }))
  if (this.addIcons.repeatIcons && this.addIcons.repeatIcons.length > 0) {
    // 设置新增标识,便于区别
    this.addIcons.repeatIcons = this.addIcons.repeatIcons.map((v) => ({ ...v, isNew: true }))
    switch (mode) {
      case 'onlyNoRepeat':
        newIcons.push(...this.addIcons.noRepeatIcons, ...this.icons)
        break;
      case 'mergeRepeatReName':
        newIcons.push(...this.addIcons.noRepeatIcons, ...this.addIcons.repeatIcons, ...this.icons)
        break;
      case 'mergeRepeatUpdate':
        let memoIcons = this.icons.reduce((memo, c) => {
          memo[c.id] = c;
          return memo;
        }, {})
        memoIcons = this.addIcons.repeatIcons.reduce((memo, c) => {
          memo[c.oldId] = c;
          return memo;
        }, memoIcons)
        newIcons.push(...Object.values(memoIcons), ...this.addIcons.noRepeatIcons)
        break;
      default:
        break;
    }
  } else {
    newIcons.push(...this.addIcons.noRepeatIcons, ...this.icons)
  }
  this.icons = newIcons;
  this.currentIcons = newIcons.slice();
  this.searchIcon = '';
  this.hasChange = true;
  this.onCancel();
},

在改变icon列表之后,仅仅是更新到了webview的内存中,需求同步到本地文档中,这儿新增是否同步的按钮。在index.html中新增如下代码:

<div v-if="hasChange">
  当时icons有改变,是否同步到文档。
  <button class="--ch-primary-btn  --ch-btn" v-on:click="onAsyncFile()">同步</button>
  <button class="--ch-primary-btn  --ch-btn" v-on:click="onComeBack()">还原</button>
</div>

触及到了同步和还原两个操作

/* 同步当时的icons到文档中,适用于icons序列有改变,包括删去,去色等icon改变操作 */
onAsyncFile: function () {
  const that = this;
  // 针对重命名类,需求置新id值
  this.updateIconsToFile(this.icons, function () {
    that.hasChange = false;
    // 刷新icon列表
    that.refreshIconList();
    createMsg("同步成功!", 'success')
  })
},
/* 回滚icons改变,适用于icons的删去、去色等改变操作的回滚 */
onComeBack: function () {
  this.icons = this.copyIcons.slice();
  this.currentIcons = this.copyIcons.slice();
  this.searchIcon = '';
  this.addIconsToSvgContainer(this.copyIcons.slice(), true)
  this.hasChange = false;
}
/* 更新icons到本地文件 */
updateIconsToFile: function (newIcons, cb) {
            const that = this;
            callVscode({ cmd: 'vscode:updateIconList', newIcons: newIcons }, function (data) {
                if (data && data.success) {
                    that.icons = newIcons;
                    that.currentIcons = newIcons.slice();
                    that.copyIcons = newIcons.slice();
                    that.searchIcon = '';
                    createMsg("新增成功!", 'success')
                    if (cb) {
                        cb();
                    }
                }
            });
},

特别是同步这儿,调用了后台的updateIconList函数,该函数先读取本地文件,然后经过正则匹配出svg字符串,将最新的icons替换原先的内容,即可完结同步。updateIconList代码如下:

// 更新icon列表
  async updateIconList(global: any, message: any) {
    const currentFile = global.currentFile;
    if (fs.existsSync(currentFile)) {
      let content = await getLocalFileFs(currentFile)
      const res = content.match(new RegExp('<svg>.*</svg>'));
      if (res) {
        const newSvgContent = `<svg>${message.newIcons.map((v: any) => {
          return v.symbolStr;
        }).join('')}</svg>`;
        // 改变js文本内容并写入
        content = content.replace(res[0], newSvgContent)
        fs.writeFile(currentFile, content, function (data) {
          invokeCallback(global.panel, message, { success: true });
        });
      }
    }
  },

以上内容则是新增icons到列表中的同步操作。

icon的删去和去色

这儿实现一下icon的删去和去色

首先改造一下原先的icon-item,新增了一个鼠标移动上去就显示对该icon的操作弹窗,这儿提供了删去和去色两种操作。

Vue.component('icon-item', {
    props: {
        id: {
            type: String,
            default: '',
        },
        name: {
            type: String,
            default: '',
        },
        item: {
            type: Object,
            default: {},
        },
        actions: {
            type: Array,
            default: ['delete','removeColor'],
        },
        actionsTrigger: {
            type: Function,
            default: null
        }
    },
    data() {
        return {
            actionsList: [
                {
                    label: '删去',
                    key: 'delete',
                },
                {
                    label: '去色',
                    key: 'removeColor'
                }
            ],
            actionsShow: false,
        }
    },
    computed: {
        useId: function () {
            return `#${this.id}`
        },
        actionsWrapper: function () {
            return this.actions.map((v) => this.actionsList.find((c) => c.key === v));
        }
    },
    methods: {
        actionsHandle: function ({ key, id }) {
            this.actionsTrigger(key, id)
        },
        mouseover: function () {
             this.actionsShow = true;
        },
        mouseleave: function () {
            this.actionsShow = false;
        },
    },
    template: `
        <div :class="{'--ch-icon-item':true,'--ch-icon-item-new-c':item.isNew}" v-on:mouseover="mouseover()" v-on:mouseleave="mouseleave()">
            <div v-if="item.isNew" class="--ch-icon-item-new-tag">new</div>
            <div class="--ch-icon-item-icon">
                <svg>
                    <use v-bind:xlink:href="https://juejin.im/post/7203165621244297277/useId"></use>
                </svg>   
            </div>
            <div v-if="item.oldName">原id:{{item.oldName}}</div>
            <div  v-if="actionsShow" class="--ch-icon-action-popup">
                <div
                    v-for="item in actionsWrapper"
                    :key="item.key"
                    v-on:click="actionsHandle({key:item.key,id})"
                    class="--ch-icon-action-popup-item"
                >
                    <div>{{item.label}}</div>
                </div>
            </div>
        </div>
    `
})

actionsWrapper函数来自index.js的主函数中,代码如下

/** 展现icons的行为触发器 */
        showActionsTrigger: function (key, id, info) {
            switch (key) {
                case 'delete':
                    this.hasChange = true;
                    // 更新 icons序列
                    this.icons = this.icons.filter((v) => v.id !== id)
                    this.currentIcons = this.currentIcons.filter((v) => v.id !== id)
                    break;
                case 'removeColor':
                    let rCIconIdx = this.icons.findIndex((v) => v.id === id);
                    if (rCIconIdx > -1) {
                        const rCIcon = this.icons[rCIconIdx];
                      	// 去除symbolStr中的fill的色彩值,改为currentColor
                        const newSymbolStr = rCIcon.symbolStr.replace(new RegExp(`fill=['"].+?['"]`, 'g'), `fill="currentColor"`)
                        const newIcon = {
                            ...rCIcon,
                            symbolStr: newSymbolStr
                        };
                      	// 更新到svgContainer和替换icon列表
                        this.updateIconAvgContainer(newIcon)
                        this.icons[rCIconIdx] = newIcon;
                        this.icons = [...this.icons]
                        rCIconIdx = this.currentIcons.findIndex((v) => v.id === id)
                        if (rCIconIdx > -1) {
                            this.currentIcons[rCIconIdx] = newIcon;
                            this.currentIcons = [...this.currentIcons]
                        }
                        this.hasChange = true;
                    }
                    break;
                default:
                    break;
            }
        },

删去只是从展现的icons中移除了icon,一起提示是否需求同步。而去色较为杂乱,依据前面的前置常识,需求修改原有的symbol中的fill特点值,还需求更新svgContainer中对应的icon以及更新展现icons的symbolStr。

作用如图

IconView——在项目中维护iconfont.js

自此新增、删去、去色等保护功用现已介绍完毕!能够据此新增优化更多的功用,比如icon重命名,仿制svg字符串等。

注:部分代码参考来自网络

参考资料:

blog.csdn.net/longtengg1/…

blog.haoji.me/vscode-plug…