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中获取propslet 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源码