为什么写了这个插件
最近工作中,突然对visicode插件开发感兴趣,但又没想好以什么插件开始,刚好在写小程序,就索性先尝试写一个支持快速创建小程序页面/组件的模板文件的插件。
插件实现了哪些功能?
- 支持在文件树右键点击新建小程序页面/组件
- 新建小程序也页面的同时修改app.json文件
- 如果是非小程序工程,该插件不生效
接下来我们先来提前感受下插件项目的初始化搭建,以及hello world模板插件的学习,如果不想看这一部分,可以提前跳到最后一节。
小试牛刀-感受一下hellowrld
1. 安装脚手架
npm install -g yo generator-code
2. 新建项目
yo code
然后按照提示一步步选择就行,我选择了New Extension(TypeScript),后面就是写一些插件的名称,标识,描述,是否初始化为一个git仓库,是否需要使用webpack打包,包管理器等。这是我的选择:
项目新建完以后,默认是一个Hello World的示例插件,可以看下效果。
3. 插件启动
npm run watch
4. 看下插件效果-调试插件
点击后,会自动打开一个新的visicode窗口,按下 command+shift+p ,输入Hello World,会弹出一个 Hello World from mini-helper! 的消息提示框。
学习Hello world插件
1. 认识一下项目结构
.
├── .eslintrc.json
├── .gitignore
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── extension.ts // 入口文件
│ └── test
├── tsconfig.json
└── vsc-extension-quickstart.md
- package.json 插件的一些配置项,都在这里
- extension.ts 插件的入口文件
2.认识package.json中的各个字段含义
{
"name": "mini-helper",
"displayName": "mini-helper",
"description": "小程序开发助手",
"version": "0.0.1",
"engines": {
"vscode": "^1.76.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:mini-helper.helloWorld"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "mini-helper.helloWorld",
"title": "Hello World"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.76.0",
"@types/glob": "^8.0.1",
"@types/mocha": "^10.0.1",
"@types/node": "16.x",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"eslint": "^8.33.0",
"glob": "^8.1.0",
"mocha": "^10.1.0",
"typescript": "^4.9.4",
"@vscode/test-electron": "^2.2.2"
}
}
- main: 配置插件的入口
- activationEvents: 激活事件,比如调用命令时激活(onCommand)、启动后激活(onStartupFinished)等
- contributes 内容配置,比如注册一些命令,拓展菜单等。
3. 认识插件入口文件extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "mini-helper" is now active!');
// 在这里注册命令,命令是package.json => contributes => commands => command 中配置的命令
let disposable = vscode.commands.registerCommand('mini-helper.helloWorld', () => {
// 当这个命令别激活的时候,弹出一个消息提示框
vscode.window.showInformationMessage('Hello World from mini-helper!');
});
context.subscriptions.push(disposable);
}
export function deactivate() {}
extension文件包含两部分,一部分是 activate ,一部分是 deactivate。
- activate: 当插件被激活时执行。比如,这个例子中,package.json中配置的
activationEvents是onCommand:mini-helper.helloWorld。 那就是mini-helper.helloWorld这个命令被调用的时候激活插件。 - deactivate:当插件关闭前执行。
4. 小结一下
原来项目模板自带的插件,做了三件事:
- 在package.json中注册一个命令
"contributes": {
"commands": [
{
"command": "mini-helper.helloWorld",
"title": "Hello World"
}
]
},
- 在package.json中配置插件激活事件
"activationEvents": [
"onCommand:mini-helper.helloWorld"
]
- 编写extension.ts, 注册事件
export function activate(context: vscode.ExtensionContext) {
// 在这里注册命令,命令是package.json => contributes => commands => command 中配置的命令
let disposable = vscode.commands.registerCommand('mini-helper.helloWorld', () => {
// 当这个命令别激活的时候,弹出一个消息提示框
vscode.window.showInformationMessage('Hello World from mini-helper!');
});
context.subscriptions.push(disposable);
}
学习完,前置知识,现在我们就可以步入正题了,开始我们的插件开发了。
磨刀霍霍-新建小程序页面/组件模板文件插件开发
1. 新增两个菜单【新建小程序文件/新建小程序页面】
新增菜单,需要在package.json中的contributes新增menu配置。
- explorer/context:资源管理器上下文菜单
- command:必须配置,点击菜单后触发的命令
- when:菜单什么时候出现,explorerResourceIsFolder所选文件是文件夹的时候才出现
- group:配置分组信息
完整配置如下:
"contributes": {
"commands": [
{
"command": "mini-helper.helloWorld",
"title": "Hello World"
},
{
"command": "mini-helper.createMiniPage",
"title": "新建小程序页面"
},
{
"command": "mini-helper.createMiniComponent",
"title": "新建小程序组件"
}
],
"menus": {
"explorer/context": [{
"command": "mini-helper.createMiniPage",
"when": "explorerResourceIsFolder",
"group": "1_modification"
},{
"command": "mini-helper.createMiniComponent",
"when": "explorerResourceIsFolder",
"group": "1_modification"
}]
}
},
可以看下效果:
2. 实现输入框
点击 新建小程序页面 菜单项后,出现一个输入框,可以输入新的文件夹的名称,效果如下:
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "mini-helper" is now active!');
let disposable = vscode.commands.registerCommand(
"mini-helper.helloWorld",
() => {
vscode.window.showInformationMessage("Hello World from mini-helper!");
}
);
let disposableCreateMiniPage = vscode.commands.registerCommand(
"mini-helper.createMiniPage",
() => {
const inputBox = vscode.window.createInputBox();
inputBox.prompt =
"创建小程序文件:axml/ts/less/json, 输入以/结尾的路径会创建一个文件夹";
inputBox.placeholder = "请输入路径";
inputBox.show();
inputBox.onDidChangeValue((value) => {
console.log(value, "onDidChangeValue");
if (!value) {
inputBox.validationMessage = "请输入路径";
return;
}
});
inputBox.onDidAccept(() => {
console.log("onDidAccept", inputBox.value);
inputBox.hide();
});
}
);
context.subscriptions.push(disposable);
context.subscriptions.push(disposableCreateMiniPage);
}
export function deactivate() {}
3. 定义模板文件
在src下新建templates文件夹,用来存放,axml、json和ts的模板
src/templates/axmlTemplate.ts:
const axmlTemplate = `<view>
New View
</view>
`;
export default axmlTemplate;
src/templates/pageJsonTemplate.ts:
const pageJsonTemplate = `{
"usingComponents": {}
}
`;
export default pageJsonTemplate;
src/templates/pageTemplate.ts:
Page({
data: {},
onLoad() {},
});
`;
export default pageTemplate;
src/templates/index.ts
import axmlTemplate from "./axmlTemplate";
import pageJsonTemplate from "./pageJsonTemplate";
import pageTemplate from "./pageTemplate";
export default {
axmlTemplate,
pageTemplate,
pageJsonTemplate,
};
4. 实现page文件夹新建并自动创建模板文件
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import template from "./templates";
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "mini-helper" is now active!');
let disposable = vscode.commands.registerCommand(
"mini-helper.helloWorld",
() => {
vscode.window.showInformationMessage("Hello World from mini-helper!");
}
);
let disposableCreateMiniPage = vscode.commands.registerCommand(
"mini-helper.createMiniPage",
(uri: vscode.Uri) => {
const inputBox = vscode.window.createInputBox();
inputBox.prompt =
"创建小程序文件:axml/ts/less/json, 输入以/结尾的路径会创建一个文件夹";
inputBox.placeholder = "请输入路径";
inputBox.show();
inputBox.onDidChangeValue((value) => {
console.log(value, "onDidChangeValue");
if (!value) {
inputBox.validationMessage = "请输入路径";
return;
}
});
inputBox.onDidAccept(() => {
console.log("onDidAccept", inputBox.value);
inputBox.hide();
const fsPath = uri.fsPath;
const value = inputBox.value;
const curFileDir = path.resolve(fsPath, value);
const curPath =
value.lastIndexOf("/") < 0 ? `${value}/${value}` : `${value}index`;
fs.mkdir(curFileDir, (err) => {
if (err) {
return;
}
[
{
template: template.axmlTemplate,
suffix: ".axml",
},
{
suffix: ".less",
template: "",
},
{
template: template.pageTemplate,
suffix: ".ts",
},
{
template: template.pageJsonTemplate,
suffix: ".json",
},
].forEach((item) => {
fs.appendFileSync(
path.resolve(fsPath, `${curPath}${item.suffix}`),
item.template
);
});
});
});
}
);
context.subscriptions.push(disposable);
context.subscriptions.push(disposableCreateMiniPage);
}
5.更新app.json
export function activate(context: vscode.ExtensionContext) {
...
let disposableCreateMiniPage = vscode.commands.registerCommand(
"mini-helper.createMiniPage",
(uri: vscode.Uri) => {
...
inputBox.onDidAccept(() => {
...
fs.mkdir(curFileDir, (err) => {
...
+ const appJsonPath = path.resolve(
+ fsPath.slice(0, fsPath.indexOf("src") + 3),
+ "./app.json"
+ );
+ const fileInfo = fs.readFileSync(appJsonPath, "utf-8");
+ const data = JSON.parse(fileInfo);
+ data.pages.push(`pages/${curPath}`);
+ fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
});
});
}
);
6. 如果是非小程序工程,进行提示
我们约定这样的目录结构,页面必须在src/pages下创建,app.json在src目录下,满足这样的条件,才可以创建页面文件。整体代码如下:
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import template from "./templates";
const getPathInfo = (fsPath: string) => {
const index = fsPath.indexOf("src");
const rootPath = fsPath.slice(0, index - 1);
console.log(rootPath);
const appJsonPath = path.resolve(rootPath, "./src/app.json");
const hasAppJson = fs.existsSync(appJsonPath);
const isMiniProject = fs.existsSync(
path.resolve(rootPath, "./mini.project.json")
);
console.log(index, fsPath);
return {
fsPath,
path: fsPath.slice(index),
rootPath,
appJsonPath,
hasAppJson,
isMiniProject,
isInSrc: index >= 0,
isInPages: fsPath.indexOf("pages") >= 0,
};
};
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "mini-helper" is now active!');
let disposable = vscode.commands.registerCommand(
"mini-helper.helloWorld",
() => {
vscode.window.showInformationMessage("Hello World from mini-helper!");
}
);
let disposableCreateMiniPage = vscode.commands.registerCommand(
"mini-helper.createMiniPage",
(uri: vscode.Uri) => {
const fsPath = uri.fsPath;
const pathInfo = getPathInfo(fsPath);
if (!pathInfo.isInSrc) {
vscode.window.showErrorMessage("请在src目录下创建");
return;
}
if (!pathInfo.hasAppJson) {
vscode.window.showErrorMessage("app.json文件不存在,无法创建");
return;
}
if (!pathInfo.isInPages) {
vscode.window.showErrorMessage("页面必须建在pages目录下");
return;
}
const inputBox = vscode.window.createInputBox();
inputBox.prompt =
"创建小程序文件:axml/ts/less/json, 输入以/结尾的路径会创建一个文件夹";
inputBox.placeholder = "请输入路径";
inputBox.show();
inputBox.onDidChangeValue((value) => {
console.log(value, "onDidChangeValue");
if (!value) {
inputBox.validationMessage = "请输入路径";
return;
}
});
inputBox.onDidAccept(() => {
console.log("onDidAccept", inputBox.value);
inputBox.hide();
const fsPath = uri.fsPath;
const value = inputBox.value;
const curFileDir = path.resolve(fsPath, value);
const curPath =
value.lastIndexOf("/") < 0 ? `${value}/${value}` : `${value}index`;
fs.mkdir(curFileDir, (err) => {
if (err) {
return;
}
[
{
template: template.axmlTemplate,
suffix: ".axml",
},
{
suffix: ".less",
template: "",
},
{
template: template.pageTemplate,
suffix: ".ts",
},
{
template: template.pageJsonTemplate,
suffix: ".json",
},
].forEach((item) => {
fs.appendFileSync(
path.resolve(fsPath, `${curPath}${item.suffix}`),
item.template
);
});
const appJsonPath = pathInfo.appJsonPath;
const fileInfo = fs.readFileSync(appJsonPath, "utf-8");
const data = JSON.parse(fileInfo);
data.pages.push(`pages/${curPath}`);
fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
});
});
}
);
context.subscriptions.push(disposable);
context.subscriptions.push(disposableCreateMiniPage);
}
export function deactivate() {}
7. 兼容新建小程序组件
- 由于新建小程序文件的核心代码差不多,只是小程序组件的ts文件和json文件存在一点差异,可以再新建两个模板文件:
src/templates/componentJsonTemplate.ts
const componentJsonTemplate = `{
"component": true,
"usingComponents": {}
}
`;
export default componentJsonTemplate;
src/templates/componentTemplate.ts
Component({
mixins: [],
data: {},
props: {},
didMount() {},
didUpdate() {},
didUnmount() {},
methods: {},
});`;
export default componentTemplate;
- 将创建文件夹的方法抽离出来
enum CreateTypeEnum {
page,
component,
}
function createFile(
type: CreateTypeEnum,
pathInfo: ReturnType<typeof getPathInfo>
) {
const { path: currentPath, fsPath } = pathInfo;
const inputBox = vscode.window.createInputBox();
inputBox.prompt =
"创建小程序文件:axml/ts/less/json, 输入以/结尾的路径会创建一个文件夹";
inputBox.placeholder = "请输入路径";
inputBox.show();
inputBox.onDidChangeValue((value) => {
console.log(value, "onDidChangeValue");
if (!value) {
inputBox.validationMessage = "请输入路径";
return;
}
if (value.lastIndexOf("/") < 0) {
inputBox.prompt = `将会创建这些文件 ${currentPath}/${value}/${value}.{axml,less,ts,json}, 输入以/结尾的路径会创建一个文件夹`;
} else {
inputBox.prompt = `将会创建这些文件 ${currentPath}/${value}index.{axml,less,ts,json}, 输入以/结尾的路径会创建一个文件夹`;
}
});
inputBox.onDidAccept(() => {
console.log("onDidAccept");
inputBox.hide();
const value = inputBox.value;
const curFileDir = path.resolve(fsPath, value);
const curPath =
value.lastIndexOf("/") < 0 ? `${value}/${value}` : `${value}index`;
fs.mkdir(curFileDir, (err) => {
if (err) {
console.log(err);
return;
}
[
{
template: template.axmlTemplate,
suffix: ".axml",
},
{
suffix: ".less",
template: "",
},
{
template:
type === CreateTypeEnum.page
? template.pageTemplate
: template.componentTemplate,
suffix: ".ts",
},
{
template:
type === CreateTypeEnum.page
? template.pageJsonTemplate
: template.componentJsonTemplate,
suffix: ".json",
},
].forEach((item) => {
fs.appendFileSync(
path.resolve(fsPath, `${curPath}${item.suffix}`),
item.template
);
});
if (type === CreateTypeEnum.page) {
const appJsonPath = pathInfo.appJsonPath;
const fileInfo = fs.readFileSync(appJsonPath, "utf-8");
const data = JSON.parse(fileInfo);
data.pages.push(`pages/${curPath}`);
fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
}
});
});
}
8.最终代码参考如下
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import template from "./templates";
const getPathInfo = (fsPath: string) => {
const index = fsPath.indexOf("src");
const rootPath = fsPath.slice(0, index - 1);
console.log(rootPath);
const appJsonPath = path.resolve(rootPath, "./src/app.json");
const hasAppJson = fs.existsSync(appJsonPath);
const isMiniProject = fs.existsSync(
path.resolve(rootPath, "./mini.project.json")
);
console.log(index, fsPath);
return {
fsPath,
path: fsPath.slice(index),
rootPath,
appJsonPath,
hasAppJson,
isMiniProject,
isInSrc: index >= 0,
isInPages: fsPath.indexOf("pages") >= 0,
};
};
enum CreateTypeEnum {
page,
component,
}
function createFile(
type: CreateTypeEnum,
pathInfo: ReturnType<typeof getPathInfo>
) {
const { path: currentPath, fsPath } = pathInfo;
const inputBox = vscode.window.createInputBox();
inputBox.prompt =
"创建小程序文件:axml/ts/less/json, 输入以/结尾的路径会创建一个文件夹";
inputBox.placeholder = "请输入路径";
inputBox.show();
inputBox.onDidChangeValue((value) => {
console.log(value, "onDidChangeValue");
if (!value) {
inputBox.validationMessage = "请输入路径";
return;
}
if (value.lastIndexOf("/") < 0) {
inputBox.prompt = `将会创建这些文件 ${currentPath}/${value}/${value}.{axml,less,ts,json}, 输入以/结尾的路径会创建一个文件夹`;
} else {
inputBox.prompt = `将会创建这些文件 ${currentPath}/${value}index.{axml,less,ts,json}, 输入以/结尾的路径会创建一个文件夹`;
}
});
inputBox.onDidAccept(() => {
console.log("onDidAccept");
inputBox.hide();
const value = inputBox.value;
const curFileDir = path.resolve(fsPath, value);
const curPath =
value.lastIndexOf("/") < 0 ? `${value}/${value}` : `${value}index`;
fs.mkdir(curFileDir, (err) => {
if (err) {
console.log(err);
return;
}
[
{
template: template.axmlTemplate,
suffix: ".axml",
},
{
suffix: ".less",
template: "",
},
{
template:
type === CreateTypeEnum.page
? template.pageTemplate
: template.componentTemplate,
suffix: ".ts",
},
{
template:
type === CreateTypeEnum.page
? template.pageJsonTemplate
: template.componentJsonTemplate,
suffix: ".json",
},
].forEach((item) => {
fs.appendFileSync(
path.resolve(fsPath, `${curPath}${item.suffix}`),
item.template
);
});
if (type === CreateTypeEnum.page) {
const appJsonPath = pathInfo.appJsonPath;
const fileInfo = fs.readFileSync(appJsonPath, "utf-8");
const data = JSON.parse(fileInfo);
data.pages.push(`pages/${curPath}`);
console.log(data);
fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2), "utf-8");
}
});
});
}
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "mini-helper" is now active!');
let disposable = vscode.commands.registerCommand(
"mini-helper.helloWorld",
() => {
vscode.window.showInformationMessage("Hello World from mini-helper!");
}
);
let disposableCreateMiniPage = vscode.commands.registerCommand(
"mini-helper.createMiniPage",
(uri: vscode.Uri) => {
const fsPath = uri.fsPath;
const pathInfo = getPathInfo(fsPath);
if (!pathInfo.isInSrc) {
vscode.window.showErrorMessage("请在src目录下创建");
return;
}
if (!pathInfo.hasAppJson) {
vscode.window.showErrorMessage("app.json文件不存在,无法创建");
return;
}
if (!pathInfo.isInPages) {
vscode.window.showErrorMessage("页面必须建在pages目录下");
return;
}
createFile(CreateTypeEnum.page, pathInfo);
}
);
let disposableCreateComponent = vscode.commands.registerCommand(
"mini-helper.createMiniComponent",
(uri: vscode.Uri) => {
const fsPath = uri.fsPath;
const pathInfo = getPathInfo(fsPath);
if (!pathInfo.isInSrc) {
vscode.window.showErrorMessage("请在src目录下创建");
return;
}
createFile(CreateTypeEnum.component, pathInfo);
}
);
context.subscriptions.push(disposable);
context.subscriptions.push(disposableCreateMiniPage);
context.subscriptions.push(disposableCreateComponent);
}
export function deactivate() {}