阅读 1063

从零开发Vscode上传图片插件

一、内容简介

在工作中经常用有一些图片来美化我们的前端页面,这时需要我们先将图片下载到本地再通过tinypng这个网站将图片压缩后上传到七牛云,最后将图片链接放在我们的代码中,虽然工作也都在搬砖,但是这种砖不是我喜欢的那种砖。于是,就想到开发一个Vscode插件来帮我自动压缩上传图片。
效果预览:

二、需求分析

  • 1.可在Vscode的setting中配置上传所需的参数,可以根据个人的需求单独进行配置;
  • 2.在开发过程中可在编辑器中直接选择图片并上传到七牛并将图片链接填写到光标位置;
  • 3.鼠标悬浮在图片链接时可以预览图片

三、从零开发

一个好的文档可以帮助我们更容易的开发:如果英文比较好的同学可以直接看Vscode英文文档,这里api会比较全,可以找到更简洁的方案实现功能; 不过我的话,还是花很久时间找了这篇比较全的中文文档

1. 搭建项目

vscode插件的开发需要现在全局安装脚手架:npm install -g yo generator-code
安装成功后,直接对应命令yo code 来生成一个Vscode插件工程 输入对应的配置后,就可以生成一个项目了

2. 启动项目调试

安装好依赖进入项目后,按f5会打开一个新的Vscode编辑器,在命令行面板(Ctrl+Shift+P / Shift+command+P)输入Hello World,如果编辑器右下角弹出弹窗 则说明工程可以正常运行,可以开始我们的需求开发啦;

3. 配置所需参数

在package.json中,可以看到有contributes参数,在这里添加一个configuration来设置插件参数, 如图: 完整configuration配置如下:

"configuration": [{
    "title": "图片上传工具配置",
    "properties": {
        "upload_image.domain": {
            "type": "string",
            "default": "",
            "description": "设置上传域名"
        },
        "upload_image.accessKey": {
            "type": "string",
            "default": "",
            "description": "设置七牛上传accessKey"
        },
        "upload_image.secretKey": {
            "type": "string",
            "default": "",
            "description": "设置七牛上传secretKey"
        },
        "upload_image.scope": {
            "type": "string",
            "default": "",
            "description": "设置七牛上传上传空间"
        },
        "upload_image.gzip": {
            "type": "boolean",
            "default": "true",
            "description": "是否启用图片压缩"
        }
    }
}]
复制代码

点击重新运行后在调试的Vscode编辑器中找到settings,找到Extendsions 可以看到我们配置的配置项已经可以在编辑器中自己设置了:

3. 配置右键菜单

接下来可以实现我们的一个选择图片的交互功能:鼠标点击后右键可以弹出一个菜单选择图片后,拿到图片和我们之前配置的一些参数;

1. 在package.json定义命令和事件:

"activationEvents": [
    "*"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
        {
            "command": "extension.choosedImage",
            "title": "选择图片"
        }
    ],
    "menus": {
        "editor/context": [{
            "when": "editorFocus",
            "command": "extension.choosedImage",
            "group": "navigation"
        }]
    },
复制代码

我的理解是:activationEvents为需要注册的事件;commands定义事件名和标题;menus定义事件执行的时机和命令显示的位置。

2. 在extension.js中编辑事件:

import * as vscode from 'vscode'

export function activate(context: vscode.ExtensionContext) {
  let texteditor = vscode.commands.registerTextEditorCommand(
    'extension.choosedImage',
    async (textEditor, edit, args) => {
    	// 获取七牛参数
        const qiniuConfig = vscode.workspace.getConfiguration('upload_qiniu_config')
        const uri = await vscode.window.showOpenDialog({
            canSelectFolders: false,
            canSelectMany: false,
            filters: {
              images: ['png', 'jpg'],
            },
      	})
      }
      console.log(qiniuConfig, uri)
  )
  
  context.subscriptions.push(texteditor)
}
复制代码

通过getConfiguration获取在编辑器中配置的内容;showOpenDialog来调出文件选择框: 这里有一个需要注意的点就是在使用getConfiguration获取配置参数时,传的名字应该与package.json中的configuration中设置的key值相同。

3. 完成图片上传七牛云功能:

这里使用nodeqiniu.jsnpm install qiniu安装依赖后,实现一个简单的上传功能就好:
新建一个util文件夹,单独写一下上传逻辑

util/upload.ts

const qiniu = require('qiniu')

const getToken = (accessKey: string, secretKey: string, scope: string) => {
  const options = {
    scope,
  }
  const mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
  const putPolicy = new qiniu.rs.PutPolicy(options)
  const uploadToken = putPolicy.uploadToken(mac)
  return uploadToken
}

// 七牛上传配置
export interface QiNiuUpConfig {
  domain: string // 上传后域名
  accessKey: string // 七牛参数
  secretKey: string // 七牛参数
  scope: string // 七牛上传空间
  gzip: boolean // 是否需要压缩
}

export const upImageToQiniu = (
  localFile: string,
  cb: { (res: any): void; (arg0: any): void },
  upConfig: QiNiuUpConfig
) => {
  const config = new qiniu.conf.Config()
  // config.zone = qiniu.zone.Zone_z2
  const formUploader = new qiniu.form_up.FormUploader(config)
  const putExtra = new qiniu.form_up.PutExtra()
  const token = getToken(upConfig.accessKey, upConfig.secretKey, upConfig.scope)
  // 获取当前时间戳
  var key = new Date().getTime()
  formUploader.putFile(token, key, localFile, putExtra, function (
    respErr: any,
    respBody: any,
    respInfo: any
  ) {
    const url = upConfig.domain + '/' + respInfo.data.key
    cb(url)
  })
}
复制代码

4. 将图片链接写人编辑器中:

通过上面的方法已经可以获得图片上传后的链接,接下来就是将链接写入编辑器中:
首先判断编辑器选择位置,editor.selection中可以获得光标位置、光标选择首尾位置。若光标有选中内容则editBuilder.replace替换选中内容,否则editBuilder.insert在光标位置插入图片链接:

// 将图片链接写入编辑器
function addImageUrlToEditor(url: string) {
  let editor = vscode.window.activeTextEditor
  if (!editor) {
    return
  }
  const { start, end, active } = editor.selection
  if (start.line === end.line && start.character === end.character) {
    // 在光标位置插入内容
    const activePosition = active
    editor.edit((editBuilder) => {
      editBuilder.insert(activePosition, url)
    })
  } else {
    // 替换内容
    const selection = editor.selection
    editor.edit((editBuilder) => {
      editBuilder.replace(selection, url)
    })
  }
}
复制代码

这时一个完整的上传图片到七牛并将返回链接写入编辑器功能完全实现:

let texteditor = vscode.commands.registerTextEditorCommand(
  'extension.choosedImage',
  async (textEditor, edit, args) => {
      const qiniuConfig = vscode.workspace.getConfiguration('upload_qiniu_config')
      const uri = await vscode.window.showOpenDialog({
        canSelectFolders: false,
        canSelectMany: false,
        filters: {
          images: ['png', 'jpg'],
        },
      })
      if (!uri) {
          return
      }
      
      const upConfig = {
          accessKey: qiniuConfig.accessKey,
          secretKey: qiniuConfig.secretKey,
          domain: qiniuConfig.domain,
          gzip: qiniuConfig.gzip,
          scope: qiniuConfig.scope,
      }
      // 图片本地路径
      const localFile = uri[0].fsPath
      upImageToQiniu(
        localFile,
        (res: string) => {
          let url = res
          // 将图片链接写入编辑器
          addImageUrlToEditor(url)
        },
        upConfig
      )
  }
)
复制代码

5. 实现鼠标悬浮预览图片:

使用vscode.languages.registerHoverProvider注册一个鼠标悬浮事件,registerHoverProvider内通过provideHover函数返回 new Hover() 来显示内容。

预览图片功能开发思路:

  • 1.Hover对象可支持markdown语法,可使用![](图片链接)来显示一张图片
  • 2.在Vscode插件中不可使用http图片,只能显示https图片,所以可以通过node将图片链接转为base64链接后,使用![](base64链接)来显示
  • 3.在转换base64图片时,需先判断是否为http、https,根据不同类型使用node对应的http、https模块来进行转换
  • 4.通过http(https).get请求图片后,可根据content-type来判断当前链接是否为图片
  • 5.当图片过大时,生成的base64链接可能很长导致图片显示失败,所以在转base64时,可以将图片添加七牛裁剪参数后进行转换

平时在使用Vscode开发时,鼠标hover在链接会有单独的提示可以打开链接,但是在文档中并没有找到对应的api,所以我只能获取到对应行的文本和鼠标悬浮位置,通过正则来匹配当前hover位置是否有链接,所以我们先在实现了一个获取当前文本中所有链接及对应位置的方法:

util/handleHover.ts

// 获取http链接 在字符串中的位置
const getHttpLinkPosition = (content: string): Array<any> => {
  const regx = /['"][http(s)://](\S*)['"]/g
  const arr = []
  let str = content
  let textStart = 0
  while (str) {
    let httpIndex = str.indexOf('http')
    if (httpIndex === 1) {
      // 'http || "http
      let resREGX = str.match(regx)
      if (resREGX) {
        arr.push({
          start: textStart,
          end: textStart + resREGX[0].length,
          value: resREGX[0],
          length: resREGX[0].length,
        })
        str = str.substring(resREGX[0].length)
        textStart += resREGX[0].length
      }
    } else if (httpIndex > -1) {
      str = str.substring(httpIndex - 1)
      textStart += httpIndex - 1
    } else {
      str = str.substring(str.length)
    }
  }
  return arr
}
复制代码

这个正则可能并不是最好的方案,只是匹配了'|" + http(s)开头到'|"结尾的位置为链接地址,同时将链接所在文本的位置及链接长度(start、end、length)一起返回。
有了这个获取链接位置的方法后,再加一个获取hover位置对应链接的方法:

util/handleHover.ts

// 将链接左右两边的引号删掉
const filterHttpLink = (link: string): string => {
  if (link) {
    link = link.substr(0, link.length - 1)
    link = link.substr(1)
  }
  return link
}

// 获取hover的 http链接
export const getHoverHttpLink = (content: string, position: number): string => {
  let link = ''
  const httpPositions = getHttpLinkPosition(content)
  if (httpPositions.length) {
    httpPositions.forEach((item) => {
      if (item.start <= position && item.end >= position) {
        link = item.value
      }
    })
  }
  // 如果有链接,去除链接两边的引号后返回
  return filterHttpLink(link)
}
复制代码

有了这些方法后,我们只要在extension.ts中注册一个registerHoverProvider事件,在使用![](图片链接)来显示图片,开发过程中在这里有几个坑需要注意:
1.只能显示https的链接,所以我们需要添加一个方法,将网络链接转为base64链接
2.当图片过大时base64链接可能过长显示不出来,所以是通过七牛添加裁剪参数后,再转为base64:

util/handleHover.ts

import * as https from 'https'
import * as http from 'http'

// 图片添加裁剪参数
export const addImageCropParam = (
  url: string,
  width?: number,
  height?: number,
  type?: number
): string => {
  // 如果url中已经带有裁剪参数,先去掉之前的参数
  const [path] = url.split('?imageView2')
  url = path

  let cropUrl = type ? `?imageView2/${type}` : '?imageView2/2'
  if (!!width) {
    cropUrl += `/w/${width}`
  }
  if (!!height) {
    cropUrl += `/h/${height}`
  }
  if (!!width || !!height) {
    url += cropUrl
  }

  return url
}


// 将图片链接转为base64
export const translateImageUrlToBase64 = (url: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    let resUrl = ''
    // 链接是否为https
    const isHttps = url.includes('https')

    if (!url) {
      resolve(resUrl)
    } else {
      url = addImageCropParam(url, 100)
      ;(isHttps ? https : http).get(url, {}, function (res: http.IncomingMessage) {
        const contentType = res.headers['content-type']
        // 请求为图片
        if (contentType && contentType.includes('image')) {
          // url = addImageCropParam(url, 100)
          var chunks: Array<any> = [] //用于保存网络请求不断加载传输的缓冲数据
          var size = 0 //保存缓冲数据的总长度
          res.on('data', function (chunk: any) {
            chunks.push(chunk)
            //累加缓冲数据的长度
            size += chunk.length
          })
          res.on('end', function (err: any) {
            //Buffer.concat将chunks数组中的缓冲数据拼接起来,返回一个新的Buffer对象赋值给data
            var data = Buffer.concat(chunks, size)
            //将Buffer对象转换为字符串并以base64编码格式显示
            const base64Img = data.toString('base64')
            resolve(`data:image/png;base64,${base64Img}`)
          })
        } else {
          resolve(resUrl)
        }
      })
    }
  })
}
复制代码

现在我们需要做图片预览的辅助函数已经都有了,就是需要添加registerHoverProvider里的逻辑,这部分逻辑就非常简单了:

extension.ts

// 鼠标悬浮预览图片
vscode.languages.registerHoverProvider('*', {
  async provideHover(document, position) {
    const { character } = position
    // 当前行的文本内容
    const currentLineText = document.lineAt(position).text
    // 匹配当前行内链接
    const httpLink = getHoverHttpLink(currentLineText, character)
    var strToBase64 = await translateImageUrlToBase64(httpLink)
    const markString = strToBase64 ? new vscode.MarkdownString(`![](${strToBase64})`, true) : ''
    
    return {
      contents: [markString],
    }
  },
})
复制代码

写到这里,我们的完整功能已经实现,看下效果:

四、发布插件

功能开发完成后可以将我们的插件发布到Vscode上,别的小伙伴就可以安装你开发的插件愉快的开发啦~~。

1.创建账号

首先我们进入文档中提到的组织主页,完成验证登录后创建一个组织。进入主页后, 点击Personal access tokens安装文档中的创建一个新的token(此处为中文文档,英文的看不懂,有看得懂的小伙伴可以评论告诉我后再进行补充)。

2.创建发行放

这里文档中已经介绍的很清楚,如果命名行创建不成功也可以使用提示的地址进行注册。

3.插件发布

这里我并没有在终端登录成功,而是在本地vsce package打包后生成.vsix文件,在管理页中添加插件,然后将生成的.vsix文件上传就显示上传成功啦: 在Vscode中搜索插件upload-to-qiniu也可以搜到对应插件: 测试一下安装的新插件 开心😄已经上传成功啦~

五、结束

目前上传的插件已经可以使用但是还有一个图片压缩的功能没有实现,后面会都补上。我已经将插件源码上传至github,欢迎大家点个start^ ^。如果对插件还有更好的想法也可以互相交流。

感谢阅读🙏

文章分类
前端
文章标签