前言#
之前写过几次vscode插件,但是过段时间自己就忘了流程了。。。。所以写个文章方便自己review。
这次咱来写一个将$.ajax转换成axios的插件。
感兴趣的可以去看下,名字叫AJAX_JQ_2_AXIOS

github地址:
https://github.com/1714080902120/jq-ajax-2-axios文档#
官方有提供文档,虽然文档不是很详细
Your First Extension | Visual Studio Code Extension API
环境支持#
npm install -g yo generator-code这个yo[1](Yeoman)是一个多功能的npm包,可以看作是一个脚手架,里面不止有vscode插件的模板,还有其它比如eslint插件的模板等。
而generator-code[2]则是yo的vscode extension相关的模板
其实也不一定非得安装,你也可以手搓一个。。(我第一次写的时候就是参考别人的项目手搓的。。。)
安装完之后可以开个终端看下

创建项目#
在终端输入
yo code然后就会有一连串的选项,选项都是大家知道的,比如是否使用ts或者webpack打包等,这里就不多说了。

创建完之后可以看到文件目录

其中test是写单元测试的地方。
而extension.ts就是我们的入口,这篇文章是入门,所以不会有其它配置,这里就不多介绍其它文件了。
打开extension.ts文件你可以看到一个简单的结构

activate:这个函数会在某个时候激活,比如你运行某个指令的时候,这完全取决于你的配置。deactivate:这个自然就是取消激活的时候触发的。
我们去到package.json文件中稍微看下

这个activationEvents[3]有很多场景,比如onStartupFinished等,你可以自由组合
而这个contributes[4]则是用来修改vscode自身的,比如你这个插件想改变配色,这个时候就可以用contributes.colors[5] 。
可以看到初始的激活场景就是当你F1选择该指令的时候,而我们的指令出现的地方目前仅能通过F1唤起指令面板才可以找到入口。
而registerCommand[6]则是注册指令,可以传入一个回调。当这个指令被触发的时候就会执行这个回调,我们的主要代码就写在这里面。
运行#
在运行之前,确保你的项目是在根目录,即非workspace,整个vscode界面只有你这个项目。因为可能会导致识别路径错误。
确保完之后直接F5,是的,直接F5,如果有弹出什么ts:watch出错啥的,直接忽略选择debug anyway。
这个时候就会弹出一个新窗口,原来的窗口则显示这样

但是这不是我们的插件激活场景,我们的插件激活场景是在触发指令的时候。
我们按F1(在新弹出来的那个窗口),选择我们注册的指令,注意这里是和contributes.commands中你配置的指令的title关联的

然后按下确认

这个时候就出发我们registerCommand注册的回调了。
然后回到我们编写代码的窗口,你会发现终端输出了一段log

这就表示我们的插件被激活了。
那么到这,我们已经学会基础的使用了。
现在关闭之前弹出来的窗口,这样你这个插件就停止运行了。
引入依赖#
我们这个是要做一个将$.ajax转换成axios + try catch代码的插件,那么这里就得用上babel[7]里的几个依赖了。
如果你对babel不是很了解,你可以看我之前写的一篇学习文章:babel入门学习 - 知乎 (zhihu.com)
然后我们来引入依赖,由于是ts项目,我们还得引入对应的types,避免报错。


npm install @babel/core @babel/generator @babel/parser @babel/preset-env @babel/traverse @babel/types
npm install @types/babel__generator @types/babel__traverse -D项目配置#
在开始敲代码之前,我们还需要配置一下我们的项目
ts配置#
前面说了引入babel是不能直接在ts项目中用的,我们还得引入对应的types包,但是引入types包也还是不能直接用的,我们还得在tsconfig.json中配置下

其中我们最重要的是配置types。
至于target和lib都是我用的api有需求才做的调整,我后面会用到String.prototype.replaceAll[8]这个方法,而这个方法的兼容性还是比较差的。
vscode指令配置#
前面我们知道了contributes是用来修改vscode的,所以我们可以拓展一些场景用来作为我们的插件入口。
"activationEvents": [
"onCommand:jq-ajax-2-axios.transform"
],
"contributes": {
"commands": [
{
"command": "jq-ajax-2-axios.transform",
"title": "transform $.ajax block"
}
],
"menus": {
"editor/context": [
{
"command": "jq-ajax-2-axios.transform",
"group": "1_modification"
}
]
},
"keybindings": [
{
"command": "jq-ajax-2-axios.transform",
"key": "ctrl+shift+q",
"mac": "cmd+shift+q"
}
]
}, 
group: 1_modification:这个则表示在目录的哪块区域

keybindings[11]:这个很好理解,就是快捷键,这里是区分MacOS、Linux和Window的,大家都知道是为啥,这里就不多说了。
那么就配置完了,接下来我们来开始写代码
实现#
分析#
既然我们是要将$.ajax转换成axios + try catch的方式,那么我们就需要了解这两个的格式和结构。
来看一个典型的$.ajax
$.ajax({
type: 'post',
url: "/ajax/xxx",
data: '123',
error() {
console.log('error')
},
fail: () => {
console.log('i am fail')
},
success(result) {
result = $.parseJSON(result);
if (result.success) {
console.log('i am success')
} else {
console.log('i am not success')
}
},
complete() {
console.log('i am finish')
},
}); 传入一个对象,但这个对象它有属性,有method,而method中又还有function/arrow function的区别,我们要做的就是将以下的代码提取出来。
typeurldatasuccessfailerrorcomplete
至于如何拿到这些,我们就需要借助babel了。
拿到之后我们直接就用str拼接即可,就不用babel/template[12]了。
获取选中区域#
不过第一步得先拿到选中区域。怎么拿到选中区域呢?
这个时候就需要vscode的api了。
回到我们的extension.ts文件中
export function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand(
"jq-ajax-2-axios.transform",
() => {
// 当前获取编辑器
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const allSelection = editor.selection;
let start = new vscode.Position(allSelection.start.line, 0);
let end = new vscode.Position(
allSelection.end.line,
99999999999999999999999999999999
);
let range = new vscode.Range(start, end);
// 获取选择的那段文本
const text = editor.document.getText(range);
let textTrim = text.trim();
// ...
}
);
context.subscriptions.push(disposable);
} vscode.window:[13] 也就是窗口window.activeTextEditor[14]:获取当前的编辑窗口activeTextEditor.selection[15]: 获取选中区域对象,它里面包含了选中的起始位置vscode.Position[16]: 这玩意儿是二维的,需要你传入行数和列数,是不可变的(immutable)。vscode.Range[17]: 创建一个范围,我们需要传入两个Position对象作为start和end。editor.document[18].getText: 获取编辑窗口中选中的范围,接收一个Range。
最终拿到的text就是我们通过鼠标滑出来的区域。
可以打个log看下是否正常拿到内容


可以看到正常拿到了。
然后我们来解析这块区域的内容
解析内容#
这里就要借助我们的babel了。
import * as vscode from "vscode";
import * as parser from "@babel/parser";
import {
ObjectProperty,
Identifier,
isArrowFunctionExpression,
isFunctionExpression,
} from "@babel/types";
import * as traverse from "@babel/traverse";
import * as generator from "@babel/generator";
// ...
try {
const parseResult = parser.parse(textTrim);
let method = "get";
let url = "";
let data = null;
let success = null;
let error = null;
let complete = null;
let fail = null;
const targetObjPropDict: Record<string, any> = {
type: (type: string) => {
method = type;
},
data: (_data: any) => {
data = _data;
},
url: (_url: string) => {
url = _url;
},
error: (param: FuncType) => {
error = param;
},
success: (param: FuncType) => {
success = param;
},
complete: (param: FuncType) => {
complete = param;
},
fail: (param: FuncType) => {
fail = param;
},
};
traverse.default(parseResult, {
// 获取必要的几个数据
ObjectProperty(path) {
let node: ObjectProperty = path.node;
let key = node.key as Identifier;
let fn = targetObjPropDict[key.name];
if (fn) {
let value = node.value;
// () => {} or function () {}
if (
isArrowFunctionExpression(value) ||
isFunctionExpression(value)
) {
let params = value.params;
let body = value.body;
let isAsync = value.async;
fn({
isAsync,
body: generator.default(body).code,
params: generator.default(params[0]).code,
});
} else {
const res = generator.default(value);
fn(res.code);
}
}
},
ObjectMethod(path) {
let node = path.node as any;
let key = node.key;
let params = node.params;
let fn = targetObjPropDict[key.name];
if (fn) {
let isAsync = node.async;
let body = node.body;
params = generator.default(params[0]).code;
body = generator.default(body).code;
fn({
params,
isAsync,
body,
});
}
},
});
// ...
} catch (error) {
throw new Error((error as string) || "tranform error");
}在刚刚的代码基础上,加上上面的代码。
这里我简单的解释下:
-
parser.parse(textTrim):将文本转换成AST。 -
traverse.default(ast):遍历整棵AST Tree,我在遍历过程中监听了几个场景: -
ObjectProperty:对象属性,这里面有我们的type、data、url以及function/arrow function,这个function和arrow function指的是比如/error: function () {}/error: () =>{}这两种写法,它们也算是ObjectPropertyObjectMethod:这个其实也是方法,不过是error () {}这种写法。和上面函数的写法没差别,至于为什么要做区分,我个人觉得应该是和其他语言里的”关联函数“和函数的区别吧,用method和function区分。
-
generator.default:将ast转换为字符串。
这个过程中我们拿到了几乎所有的内容。
支持额外配置#
我们还可以提供插件配置入口给使用者,这样用户可以通过setting.json的方式做一些不同的行为
const { reg_str = [], axios_path = '', axios_alias = '', replace_all = false, remove_fn_wrapper = false, } = vscode.workspace.getConfiguration("jq_ajax_2_axios.config") || {}; reg_str:一个二维数组,元素类型为:[string, string],第一个为一个正则表达式,第二个则是替换的内容。这样用户就可以自定义哪些内容自动替换成另一些内容。axios_path:你的axios路径,你可以配置成import xx的方式,这样就不需要手动导入了。axios_alias:axios的别名,比如你如果是直接将axios放到window上或者this上,那么你这个时候就可以改成this.axios,这样就不需要手动调整了,也不需要导入了。remove_fn_wrapper:是否需要用函数包裹你的内容vscode.workspace[19].getConfiguration: 获取工作区的配置内容。
转换#
现在我们拿到用户自定义配置内容和数据了,这个时候就可以进行转换了。
其实这里应该搭配上babel/template的,但是我图省事就不这么做了。
在extension.ts文件下面创建一个template.ts文件
export function genTemplate(params: Record<string, any>): string {
const {
method = "get",
data = "{}",
url = "",
success,
error,
complete,
fail,
reg_str,
axios_path,
axios_alias,
replace_all,
remove_fn_wrapper
} = params;
const errParamsName = error?.params || fail?.params || "_err";
if (fail && fail?.body !== "{}") {
let body = fail.body.trim();
fail.body = body.slice(1, body.length - 1);
}
if (error && error?.body !== "{}") {
let body = error.body.trim();
error.body = body.slice(1, body.length - 1);
}
if (success && success?.body != "{}") {
let body = success.body.trim();
success.body = body.slice(1, body.length - 1);
}
if (complete && complete?.body != "{}") {
let body = complete.body.trim();
complete.body = body.slice(1, body.length - 1);
}
const axios_name = axios_alias || 'axios';
return reg_str.reduce(
(prev: string, current: [string, string]) => {
const [rule, target] = current;
const reg = replace_all ? new RegExp(rule, 'gi') : new RegExp(rule);
return replace_all ? prev.replaceAll(reg, target) : prev.replace(reg, target);
},
`
${remove_fn_wrapper ? '' : 'async function use_axios() {'}
${axios_path}\n
try {
${
success
? `let ${
success.params || "res"
} = await ${axios_name}.${method.replaceAll(/\'|\"/gi, '')}(${url}, ${
["''", '""'].includes(data) ? "{}" : data
});`
: ""
}
${success && success?.body != "{}" ? success.body : ""}
} catch (${errParamsName}) {
${fail && fail?.body !== "{}" ? fail.body : ""}
${error && error?.body !== "{}" ? error.body : ""}
} ${
complete && complete?.body !== "{}"
? `finally {
${complete.body}
}`
: ""
}
${remove_fn_wrapper ? '' : '}\n use_axios();'}
`
);
}可读性挺差,但是很好理解,就是一个简单的字符串模板。
没啥好说的,也没啥技术含量。
接着导入到extension.ts中
// ...
const template = genTemplate({
method,
data,
url,
success,
error,
complete,
fail,
reg_str,
axios_path,
axios_alias,
replace_all,
remove_fn_wrapper
});
editor.edit((editBuilder) => {
editBuilder.replace(range, template);
});
// ...textEditor.edit:它接收一个回调,回调的参数会传入一个editBuilder[20]

我们通过这个editBuilder来将我们最新的代码替换原来的代码
现在就ok了,我们来试下
顺便加点配置


大功告成~
发布#
既然完成了,那接下来自然就是发布。
这一步稍微有那么点复杂。
首先我们需要去到Azure DevOps Services | Microsoft Azure
然后创建一个账户

注册完之后到个人页面找到这个

然后创建一个新的token


然后保存好你的token,接下来要用来登陆的

然后回到我们的项目中,新开个终端
npm install -g @vscode/vsce这个vsce是vscode发布插件的工具
你可以试下vsce --version测试下是否安装成功
然后登录
vsce login <publisher name>然后会让你输入token,你把刚才保存的token复制过来输入即可。
登录成功后就可以发布了
vsce publish 另外如果你是多次发布,你需要修改你的版本号,对应package.json里的version,你可以使用
vsce publish patch:表示更新一个小版本,比如1.0.0 -> 1.0.1vsce publish minor:表示更新一个中版本,即1.0.0 -> 1.1.0vsce publish major:更新一个大版本,即1.0.0 -> 2.0.0
不过在发布前,你一般可以同步你的个人信息或者github项目地址
比如:

注意这里的icon,表示你的插件的图标,放在根目录即可,最好是128 * 128大小的。
另外最好补充下README,因为这个将作为你的插件的教程。
发布出去后你可以去到https://marketplace.visualstudio.com/manage发布市场去看下自己的插件是否发布成功了。

那么到这里,一个完整的流程就结束了。
总结#
莫得
补充#
另外有一点需要注意

创建项目的时候最好看下engines的版本,如果大于当前vscode的版本,那就是有问题的,你需要手动调整到适应的版本
参考#
- ^yo https://www.npmjs.com/package/yo
- ^generator-code https://www.npmjs.com/package/generator-code
- ^activationEvents https://code.visualstudio.com/api/references/activation-events
- ^contributes https://code.visualstudio.com/api/references/contribution-points
- ^contributes.colors https://code.visualstudio.com/api/references/contribution-points#contributes.colors
- ^registerCommand https://code.visualstudio.com/api/references/vscode-api#commands.registerCommand
- ^babel https://babeljs.io/
- ^String.prototype.replaceAll https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
- ^commands https://code.visualstudio.com/api/references/contribution-points#contributes.commands
- ^menus https://code.visualstudio.com/api/references/contribution-points#contributes.menus
- ^keybinds https://code.visualstudio.com/api/references/contribution-points#contributes.keybindings
- ^@babel/template https://babeljs.io/docs/babel-template
- ^vscode.window https://code.visualstudio.com/api/references/vscode-api#window
- ^activeTextEditor https://code.visualstudio.com/api/references/vscode-api#window
- ^TextEditor.Selection https://code.visualstudio.com/api/references/vscode-api#Selection
- ^vscode.Position https://code.visualstudio.com/api/references/vscode-api#Position
- ^vscode.Range https://code.visualstudio.com/api/references/vscode-api#Range
- ^textDocument https://code.visualstudio.com/api/references/vscode-api#TextDocument
- ^vscode.workspace https://code.visualstudio.com/api/references/vscode-api#workspace
- ^textEditOrEdit https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit
编辑于 2023-08-07 14:07・IP 属地广东
