解锁新技能~开发vscode插件(上)(除夕也要卷)

需求

实现一个工具函数库,可以通过 npm 包和 vscode 插件两种方式使用

工具函数库目录结构:

    star-tools
    ├── validators
    │   └── index.ts 计划把验证函数都放到这一个文件中
    ├── 3d
    │   ├── control1.ts 因为 3d 类似 control 这种代码会比较长,是一个类,所以单独用文件
    │   ├── control2.ts
    │   └── index.ts 引入 control1、control2 等然后统一导出
    └── ...
        ├── ...
        └── index.ts

期望的 npm 包使用方式:

import {control1} 'star-tools/3d'

control1 拿到的就是 ts 源码

期望通过 vscode 插件使用的方式

  1. 调出 vscode 命令面板
  2. 每个函数对应一个命令,比如输入 star-tools:3d/control1,找到这个命令
  3. 选中命令后 enter
  4. control1.ts 中的代码插入到当前工作区的 focus 处

项目目录制定

两种方式在一个项目中,共用一份源码,主要是为了不维护双份的工具函数
如果不考虑两种方式共用一个项目,

对于 npm 包引用的方式,目录结构:

    star-tools
    ├── validators、3d、... 目录不变
    ├── .gitignore
    └── package.json

直接 publish 源码,install 的就是源码,可以 import {control1} ‘star-tools/3d’ 引用到

vscode 插件目录结构

    star-tools
    ├── src
    │   └── extension.ts 插件的入口文件,主要是插件的逻辑代码
    ├── tools
    │   └── 省略,就是把三个分类文件夹直接移动过来
    ├── .gitignore
    ├── package.json
    └── tsconfig.json

结论

显而易见了,就直接用 vscode 插件的目录结构,这样直接 publish 源码仍然可以引用到工具函数,并且两种方式共用同一份 tools

目前来看,这样的目录结构虽然 ok,但是会有不合理的地方,比如要把只和 vscode 相关的部分也发布到了 npm,这个会在稍后解决。

前置知识恶补

整个开发过程其实很不顺利,开发之前要先做一些调研,有一些前置知识会少走弯路。但很矛盾的是,一开始根本没有头绪,都不知道要先掌握哪些基本的前置知识。希望我总结的这些前置知识可以让你少走弯路咯 ~

进一步明确需求

在这个项目中:

  • 由于插件的逻辑是用 ts 编写的,所以这部分代码需要编译为 js,因为 vscode 插件的运行时需要时 commonjs 模块规则
  • 作为 vscode 插件时:代码片段作为源码直接插入,不需要把 ts 编译为 js。
  • 作为 npm 包引用时:~~ 同上 ~~~

一些有用的 API

  • 同步读取文件用 fs.readFileSync

各种有用的配置文件

  • package.json : 配置主入口、依赖、scripts 等、开发和发布后都要用到

  • tsconfig.json : 负责把 ts 编译 为 js。vscode 插件运行时只能是 js,如果插件源码是 ts,tsconfig 是必要的(或者用一些其他的打包工具内部集成了 ts 编译为 js 的能力)

  • .vscode 目录下的配置文件是用来配置 vscode 调试功能的,插件发布后无关。

  • .npmignore 用来过滤发布到 npm 的文件。

  • .vscodeignore 用来过滤发布到 marketplace 的文件。

  • package.json 中的 scripts 脚本,& 是并行,&& 是串联进行

F5 调试插件时 干嘛了

调试的入口文件为 .vscode/launch.json,一般情况下,vscode 左侧 debugger 工具面板都会自动定位到 .vscode/launch.json 中的 第一个命令\

解锁新技能~开发vscode插件(上)(除夕也要卷)
按下 f5 时,先经历了这样的过程,然后再把调试窗口启动\

解锁新技能~开发vscode插件(上)(除夕也要卷)
原来 f5 并不神秘,就是 npm run dev 哇,实际执行的脚本在 star-tools 中是

yarn run remove-out && tsc && yarn run move-src && node ./build/genVscdPkg.js

只看 tsc,它是负责把 ts 编译为 js 的,之后插件才能在 vscode 的环境中运行起来。编译之前会先读取 tsconfig.json 中的配置,然后开始编译并输出编译后的代码。

remove-out、move-src、node ./build/genVscdPkg.js 先不管,这些都属于对目录和文件内容的定制化处理。会在后面的优化中提到

解锁新技能~开发vscode插件(上)(除夕也要卷)

  • 一般情况下 rootDir 都等同于当前目录所以是 ‘./’
  • 对于 star-tools 来说工具函数是不需要编译的,因为 star-tools 不参与运行,充当代码片段的作用。和插件相关的逻辑代码才需要编译,逻辑代码都在 src 下。所以配置了 includes
  • 编译后输出到 out 目录下,out 目录下的文件与 src 下的对应
  • 配置 mapSource 为 true 方便调试,所以可以看到 out 目录下都有对应 .map.js

================== ok!上面编译阶段就完成了, ====================
这时 vscode 就会把编译窗口启动,开始运行代码,再次看 package.json。其中 main 选项配置的文件就是运行时的入口文件

"main": "./out/main.js",

运行时就是插件的逻辑了,取决于插件的功能啦,star-tools 最开始的逻辑就是把每个工具函数都注册为命令,然后巴拉巴拉…(后面讲具体功能再说)

vsce publish 干嘛了 ?

运行 vsce publish 命令后,经历了这样的过程:\

解锁新技能~开发vscode插件(上)(除夕也要卷)

  • 使用 yarn run esbuild-base 来编译,是因为 esbuild 在 tsc 的基础上有一些扩展功能比如压缩等,编译之前也读取了 tsconfig 的配置。我这里其实只是强迫症想合并到一个文件。
  • 编译打包之后输出到 out 文件夹

============= 发布准备结束!下面就到发布阶段了 ==============
(其实上面的流程也是编译阶段,和开发调试时不一样在于配置不同输出不同)

  • 读取 .vscodeignore 对文件进行过滤
  • 发布到 marketplace

安装插件 又干嘛了 ?

点击安装 => 下载发布到 marketplace 的插件源码 => 根据 package.json 中的 dependencies(‼️ 注意 ,要考的)安装依赖\

解锁新技能~开发vscode插件(上)(除夕也要卷)
什么时候运行呢?由 package.json 中的配置项 activationEvents 决定

  • 不配置或者为 [] :当在命令面板中选择插件相关的命令时开始进入运行时
  • 配置为 * 号 : vscode 启动就进入运行时

======================= 安装阶段 🔚 =========================

运行时入口文件是 package.json 中的 main,接下来同开发调试时啦

npm publish 比 vsce publish 简单多了

解锁新技能~开发vscode插件(上)(除夕也要卷)

  • 不需要 tsc 编译了,因为就是发布源码,直接引用。(至于 node ./build/genNpmPkg 先不管,也是属于对目录和文件内容的定制化处理)

npm install

过滤后安装的包结构:

    - star-tools
      - tools
        - ... 源码结构
      - package.json
      - readme.md

插件开发插件第一阶段 基本功能

功能点

解锁新技能~开发vscode插件(上)(除夕也要卷)
从代码上来讲,图中三部分对应以下三段代码:

配置命令

  • package.json
// ...
"contributes": {
        "commands": [
            {
                "command": "star-tools.3d.DeviceOrientationControls",
                "title": "star-tools: 3d相关/陀螺仪控制器"
            }
            //...
        ]
},
// ...

注册命令

  • src/extension.ts
const registerCommand =  (subName: string, methodFileName: string, method: fs.PathLike) => {
  const methodName = methodFileName.split(".")[0];
  const commandName = `star-tools.${subName}.${methodName}`;
  const content =    (method as string); //processSourceFile 把文件作为字符串读取,逻辑省略
  return vscode.commands.registerCommand(commandName, () => whenCommand(methodName, content));
};
export async function activate(context: vscode.ExtensionContext) {
  // ... 读取目录逻辑省略
  // subName 为分类名称如 “3d”
  // methodFileName 为函数名称如 “DeviceOrientationControls.ts”
  // method 为文件对应路径
  context.subscriptions.push( registerCommand(subName, methodFileName, method))
}

插入代码片段

  • src/extension.js
const whenCommand = (methodName: string, content: string) => {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    vscode.window.showWarningMessage("star-tools: 工作区打开文件后才能使用该功能");
    return;
  }
  const { selections } = editor;
  if (selections.length === 0) {
    vscode.window.showWarningMessage("star-tools: 请先选择一个区域");
    return;
  }
  const firstSelection = selections[0];
  const { start, end } = firstSelection;
  const range = new vscode.Range(start, end); //计算选区范围

  editor.edit((editBuilder) => {
    editBuilder.replace(range, content); //替换选区
  });

  vscode.window.showInformationMessage(`✅ 已插入函数: ${methodName}`);
};

插件开发插件第二阶段 功能优化

做了一些规范化和自动化的事

每个文件对应一个代码片段

  • 符合封闭开放原则,对修改封闭,对增加开放,易于维护
  • 利于 AST 解析(马上用到)
import aaa from 'aaa'  //也可以引用多个,或者没有依赖
// 主体代码部分 start
type TXxx {

}
function bbb (){

}
function xxx (){
   bbb()
}
// 主体代码部分 end
export default xxx //每个工具函数都有一个独立的文件,导出都用 export default

代码片段分割

vscode 插件的形式使用时,插入时,应该剔除 export 语句,并且将 import 插入顶部

  1. 改写 processSourceFile,加入 AST 解析逻辑
export function processSourceFile(filePath: string) {
  const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(), ts.ScriptTarget.Latest, true);

  let imports: ts.ImportDeclaration[] = [];
  let exportDefault: ts.ExportAssignment | any = null;
  let otherStatements: ts.Statement[] = [];

  function findImportsAndExports(node: ts.Node) {
    if (ts.isImportDeclaration(node)) {
      imports.push(node);
    } else if (ts.isExportAssignment(node)) {
      exportDefault = node;
    } else {
      ts.isStatement(node) && otherStatements.push(node);
    }

    ts.forEachChild(node, findImportsAndExports);
  }

  ts.forEachChild(sourceFile, findImportsAndExports);

  // 将import语句转为字符串
  let importStr = imports.map((imp) => sourceFile.text.substring(imp.getStart(), imp.getEnd())).join("\n");

  // 将除了export和import之外的语句转为字符串
  let bodyStart = imports.length ? imports[imports.length - 1].getEnd() : 0;
  let bodyEnd = exportDefault ? exportDefault.getStart() : sourceFile.getEnd();
  let bodyStr = sourceFile.text.substring(bodyStart, bodyEnd);

  // 将export语句转为字符串
  let exportStr = exportDefault ? sourceFile.text.substring(exportDefault.getStart(), exportDefault.getEnd()) : "";

  return { importStr, bodyStr, exportStr };
}
  1. 改写 whenCommand
const whenCommand = (methodName: string, content: FileContent) => {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    vscode.window.showWarningMessage("star-tools: 工作区打开文件后才能使用该功能");
    return;
  }
  const { selections } = editor;
  if (selections.length === 0) {
    vscode.window.showWarningMessage("star-tools: 请先选择一个区域");
    return;
  }
  const firstSelection = selections[0];
  const { start, end } = firstSelection;
  const range = new vscode.Range(start, end);

  editor.edit((editBuilder) => {
    editBuilder.insert(new vscode.Position(0, 0), content.importStr + "\n"); //引用
    editBuilder.replace(range, content.bodyStr); //主体
  });

  vscode.window.showInformationMessage(`✅ 已插入函数: ${methodName}`);
};

原文链接:https://juejin.cn/post/7332665033800105984 作者:鱼小柔

(0)
上一篇 2024年2月10日 上午10:11
下一篇 2024年2月10日 上午10:21

相关推荐

发表回复

登录后才能评论