构建vscode的vue组件代码补全插件以及上传

3,575 阅读6分钟

1.安装工具

安装vscode Generator

npm install -g yo generator-code

2.构建初始项目

yo code

  • 选择New Code Snippets

  • 根据提示完成后续配置填写

  • 完成后自动生成一个snippets初始项目,项目内容如下

  • snippets插件不同于其他插件,此插件关键内容就是一个json文件,内容格式如下

  • 照着葫芦画瓢就行。

"Affix": {

        "prefix": "Affix",

        "body": ["<affix", ":offsetTop =\"offsetTop\"", ":offsetBottom =\"offsetBottom\"", "></affix>"],

        "description": "affix组件配置参数:"

    }

  • 效果:

  • 回车后自动填充代码片段

3.snippet.json自动生成

扩展require方法

  我们需要做的就是把每一个组件的信息拿出来,按snippet的格式输入到snippet.json文件中去,如何从组件库中提取每一个组件对应的props呢,当然不是手工收集这种蠢蠢的方式,程序员的方式当然是用代码工具避免重复劳动。我的想法是写一个工具方法从组件中获取props,然后在node环境中执行,并生成最终的snippet.json文件。我们知道require一个模块时,会返回到export中的对象,这样就能拿到props了。


const component = require("./src/components/alert/index.js");

console.log(component);

  • 执行 node snippetDemo.js,第一个问题出现了

  node对ES6是部分支持的,在node环境中并不支持ES6模块,这个很容易可以找到解决方案,这边用的是babel-register,安装后直接require("babel-register")。再次执行,这次报错不一样了,由于组件是vue单文件组件的形式,node环境中并不能编译通过,因此在template部分报了错。

  平时做web开发的时候都是先使用vue-loader将.vue编译成js, 那有没有一种方式可以在require的时候动态编译将.vue编译成js呢。当然有的,先去深入了解require原理。

require模块的过程:Module._load("a.js") --> var module = new Module(); --> module.load("a.js") --> module._compile()

Module.prototype.require = function(path) {
  return Module._load(path, this);
};
Module._load = function(request, parent, isMain) {

  var filename = Module._resolveFilename(request, parent);
  
  // 判断是否为内置模块
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 生成模块实例入缓存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 加载模块
  try {
    module.load(filename);
  }

  // 输出模块的exports属性
  return module.exports;
};
  • module.load方法如下,加载模块时先确定模块的后缀名,然后执行相应文件的加载方法
Module.prototype.load = function(filename) {
  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};
  • js文件的extension方法定义
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};

  从上面的代码可以看出require一个js文件时,实际上io读取文件后会通过moudle.load的方法加载文件,然后依次执行_extension里挂载的方法,读取文件字符串然后执行_compile。如果在module._compile之前多做一步,将.vue文件解析成js文件,那么就可以实现require的时候动态编译vue文件,实现我需要的功能了。

  • 因此我写了一个工具模块,定义了一个register方法

function register(options) {

  require.extensions[VUE_EXT] = (module, file) => {

    let fileString = fs.readFileSync(file, 'utf8');

    let script = compile(fileString, file);

    console.log(script);

    return module._compile(script, file);

  };

  return true;

}

  • 其中compile部分代码如下

function compile(content, file) {

  let vue = {};

  let selections = ['script', 'template', 'style'];

  var parts = vueCompiler.parseComponent(content, {

    pad: "space"

  });

  for (let section of selections) {

    let tempPart = parts[section];

    let content = getContent(tempPart, path.dirname(file));

    vue[section] = content;

  }

  let result = require('babel-core').transform(vue.script, {

    plugins: ['transform-es2015-modules-commonjs']

  });

  vue.script = result.code + injectTemplate(vue.template);

  return vue.script;

}

function getContent(part, filePath) {

  if(!part){

    return "";

  }

  return part.src ?

    loadSrc(part.src, filePath) :

    part.content

}



function loadSrc(src, filePath) {

  var dir = path.dirname(filePath)

  var srcPath = path.resolve(dir, src);

  try {

    return fs.readFileSync(srcPath, 'utf-8')

  } catch (e) {

    console.log("fail to load");

  }

}

  主要用了vue-template-compiler这个模块,可以将vue单文件中的template,script,style部分分别提取出来。

  • 将template部分注入

function injectTemplate(template) {

  let js = [

    '',

    'var __vue__options__ = (module.exports.__esModule) ?',

    'module.exports.default : module.exports;',

    '__vue__options__.template = ' + JSON.stringify(template) + ';',

    '',

  ];

  return js.join(os.EOL);

}

  • 为解决import问题,先使用babel的transform-es2015-modules-commonjs插件将es6模块转成commonjs模块

let result = require('babel-core').transform(vue.script, {

    plugins: ['transform-es2015-modules-commonjs']

  });

  • 然后将最后的script代码放到module._compile中去执行。

  • 引入将刚写的这个模块试用一下


require("babel-register");

require("vue-register").register();

const component = require("./src/components/affix/index.js");

console.log(component);

  • 已经可以获取到vue组件中的export部分,从中可以提取到props部分。

  到此给require添加钩子实现动态编译vue文件的功能已经完成了,babel-register也是用了这种方式使得require文件时动态使用babel编译。

使用字符串读取

  当我使用写好的工具去require所有的组件时,又出现了别的问题~

  我们的前端组件库某些组件依赖了一些辅助工具函数,有些工具函数使用了window对象,而在node环境中是没有window对象的。到此为止,这条路走不通了,而且这样也获取不到每一个props属性的注释,只能换条路走。

  我想到的是使用fs.readFileSync拿到组件代码字符串,然后匹配props,获取到完整的props字符串,并执行props字符串代码得到props对象。困扰我很久的问题就是匹配到"props:{"开始,那怎么匹配结束的"}",不知道这样的正则怎么写,我最终用了最low的方式,从"props:{"开始遍历,记录"{"和"}"的个数,直到遇到和第一个"{"匹配的"}”。同时顺便获取了这串props字符串中的所有注释,以作为snippets中的description。


//从代码string中获取props

let getProps = (str) => {

        var lIndex = 0,

            RIndex = 0,

            sp = str.split(/props\s*:\s*{/)[1],

            i = 0;

        if (!sp) {

            return {}

        }

        while (lIndex >= RIndex) {

            lIndex += sp[i] === "{" ? 1 : 0;

            RIndex += sp[i] === "}" ? 1 : 0;

            i++;

        }

        var propString = '{' + sp.substring(0, i - 1) + '}';

        return {

            propsData: eval('(' + propString + ')'),

            description: propString.match(/(?:^|\n|\r)\s*\/\/.*(?:\r|\n|$)/g) || []

        }

    }

注意:使用eval('(' + propString + ')')可以强制将括号内的表达式转化为对象,而不是作为语句来执行。

  • 获取到props之后,按snippets.json的格式输出

    //循环读取所有组件的props,输出snippets格式

let readProps = (componentMap) => {

    let snippets = {};

    var ComponentNames = Object.keys(componentMap);

    ComponentNames.forEach(name => {

        var fileString = fs.readFileSync(componentMap[name], {

            encoding: 'utf8'

        });

        var parts = vueCompiler.parseComponent(fileString, {

            pad: "space"

        });

        var tempContent = fileString;

        if (parts && parts.script) {

            tempContent = parts.script.content;

        }

        let props = {};

        try {

            props = getProps(tempContent);

        } catch (err) {

            // console.error(name,err);       

        }

        let propsDescription = props.description ? props.description.join(",").replace(/\/\//g, "") : "";

        let a = [];

        for (let key in props.propsData) {

            if (props.propsData[key].type !== Boolean) {

                a.push(`:${key} ="${key}"`);

            }

        }

        const kebabName = hyphenate(name);

        snippets[name] = {

            prefix: name,

            body: [

                `<${kebabName}`,

                ...a,

                `></${kebabName}>`

            ],

            description: `${kebabName}组件配置参数:${propsDescription}`,

        }

    });

    return snippets;

}

  • 然后将生成的内容写入snippets插件项目中的snippets.json中

//生成文件,并填入之前读取的文件内容

let writeFile = (file) => {

        return new Promise((res, rej) => {

            (async function () {

                await fs.writeFile("plugin/spui-snippets-master/snippets/snippets.json", JSON.stringify(file), (err) => {

                    if (err) rej(err)

                })

                res('success');

            })()

        })

    }

4.发布插件

  最后是插件的上传,关于注册,token的申请等直接参考官方文档https://code.visualstudio.com/docs/extensions/publish-extension。全局安装vsce,然后在插件目录下执行 vsce publish就可以上传插件。我考虑将插件的上传加入插件snippets.json的构建流程中,最终实现的效果是执行node a.js可以一键完成props读取,snippets.json的构建,snippet插件的上传。

  这里使用了node中的child_process模块衍生子进程,使用exec方法完成publish这个子进程操作。 exec接收三个参数:(command[, options][, callback]),command为shell命令,在这边执行发布命令'vsce publish minor -p <我的token>',通过options参数中的cwd设置子进程的当前工作目录,process.cwd()是父进程的当前目录,通过拼接将子进程的工作目录设置到snippet插件目录下。


//发布插件

let publishExtensions = () => {

    return new Promise((res, rej) => {

        var cmdStr = 'vsce publish minor -p <我的token>';

        var cmdOption = {

            cwd: process.cwd() + "/plugin/spui-snippets-master"

        }

        exec(cmdStr, cmdOption, function (err, stdout, stderr) {

            if (err) {

                console.log(err);

            } else {

                res('success');

            }

        });

    })

}

  最终调用系列方法


async function creatSnippets() {

    try {

        let componentsMap = Object.assign(fileDisplay('./src/components'), fileDisplay('./src/b-component'));

        await writeFile(readProps(componentsMap));

        console.log(`Successfully created snippets`);

        await publishExtensions();

        return console.log(`Successfully publish snippets`);

    } catch (err) {

        console.error(err);

    }

}

creatSnippets();

vue-register源码