visicode插件:实现新建小程序页面/组件模板文件-简版

656 阅读6分钟

为什么写了这个插件

最近工作中,突然对visicode插件开发感兴趣,但又没想好以什么插件开始,刚好在写小程序,就索性先尝试写一个支持快速创建小程序页面/组件的模板文件的插件。

插件实现了哪些功能?

  • 支持在文件树右键点击新建小程序页面/组件
  • 新建小程序也页面的同时修改app.json文件
  • 如果是非小程序工程,该插件不生效

接下来我们先来提前感受下插件项目的初始化搭建,以及hello world模板插件的学习,如果不想看这一部分,可以提前跳到最后一节。

小试牛刀-感受一下hellowrld

1. 安装脚手架

npm install -g yo generator-code 

2. 新建项目

yo code

image.png

然后按照提示一步步选择就行,我选择了New Extension(TypeScript),后面就是写一些插件的名称,标识,描述,是否初始化为一个git仓库,是否需要使用webpack打包,包管理器等。这是我的选择:

image.png

项目新建完以后,默认是一个Hello World的示例插件,可以看下效果。

3. 插件启动

npm run watch

4. 看下插件效果-调试插件

image.png

点击后,会自动打开一个新的visicode窗口,按下 command+shift+p ,输入Hello World,会弹出一个 Hello World from mini-helper! 的消息提示框。

image.png

学习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中配置的 activationEventsonCommand:mini-helper.helloWorld。 那就是mini-helper.helloWorld这个命令被调用的时候激活插件。
  • deactivate:当插件关闭前执行。

4. 小结一下

原来项目模板自带的插件,做了三件事:

  1. 在package.json中注册一个命令
"contributes": {
    "commands": [
      {
        "command": "mini-helper.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  1. 在package.json中配置插件激活事件
"activationEvents": [
    "onCommand:mini-helper.helloWorld"
 ]
  1. 编写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"
      }]
    }
  },

可以看下效果:

image.png

2. 实现输入框

点击 新建小程序页面 菜单项后,出现一个输入框,可以输入新的文件夹的名称,效果如下:

image.png

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() {}