構建vscode的vue元件程式碼補全外掛以及上傳

花捲愛學習發表於2018-05-04

1.安裝工具

安裝vscode Generator

npm install -g yo generator-code

2.構建初始專案

yo code

構建vscode的vue元件程式碼補全外掛以及上傳

  • 選擇New Code Snippets

  • 根據提示完成後續配置填寫

  • 完成後自動生成一個snippets初始專案,專案內容如下

構建vscode的vue元件程式碼補全外掛以及上傳

  • snippets外掛不同於其他外掛,此外掛關鍵內容就是一個json檔案,內容格式如下

構建vscode的vue元件程式碼補全外掛以及上傳

  • 照著葫蘆畫瓢就行。

"Affix": {

        "prefix": "Affix",

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

        "description": "affix元件配置引數:"

    }

複製程式碼
  • 效果:

構建vscode的vue元件程式碼補全外掛以及上傳

  • 回車後自動填充程式碼片段

構建vscode的vue元件程式碼補全外掛以及上傳

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,第一個問題出現了

構建vscode的vue元件程式碼補全外掛以及上傳

  node對ES6是部分支援的,在node環境中並不支援ES6模組,這個很容易可以找到解決方案,這邊用的是babel-register,安裝後直接require("babel-register")。再次執行,這次報錯不一樣了,由於元件是vue單檔案元件的形式,node環境中並不能編譯通過,因此在template部分報了錯。

構建vscode的vue元件程式碼補全外掛以及上傳

  平時做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);

複製程式碼

構建vscode的vue元件程式碼補全外掛以及上傳

  • 已經可以獲取到vue元件中的export部分,從中可以提取到props部分。

  到此給require新增鉤子實現動態編譯vue檔案的功能已經完成了,babel-register也是用了這種方式使得require檔案時動態使用babel編譯。

使用字串讀取

  當我使用寫好的工具去require所有的元件時,又出現了別的問題~

構建vscode的vue元件程式碼補全外掛以及上傳

  我們的前端元件庫某些元件依賴了一些輔助工具函式,有些工具函式使用了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原始碼

相關文章