背景:接受二手项目会开发周期较长得项目,会忘掉自己引证了哪些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>
作用如图
这儿提供了三种兼并的战略:
(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。
作用如图
自此新增、删去、去色等保护功用现已介绍完毕!能够据此新增优化更多的功用,比如icon重命名,仿制svg字符串等。
注:部分代码参考来自网络
参考资料:
blog.csdn.net/longtengg1/…
blog.haoji.me/vscode-plug…