IconView——在项目中可视化iconfont.js

2,508 阅读3分钟

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

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

对以上问题思考后,决定处理以上脚本比较好的方式便是开发一个解析iconfont.js的vscode插件,便可以直接在项目中使用。

效果:

image.png

初始化一个项目

初始化一个名为IconView的插件

// 安装需要的包
npm install  yo generator-code -D

// 运行,然后按流程走一遍,可以生成初始模板的插件
npx yo code

开发过程中,按F5进行调试。

可视化过程

新建一个webview

vscode插件机制提供了一个可打开一个自定义页面的功能,即webview,这里采用在webview中展示解析出来的icon。

extension.ts(vscode 插件的入口文件)文件中

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "IconView" is now active!');

  // 追踪当前 webview 面板
  let currentPanel: vscode.WebviewPanel | undefined | any = undefined;

  // 创建打开IconView的命令
  const openIconViewCommand = vscode.commands.registerCommand(
    "IconView.openIconView",
    async (uri: vscode.Uri) => {
      // 获取当前活动的编辑器
      const columnToShowIn = vscode.window.activeTextEditor
        ? vscode.window.activeTextEditor.viewColumn
        : undefined;
      if (currentPanel) {
        // 如果我们已经有了一个面板,那就把它显示到目标列布局中
        if (columnToShowIn)
          currentPanel.reveal(columnToShowIn === 2 ? 1 : 2);
      } else {
        // 不然,创建一个新面板
        vscode.window.showInformationMessage("IconView.openIconView");
        currentPanel = await openIconView(context, uri, columnToShowIn === 2 ? 1 : 2);
        // 当前面板被关闭后重置
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          null,
          context.subscriptions
        );
      }
    }
  );
  context.subscriptions.push(openIconViewCommand);
}

这里注册了一个IconView.openIconView的命令,命令主要是获取webview显示的面板位置。将命令丢进监听池。具体新建和打开webview的操作在openIconView中。

为了方便使用,需要将openIconView注册到打开文件的右键菜单,需要在package.json中配置相关参数,注册命令和配置菜单项,即可在打开的iconfont.js文件右键打开webview

// 插件激活事件
"activationEvents": [
    "onCommand:IconView.openIconView",
],
// 功能配置点 
"contributes": {
    "commands": [
      {
        "command": "IconView.openIconView",
        "title": "IconView Open IconView"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "IconView.openIconView",
          "group": "navigation",
          "when": "editorFocus"
        }
      ]
    },
    "configuration": {
      "title": "IconView"
    }
  },

openIconView的代码

export async function openIconView(context: vscode.ExtensionContext, uri: vscode.Uri, viewColumn: vscode.ViewColumn) {
  // 当前文件绝对路径
  const currentFile = uri.fsPath;
  if (projectPath) {
    // 创建webview
    const panel: vscode.WebviewPanel = vscode.window.createWebviewPanel(
      'IconView', // viewType
      "IconView", // 视图标题
      viewColumn, // 显示在编辑器的哪个部位
      {
        enableScripts: true, // 启用JS,默认禁用
        retainContextWhenHidden: true, // webview被隐藏时保持状态,避免被重置
      }
    );
    panel.webview.html = await getWebViewContent(
      context,
      uri,
      './iconView/index.html',
      {
        beforeBodyScripts: [currentFile]
      }
    );
    // 监听消息
    let global = { currentFile, panel, uri };
    panel.webview.onDidReceiveMessage(message => {
      const [_, cmdName] = message.cmd.split(':')
      if (messageHandler[cmdName]) {
        // cmd表示要执行的方法名称
        messageHandler[cmdName](global, message);
      } else {
        vscode.window.showErrorMessage(`未找到名为 ${cmdName} 的方法!`);
      }
    }, undefined, context.subscriptions);
    return panel;
  }
}

/**
* 存放所有消息回调函数,根据 message.cmd 来决定调用哪个方法,
*/
var messageHandler: any = {
  // 弹出提示
  alert(global: any, message: any) {
    vscode.window.showInformationMessage(message.info);
  },
  // 显示错误提示
  error(global: any, message: any) {
    vscode.window.showErrorMessage(message.info);
  },
  /** 将内容写入剪贴板 */
  copy(global: any, message: any) {
    vscode.env.clipboard.writeText(message.content);
    invokeCallback(global.panel, message, { success: true, msg: '内容已复制,可直接使用!' });
  },
  // 获取urls信息
  async getUrlsInfo(global: any, message: any) {
    const data = {
      targetPath: global.uri.fsPath,
    }
    invokeCallback(global.panel, message, data);
  },
}

function invokeCallback(panel: any, message: any, resp: any) {
  // 错误码在400-600之间的,默认弹出错误提示
  if (typeof resp == 'object' && resp.code && resp.code >= 400 && resp.code < 600) {
    vscode.window.showErrorMessage(resp.message || '发生未知错误!');
  }
  panel.webview.postMessage({ cmd: 'vscodeCallback', cbid: message.cbid, data: resp });
}

openIconView中使用vscode.window.createWebviewPanel新建了一个webviewPanel,模板路径是./iconView/index.html,panel需要加载html字符串内容,然后panel开启消息监听并执行相应事件。

messageHandler是信息通道的处理器,主要处理来自webView传递的事件,这里定义了几个基本事件处理函数。

invokeCallback是处理消息监听的回调函数,在具体的操作事件中调用。

webview主要通过消息通道的机制来执行vscode的插件能力,包括文件的读取修改,复制等操作。

getWebViewContent代码

export async function getWebViewContent(context: any, uri: vscode.Uri, templatePath: any, config: any = {}) {
    const projectPath = await getProjectPath(uri);
    const resourcePath = path.join(context.extensionPath, templatePath);
    const dirPath = path.dirname(resourcePath);
    let html = fs.readFileSync(resourcePath, 'utf-8');
    // 添加前置script 用户从项目本地插入脚本
    let beforeBodyScriptStr = '';
    if (config?.beforeBodyScripts && Array.isArray(config?.beforeBodyScripts)) {
        console.log('beforeBodyScripts',config?.beforeBodyScripts)
        beforeBodyScriptStr = config?.beforeBodyScripts.map((src: string) => {
            return `<script src="${src}"></script>`
        }).join('\n');
    }
    html = html.replace('{@beforeBodyScript}',beforeBodyScriptStr)

    // vscode不支持直接加载本地资源,需要替换成其专有路径格式,这里只是简单的将样式和JS的路径替换
    html = html.replace(/(<link.+?href=["']|<script.+?src=["']|<img.+?src=["'])([^@].+?)["']/g, (m, $1, $2) => {
        return $1 + vscode.Uri.file(path.resolve(dirPath, $2)).with({ scheme: 'vscode-resource' }).toString() + $1[$1.length - 1];
    });

    html = html.replace(/(<link.+?href=["']@|<script.+?src=["']@|<img.+?src=["']@)(.+?)["']/g, (m, $1, $2) => {
        return $1.substring(0, $1.length - 1) + vscode.Uri.file(path.resolve(projectPath, $2)).with({ scheme: 'vscode-resource' }).toString() + $1[$1.length - 2];
    });
    return html;
}

getWebViewContent除了读取定义的html模板之外,还将html中的引用路径改为vscode插件的资源路径,不然不能访问。

添加前置script 用户从项目本地插入脚本部分用于注入iconfont的js文件路径,这里通过html引入script的方式,可以统一来自iconfont,iconPark的iconjs文件。然后webview中可通过use来展示。

这里采用html路径的方式来开发,而不是直接采用字符串模板,是html有格式化,结构清晰。注意的是iconView一定是要第一级,因为打包后的代码中是没有src的,所以放到src中无法识别。

展示icon列表

在html引入了vue@2.x,便于html开发。下载vue.js的离线版本,这里放在了在index.html中引入vue.js,以及相关初始化脚本index.js

var app = new Vue({
    el: '#root',
    data: {
        searchIcon: '',
        icons: [],
        showIcons: [],
    },
    mounted: function () {
        // 调用vscode,通过消息机制
        callVscode({ cmd: 'vscode:getUrlsInfo' }, (data) => {
            if (data) {
                this.targetPath = data.targetPath;
            }
        });
        let icons = [];
        if (document) {
            // 等待页面渲染完成,延迟获取symbol标签
            setTimeout(() => {
                const iconElements = document.querySelectorAll('symbol');
                Array.from(iconElements).forEach((ele) => {
                    icons.push({
                        id: `#${ele.id}`,
                        name: ele.id
                    })
                })
                this.icons = icons;
                this.showIcons = icons.slice();
            }, 1000)
        }
    },
    methods: {
        /** icon搜索 */
        onSearch: function (e) {
            const value = e.target.value;
            if (value) {
                this.showIcons = this.icons.filter((v) => {
                    return v.name.indexOf(value) > -1;
                })
            } else {
                this.showIcons = this.icons.slice();
            }
        },
        onClickIcon: function ({ item }) {
            const temp = `<IconFont type="${item.id.substring(1)}"/>`;
            callVscode({ cmd: 'vscode:copy', content: temp }, (data) => {
                if (data.success) {
                    createMsg(data.msg, 'success')
                }
            });
        },
    },
})

新建一个vue实例,挂载在html中,然后在mounted钩子中延迟获取到注入html中的symbol标签,一般来说一个symbol代表一个icon,这里拿到之后,提取symbol的id,以便后面的使用,这里区分两个icon列表,用于搜索展示的全量副本和展示副本。

同时新增onSearch,onClickIcon方法,用于搜索和点击复制模板。

icon是个列表,为了方便操作,新建一个vue的component

Vue.component('icon-item', {
    props: {
        id: {
            type: String,
            default: '',
        },
        name: {
            type: String,
            default: '',
        }
    },
    data() {
        return { preview: false };
    },
    methods: {},
    template: `
        <div class="--ch-icon-item">
            <div class="--ch-icon-item-icon">
                <svg>
                    <use v-bind:xlink:href="id"></use>
                </svg>   
            </div>
            <span class="--ch-icon-name" :title="name">{{name}}</span>
        </div>
    `
})

icon用于展示icon列表

接下来看看html内容

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
      />
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <script src="./other/vue.js"></script>
  {@beforeBodyScript}
  <body>
    <div id="root">
      <div class="--ch-target-path">
        <span>文件路径:</span>
        <span>{{targetPath}}</span>
      </div>
      <input v-model="searchIcon" class="--ch-search" v-on:input="onSearch($event)" placeholder="请输入icon名字"></input>
      <div class="--ch-icon-list">
        <div
          v-for="item in showIcons"
          :key="item.id"
          class="--ch-icon-item-wrapper"
          v-on:click="onClickIcon({item})"
          >
          <icon-item
            v-bind:id="item.id"
            v-bind:name="item.name"
            ></icon-item>
        </div>
      </div>
    </div>
    <script src="./communication.js"></script>
    <script src="./utils.js"></script>
    <!-- 组件部分 -->
    <script src="./components/iconItem.js"></script>
    <script src="./index.js"></script>
  </body>
</html>

html中简单定义了搜索,文件路径,icon列表展示。

展示出来之后,接下来要实现的是点击icon可复制模板代码到剪贴板中。iconItem的容器组件绑定了点击事件,点击事件中可获取到item内容,这里定义了<IconFont type="${item.id.substring(1)}"/>(结合antd的IconFont组件使用),然后调用了vscode的复制功能。

callVsCode的代码

var callbacks = {}; // 存放所有的回调函数
var vscode = window.acquireVsCodeApi ? window.acquireVsCodeApi() : {
  postMessage(data) {
    console.log(data)
  }
};

function callVscode(data, cb) {
  if (typeof data === 'string') {
    data = { cmd: data };
  }
  if (cb) {
    // 时间戳加上5位随机数
    const cbid = Date.now() + '' + Math.round(Math.random() * 100000);
    // 将回调函数分配一个随机cbid然后存起来,后续需要执行的时候再捞起来
    callbacks[cbid] = cb;
    data.cbid = cbid;
  }
  vscode.postMessage(data);
}
window.addEventListener('message', event => {
  const message = event.data;
  switch (message.cmd) {
      // 来自vscode的回调
    case 'vscodeCallback':
      console.log(message.data);
      (callbacks[message.cbid] || function () { })(message.data);
      delete callbacks[message.cbid]; // 执行完回调删除
      break;
    default: break;
  }
});

打包使用

由于没有发布到插件市场,这次使用打包成vsix插件,通过本地安装。

// 安装打包工具vsce
npm i vsce -g

// 打包成vsix文件
vsce package

下一篇:IconView——在项目中维护confont.js

注: 以上部分代码来自于网络。

参考资料:

blog.haoji.me/vscode-plug…