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();
复制代码