中台前端套件

MFK(mid frontend kits)

插件脚手架

yeoman generator-code

npm install -g yo generator-code
yo code

.vscode  配置工程调试文件

src/test  测试用例

extension.ts 主入口文件

.*  各种工具配置文件

package.json  依赖、命令配置

"commands": [
  {
    "command": "cyberx.open",
    "title": "CyberX: 打开"
  },
  {
    "command": "cyberx.close",
    "title": "CyberX: 关闭"
  },
  {
    "command": "cyberx.reload",
    "title": "CyberX: 重载"
  },
  {
    "command": "cyberx.tpl.createView",
    "title": "CyberX: 新建组件"
  },
  {
    "command": "cyberx.tpl.createForm",
    "title": "CyberX: 创建表单"
  },
  {
    "command": "cyberx.tpl.commit",
    "title": "CyberX: 提交模板"
  }
],
"menus": {
  "explorer/context": [
    {
      "command": "cyberx.tpl.createView",
      "group": "1_modification",
      "when": "explorerResourceIsFolder"
    },
    {
      "command": "cyberx.tpl.createForm",
      "group": "1_modification",
      "when": "explorerResourceIsFolder"
    }
  ],
  "editor/context": [
    {
      "command": "cyberx.tpl.commit",
      "group": "1_modification",
      "when": "editorHasSelection"
    }
  ]
}
import * as vscode from 'vscode';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
	
	console.log('Congratulations, your extension "demo" is now active!');

	let disposable = vscode.commands.registerCommand('demo.helloWorld', () => {
		vscode.window.showInformationMessage('Hello World from demo!');
	});

	context.subscriptions.push(disposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}

const { VueLoaderPlugin } = require('vue-loader');

chainWebpack: (config) => {
  config.module
    .rule('vue')
    .test(/\.vue$/)
    .use('vue-loader')
    .loader('vue-loader');

  config
    .plugin('vue-loader-plugin')
    .use(new VueLoaderPlugin());
}
import { Location, useLocation } from 'umi'
import { VueWrapper } from 'vuera'
import FormBuilder from "./formBuilder.vue"


export default function FormPage() {
    const location:Location = useLocation();
    const savePath = location.query ? location.query.savePath : '';


    return (
        <div>
            <VueWrapper
            component={FormBuilder}
            savePath={savePath}
            />
        </div>
    )
}

构建改造

插件功能

插件 extension.ts 编译 [esbuild]

import esbuild from "esbuild";

// ... options

platform: "node",
  entryPoints: [path.resolve("src", "extension.ts")],
    external: ["vscode", "snowpack", "esbuild"],
      outdir: path.resolve("out"),
        plugins: [
          svgrPlugin(),
          sassPlugin(),
          {
            name: "extension-env-resolver",
            setup(builder) {
              builder.onResolve({ filter: /@esbuild-env/ }, () => {
                return {
                  path: "@esbuild-env",
                  namespace: "@esbuild-env",
                };
              });
              builder.onLoad({ filter: /@esbuild-env/ }, () => {
                return {
                  contents: JSON.stringify(isDev ? devEnv : prodEnv),
                  loader: "json",
                };
              });
            },
          },
        ],
bundle: true,
if (isDev) {
  esbuild.build({
    ...extensionCommonBuildOptions,
    sourcemap: "both",
    watch: {
      onRebuild(error, result) {
        if (error) trackMessage(error, console.error);
        else trackMessage(result, console.log);
      },
    },
  });
} else {
  esbuild
    .build({
    ...extensionCommonBuildOptions,
    minify: true,
    treeShaking: true,
  })
    .catch(console.error);
}

插件 extension.ts 编译 [esbuild]

webview交互

React 编译

const config = {
  mount: {
    src: { url: "/" },
  },
  plugins: [
    "@snowpack/plugin-sass",
    "@snowpack/plugin-react-refresh",
    "snowpack-plugin-svgr",
  ],
  packageOptions: {
    /* ... */
  },
  optimize: {
    sourcemap: "both",
  },
  devOptions: {
    open: "none",
    output: "stream",
    hmr: true,
  },
  buildOptions: {
    clean: true,
    out: "../../out/ui",
    baseUrl: "./",
  },
};

export default config;
import type * as vscode from "vscode";
import * as path from "path";
export async function loadSnowpackConfig(context: vscode.ExtensionContext) {
	const { loadConfiguration } = await import("snowpack");
	const root = path.resolve(context.extensionPath, "src", "react-ui");
	const configPath = path.resolve(
		context.extensionPath,
		"src",
		"react-ui",
		"snowpack.config.mjs"
	);
	return loadConfiguration({ root }, configPath);
}

snowpack.config.mjs

if (env.ENV === "dev") {
  loadSnowpackConfig(context).then((config) => {
    webviewManager.devServerConfig = {
      port: config.devOptions.port,
      hmrSocketPort: config.devOptions.hmrPort ?? config.devOptions.port,
    };
      vscode.commands.executeCommand(Commands.WebviewControl.Open).then(() => {
        console.log("Successfully opened webview");
      });
  });
}

src/dev/snowpac.dev.ts

extension.ts

// package.json

"private": true,
"workspaces": [
  "src/react-ui"
]

yarn workspaces

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
     "ecmaVersion": 6,
     "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "rules": {
  "@typescript-eslint/naming-convention": "warn",
  "@typescript-eslint/semi": "warn",
  "curly": "warn",
  "eqeqeq": "warn",
  "no-throw-literal": "warn",
  "semi": "off"
},
  "ignorePatterns": ["out", "dist", "**/*.d.ts"]
}

eslint config

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn format
yarn lint:fix
yarn lint:types

husky config

vscode 调试配置  task.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Snowpack Dev",
      "type": "npm",
      "script": "debug",
      "options": {
        "env": {
          "TSC_WATCH": "Both"
        }
      },
      "path": "src/react-ui/package.json",
      "isBackground": true,
      "problemMatcher": "$tsc-watch"
    },
    {
      "label": "Install Extension Dependencies",
      "type": "shell",
      "command": "yarn",
      "group": "none"
    },
    {
      "label": "Install UI Dependencies",
      "type": "shell",
      "command": "yarn",
      "options": {
        "cwd": "${workspaceFolder}/src/react-ui"
      }
    }
  ]
}
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
      "outFiles": ["${workspaceFolder}/out/**/*.js"],
      "sourceMaps": true,
      "preLaunchTask": "${defaultBuildTask}"
    },
    {
      // test code config
    }
  ]
}

vscode 调试配置  launch.json

webview与插件通信

webview 接收信息

useEffect(() => {
  const callback = ({ data }: any) => {
    console.log("webview", data);
  };
  window.addEventListener("message", callback);
  return () => {
    window.removeEventListener("message", callback);
  };
}, []);
window.vscodeAPI = acquireVsCodeApi();

const handleVueCmd = (type: string) => {
  const data: PayloadInterface = mapCmd[type];
  vscodeAPI.postMessage(data);
};

webview 发送信息

插件接收信息

插件发送信息

this.panel.webview.onDidReceiveMessage(
  this.messageHandler
);
this.panel.webview.postMessage(json.serialize(result));
const webviewManager = new WebviewManager(context);
context.subscriptions.push(webviewManager);

extension.ts 实例化 webview 即 下方的 this.panel.webview

代码片段功能

const LIST_PATH =
      env.ENV === "dev"
? path.resolve(__dirname, "../src/react-ui/src")
: path.resolve(__dirname, "../out/ui");


axios.get(`${GITLAB_API_URL}/projects/${PROJECT_ID}/repository/tree`, {
  params: {
    private_token: PRIVATE_TOKEN,
    path: REPO_FILE_PATH,
    ref: BRANCH,
  },
})
...
writeFileSync(path.resolve(writePath, file.file_name), content, {
  encoding: "utf8",
});

代码片段功能

代码片段提取

tpl.content.forEach((res) => {
  if (res.name === 'config.json') {
    const config = JSON.parse(
      fs.readFileSync(res.fullname, { encoding: ENCODING })
    );
    Object.assign(item, config);
    Object.assign(tmp[tplName], config);
  } else if (res.name === 'Index.vue') {
    const content = fs.readFileSync(res.fullname, {
      encoding: ENCODING
    });
    tmp[tplName]['body'] = content.split('\r\n');
  } else if (res.name.includes('preview')) {
    const img = fs.readFileSync(res.fullname);
    item.preview = `data:image/${
    MIME_TYPE[res.extension]
  };base64,${Buffer.from(img).toString('base64')}`;
  }
});
list.push(item);
fs.writeFileSync(
  path.resolve(__dirname, DIST_PATH, `${cateName}.snippets.json`),
  JSON.stringify(tmp, null, 4),
  {
    encoding: ENCODING
  }
);

fs.writeFileSync(
  path.resolve(__dirname, LIST_PATH),
  JSON.stringify(list, null, 4),
  {
    encoding: ENCODING
  }
);
vscode.window
  .showInformationMessage(
  "是否重新打开编辑器使代码片段生效?",
  "否",
  "是"
)
  .then((answer) => {
  if (answer === "是") {
    vscode.commands.executeCommand("workbench.action.reloadWindow");
  } else {
    return true;
  }
});
import fs from 'fs';
import axios, { AxiosResponse } from 'axios';
import { resolve } from 'path';
import type { CommitTplPayload, SnippetPayload } from './request-interface';

const GITLAB_API_URL = "http://code.iot.chinamobile.com/api/v4";
const token = 'xxxxx'

async function queryFilesContentFromRepo(fileList: FileItem[]) {
    const reqList = fileList.map(file => {
        const url = `${GITLAB_API_URL}/projects/${file.pid}/repository/files/${encodeURIComponent(
            file.src
        )}`;
        const options = {}
        return axios.get(url, options);
    });

    return Promise.all(reqList).then((resList: AxiosResponse[]) => {
    	...
        return filesContent;
    });
}

// 脚手架拉取
const targz = require("tar.gz");
const resource = gitUrl + paramsStr;
const tplDir = this.templatePath(this.options.template);
const write = targz().createWriteStream(tplDir);
const read = request.get(resource);
/**
 * @type others
 * @name CyberGuide
 * @title 流程图
 * @description 边缘计算接入指导流程图,支持动效
 * @code <CyberGuide></CyberGuide>
 */

const info = content.match(/\*\s@.*?(\r|\n)/gi);
let matchArr = data.match(/\*\s@(\w+)\s/);
const name = matchArr[1]; //注释前缀
let val = data.replace(/\*\s@\w+\s/, ""); //注释后半截
val = val.replace(/(\n|\r)/, "");

const iconPath = item.fullname.replace(
   /\w+.(vue|js)/,
   "icon.svg"
 );
const img = fs.readFileSync(iconPath);
iconBase64 = `data:image/svg+xml;base64,${Buffer.from(
  img
).toString("base64")}`;
obj.icon = iconBase64;
jsonContent[type].push(obj);
// commit为拼装好的snippets json文件
return axios.post(
  `${GITLAB_API_URL}/projects/10017/repository/commits`, 
  commit, 
  {
    headers: {
      'PRIVATE-TOKEN': PRIVATE_TOKENS.Snippets
    },
  timeout: 0
  })

脚手架功能

module.exports = class extends Generator {
  constructor(args, opts) {
     ...
  }

  initializing() {
    this.options.template = this._generateRandomString();
  }

  async prompting() {
    this.answers = await this.prompt(...);
  }
  writing() {
    this.spawnCommandSync("git", ["clone", ...]);
    this.fs.copyTpl...
  }
  install() {
    const runDir = this.answers.dir
      ? `${this.destinationPath()}/${this.answers.dir}`
      : this.options.directory;
    remove(`${runDir}/package.json.ejs`, () => {});
    this.yarnInstall(null, {}, { cwd: this.answers.dir });
  }
  end() {
    this.spawnCommand("npm", ["run", "serve:cms"], {
      cwd: this.answers.dir
    }),
  }

后续计划

VSCode插件实现

By pokerone

VSCode插件实现

  • 332