Javascript codemirror 高級應用

搭建javascript在線IDE

項目地址

github:https://github.com/sixtrees/j...javascript

這兩天在看阮一峯的《ES6標準入門》,對其中涉及到的代碼示例部分,感受到很不方便,不知道阮老師是如何進行代碼調試的。多是在nodejs環境或者直接在瀏覽器的控制檯中進行調試。我每次都是在nodejs命令行中進行代碼編寫,因爲命令行自己的特色,有一句代碼編寫錯誤,均可能致使須要重寫全部的代碼。html

像下圖中所示的狀況同樣,當咱們由java或者其餘語言的編寫習慣形成的語法錯誤for (var item of set ),致使測試代碼沒能獲得正確的輸出,這時候咱們就須要重頭來過,這體驗固然是很差的,在瀏覽器的控制檯中這種狀況,會好一點。可是,一旦咱們不當心觸碰到了ENTER按鍵,那就悲劇了。
enter description herejava

我也曾異想天開的認爲既然nodejs可使用node app.js來啓動nodejs程序,那爲何不試試用node test.js來進行代碼測試呢。通過測試,發現是能夠的,可是這樣,我每一次都要在命令行和文本編輯器之間進行切換。node

enter description here

因爲,我想簡單點,開發一個基於web的IDE來運行咱們輸入的javascript或者es2015也就是es6代碼。通過一個晚上加一個上午的構思和代碼編寫,已經完成了基礎代碼的編寫。下圖就是這個項目的一個運行示例。利用CodeMirror插件來做爲代碼編輯插件,而且根據須要,改寫了CodeMirror官方的javascript-hint.js文件,使得代碼提示的效果更豐富,同時也發現了網上大多數坑人的帖子帶來的問題,後面都要有詳細的說明。先來簡單的看一下咱們的系統長什麼樣吧,至於界面的設計,你們就將就的看一下,畢竟功能纔是咱們所須要的。mysql

enter description here

如何安裝

目前,我已經將代碼託管至github(點我)。下面,我來演示如何安裝本平臺,請你們放心,不會涉及太多複雜的東西,由於這個系統只是一個很簡單的IDE環境。jquery

先在webstorm下看一下項目結構:
enter description herelinux

下面開始進入安裝階段。
項目採用的是express進行開發的,所以,主要的npm依賴見下面的代碼區。git

"dependencies": {
    "body-parser": "~1.17.1",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.3",
    "express": "~4.15.2",
    "hbs": "~4.0.1",
    "morgan": "~1.8.1",
    "serve-favicon": "~2.4.2"
  }

好了,咱們開始鏈接服務器或者在本身的電腦上打開命令行(我用的msysgit,你們也能夠用cmder
進入到咱們的工做文件夾,我這裏用的是本身的D盤下面的nodejs文件夾,可使用以下命令進行快速進入es6

Administrator@neil-PC MINGW64 /d/nodejs
$ cd
$ cd d:/nodejs
$ pwd
/d/nodejs
$ git clone https://github.com/sixtrees/js-online-running.git

enter description here

克隆完成後,進入js-online-running文件夾。
enter description heregithub

執行npm安裝項目的依賴,關於npm install 如何安裝package.json文件夾下的依賴,請你們自行了解其中原因。

$ npm install
js-online-running@0.0.0 D:\nodejs\js-online-running
+-- body-parser@1.17.2
+-- cookie-parser@1.4.3
+-- debug@2.6.8
+-- express@4.15.3
+-- hbs@4.0.1
+-- morgan@1.8.1
`-- serve-favicon@2.4.3

enter description here

安裝好npm的依賴以後,咱們就可使用 node bin/www來運行項目了。bin/www文件時系統啓動文件,請不要用node app.js來試圖運行本系統。
還有就是不要在bin以前加/,這是多此一舉。執行node bin/www後,node就會啓動localhost:3000做爲項目的部署地址了,端口號能夠在bin/www文件中進行修改。此時,是沒有任何輸出的,若是你是用webstorm打開的,推薦你們使用nodemon等熱部署解決方法。具體的方法,請本身自行上網查找解決方案,反正就是很簡答的那種。像用IDEA來開發java, 我會推薦你們使用JRebel來進行熱部署。這樣的東西是能夠提供編程的效率的。

enter description here

此時,打開瀏覽器,輸入localhost:3000,就能夠看到咱們的頁面了。

enter description here

此時,nodejs控制檯也會輸入全部的資源請求狀況。
enter description here

到這裏,系統的部署就完成了。就能夠用這個平臺來進行js代碼的運行測試了。
總結一下,啓動項目,使用node bin/www,若是想隨電腦啓動,請自行查找如何在windows或者linux平臺下,讓nodejs項目對計算機啓動而啓動。

使用

下面,我使用阮一峯的《es6標準入門》的一個案例代碼來演示如何使用本平臺,其實這個平臺的初衷就是學了配合阮老師的書來學習的。
阮老師的代碼傳送門:在此

var a = [1,2,3,4];
var set = neww Set(a);
for(var item of a) {
  console.log(item);
}

這個代碼的演示程序,以下圖所示

enter description here

本系統的特色

系統採用nodejs+express進行開發的。可是主要的實現都是靠html+javascript來實現的,我當時是想用戶傳遞本身輸入的javascript語句到後臺進行執行,可是根據常識,瀏覽器是能夠直接運行javascript代碼的,所以,就想到了eval這個讓你們又愛又怕的函數來執行咱們輸入的javascript語句,而且捕獲異常,將捕獲的異常信息作爲bootstrap-treeview插件的數據來顯示具備層次結果的錯誤信息。(這裏我可能描述的不夠準備,捕獲的是Error.

總結幾個特色:

  1. 使用最新的的CodeMirrror,包含了全部的樣式和addon。用戶能夠根據本身喜歡來設置本身喜歡的代碼編輯區的配色方案。
  2. 利用CodeMirrorcursorActivity事件來完成代碼提示的觸發。網上一羣坑爹的貨,竟然說用onChange事件,老子的瀏覽器選項卡直接卡死,在仔細看了官方的文檔後,發現cursorActivity事件纔是解決代碼提示的正確道路。
  3. 利用split函數將用戶輸入的全部的代碼按照空格進行分割,獲取全部的英文單詞,將這些單詞傳遞給一個自定義的全局變量CodeMirror.ukeys,結合javascript的關鍵字來實現代碼提示,因此,本系統可以實現實時的代碼提示,並且能夠提示用戶出入的變量名,不只僅是javascript關鍵詞,從而給人更真實的IDE感
  4. 因爲時間較多,沒有作不少的後臺的程序的編寫, 你們有興趣的,能夠mysql或者mongodb來實現相似於HUE的在線IDE。

關於CodeMirror如何實現實時提示

這個問題在原生的CodeMirror有兩個問題,

問題1

一個問題就是代碼提示的時候,在沒有任何輸入的時候就會彈出全部的提示關鍵詞,這一點讓人很討厭,那麼該如何改進呢。因爲咱們這裏使用的javascript-hint.js來進行javascript代碼的提示,那咱們能夠修改的文件就應該是public/codemirror/addon/hint/javascript-hint.js文件,若是你們仔細的閱讀了CodeMirror的代碼,就會發如今javascript-hint.js文件中有一個函數getCompletions,該函數有一個參數叫token,這個token實際上是編譯原理中所說的token,也就是一個語法分割單位,這個token是咱們代碼提示的關鍵,全部的關鍵字就要跟token進行比對,若是發現token是某些關鍵字的前綴,那麼這些單詞都要做爲當前的關鍵字提示給用戶。
例如,下圖中,咱們正在輸入的語法單詞是se,那麼此時觸發的cursorActivity事件調用showHint方法,通過一系列的函數判斷,最重獲取到當前的tokense,則咱們須要將全部以se爲首的單詞進行彈出提示。那麼,針對咱們正在說的這個事情:CodeMirror在咱們沒有任何輸入的時候,也會進行代碼提示,那根據分析沒有任何輸入時,觸發cursorActivity事件時,獲得的token的值是"",咱們只須要在getCompletions函數(這個函數就是獲取和當前用戶正在輸入的語法單詞匹配的全部關鍵詞的函數)的開始判斷token=="",若是是true,則直接返回一個空的list{}。

enter description here
改寫的代碼以下:

function getCompletions(token, context, keywords,options) {

        //這裏是處理沒有任何字母輸入時也會有代碼提示的緣由。
        if (token.string == "") {
            return {list:{}};;
        }
        ...
}

問題2

另一個問題就是CodeMirror能提供的auto-complete(代碼自動完成)是須要進行按鍵綁定的,用戶經過某個按鍵組合才能觸發autocomplete事件,這對於咱們寫代碼的速度確定影響是很大。所以,咱們但願在咱們輸入字符的同時,界面能夠給出咱們實時的代碼提示,而不須要進行按鍵組合來觸發。通常網上的示例都是比較坑爹的,代碼示例以下:

editor.on("change", function () {
     editor.showHint();
});

若是你是這麼用CodeMirror的,或者你正在發愁的時候,我但願個人這的總結可以給你提供比較的幫助。繼續說,上面的代碼示例千萬不要用,由於這樣作了,瀏覽器會卡死的,而形成這種現象 的緣由多是CodeMirror內部處理的問題,我沒有仔細研究,可是通過個人仔細閱讀官方文檔和不斷摸索,我給出一個比較正確的方案來實現代碼的實時提示。

//不要用change
editor.on("cursorActivity", function () {
            //調用顯示提示
            editor.showHint();
});

若是,你是這麼作的或者你恰好這麼改正了,那麼恭喜你,你應該獲得了你要的效果。

好了,這兩個問題,就簡單論述到這裏,下面還有更重要的東西須要我來論述的。

如何增長CodeMirror代碼提示的關鍵字

enter description here

不失通常性,我這裏就描述一下這個項目中,我是如何獲取更多的關鍵字(這裏的關鍵字是指匹配用戶輸入的提示單詞)而且將這樣單詞進行匹配並跟隨原有的關鍵字進行實時提示的。

還記得剛纔看過的那個代碼片斷吧,上面的代碼是我爲了給你們描述如何來觸發代碼實時提示的解決方案的。下面纔是本項目中實際用到的代碼。下面的代碼註釋已經寫的很清楚的,首先就是利用CodeMirrorAPI來獲取用戶輸入代碼(注意是純代碼),而後利用正則來提取出全部的單詞,用match匹配後獲得的是一個數組,而後將該數組傳遞給咱們拓展在CodeMirror全局變量的上一個屬性ukeys。而後再調用editor.showHint()來處理實際的代碼提示。

/**
 * 用來實時對用戶的輸入進行提示
 */
editor.on("cursorActivity", function () {
    //獲取用戶當前的編輯器中的編寫的代碼
    var words = editor.getValue() + "";
    //利用正則取出用戶輸入的全部的英文的字母
    words = words.replace(/[a-z]+[\-|\']+[a-z]+/ig, '').match(/([a-z]+)/ig);
    //將獲取到的用戶的單詞傳入CodeMirror,並在javascript-hint中作匹配
    CodeMirror.ukeys = words;
    //調用顯示提示
    editor.showHint();
});

上面的描述但願沒有給您帶來閱讀上的不暢,下面,我將描述一下如何在javascript-hint.js文件中如何接受咱們傳入的ukeys以及如何利用內置的函數快速完成關鍵字的匹配並將匹配的結果疊加進行關鍵字的提示。

首先是javascript-hint.js中javascript代碼提示的主調函數

若是你對下面的文字描述不感興趣,請移步github(點我),直接看源碼(有註釋).

/**
     * 主調函數,加入了我本身定義的一個CodeMirror.ukeys變量,用來向CodeMirror傳遞用戶
     * 輸入的全部的單詞
     * @param editor
     * @param options
     * @returns {{list, from, to}|*}
     */
    function javascriptHint(editor, options) {

        var ukeys =  CodeMirror.ukeys;//獲取用戶的全部的輸入的單詞
        return scriptHint(editor, javascriptKeywords,ukeys,
            function (e, cur) {
                return e.getTokenAt(cur);
            },
            options);
    };

如上面的代碼所示,在javascriptHint函數中,咱們獲取到CodeMirror.ukeys,並將ukeys傳遞給scriptHint函數。scriptHint函數的主要代碼以下:

//這是處理關鍵字匹配的關鍵函數實現
    function scriptHint(editor, keywords, ukeys,getToken, options) {
        // Find the token at the cursor
        var cur = editor.getCursor(), token = getToken(editor, cur);
        if (/\b(?:string|comment)\b/.test(token.type)) return;
        token.state = CodeMirror.innerMode(editor.getMode(), token.state).state;

        // If it's not a 'word-style' token, ignore the token.
        if (!/^[\w$_]*$/.test(token.string)) {
            token = {
                start: cur.ch, end: cur.ch, string: "", state: token.state,
                type: token.string == "." ? "property" : null
            };
        } else if (token.end > cur.ch) {
            token.end = cur.ch;
            token.string = token.string.slice(0, cur.ch - token.start);
        }

        var tprop = token;
        // If it is a property, find out what it is a property of.
        while (tprop.type == "property") {
            tprop = getToken(editor, Pos(cur.line, tprop.start));
            if (tprop.string != ".") return;
            tprop = getToken(editor, Pos(cur.line, tprop.start));
            if (!context) var context = [];
            context.push(tprop);
        }
        return {
            list: getCompletions(token, context, keywords,ukeys, options),
            from: Pos(cur.line, token.start),
            to: Pos(cur.line, token.end)
        };
    }

從上面的代碼中,能夠看到scriptHint函數主要是獲取list(匹配的關鍵字)。這也印證了上面我在處理沒有任何輸入時,判斷token==""返回的是{list:{}}是正確的寫法。這個函數的改動很小,主要仍是將ukeys繼續向下傳遞 list: getCompletions(token, context, keywords,ukeys, options)。因此的單詞匹配都是在getCompletions函數中實現的,在這個函數中,提供了不少內置函數,咱們只須要添加幾行代碼就能夠完成附加關鍵詞的功能了。該函數的改動後的代碼以下所示:

/**
     *
     * @param token 當前光標下用戶正在輸入的單詞
     * @param context
     * @param keywords 關鍵字列表,本文件內定義
     * @param ukeys 用戶添加的關鍵字
     * @param options
     * @returns {*}
     */
    function getCompletions(token, context, keywords, ukeys,options) {

        //這裏是處理沒有任何字母輸入時也會有代碼提示的緣由。
        if (token.string == "") {
            return {list:{}};
        }
        var found = [], start = token.string, global = options && options.globalScope || window;

        function maybeAdd(str) {
            if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
        }

        function gatherCompletions(obj) {
            if (typeof obj == "string") forEach(stringProps, maybeAdd);
            else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
            else if (obj instanceof Function) forEach(funcProps, maybeAdd);
            forEach(ukeys,maybeAdd);//匹配咱們傳進來的用戶輸入的代碼中的全部的單詞
            forAllProps(obj, maybeAdd)
        }

        if (context && context.length) {
            // If this is a property, see if it belongs to some object we can
            // find in the current environment.
            var obj = context.pop(), base;
            if (obj.type && obj.type.indexOf("variable") === 0) {
                if (options && options.additionalContext)
                    base = options.additionalContext[obj.string];
                if (!options || options.useGlobalScope !== false)
                    base = base || global[obj.string];
            } else if (obj.type == "string") {
                base = "";
            } else if (obj.type == "atom") {
                base = 1;
            } else if (obj.type == "function") {
                if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
                    (typeof global.jQuery == 'function'))
                    base = global.jQuery();
                else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
                    base = global._();
            }
            while (base != null && context.length)
                base = base[context.pop().string];
            if (base != null) gatherCompletions(base);
        } else {
            // If not, just look in the global object and any local scope
            // (reading into JS mode internals to get at the local and global variables)
            for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
            for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
            if (!options || options.useGlobalScope !== false)
                gatherCompletions(global);
            forEach(keywords, maybeAdd);
        }
        //console.log(found);
        return found;
    }

咱們只在gatherCompletions函數中添加了一條語句
好了,若是你這麼作了,就能夠完成咱們輸入的變量也會出如今CodeMirror中了。

附贈的福利

CodeMirror還提供了sql的代碼提示的附加功能。主要針對的有MSSQLmysqlmariadb以及PLSQL。本文以一個實際的項目中的實際使用來演示如何按照上文的描述來修改對應的sql-hint.js中的內容來完成:

  1. 取消沒有輸入任何有效字符時的代碼提示問題
  2. cursorActivity設置代碼實時提示
  3. 增長表的字段到代碼提示中去。

問題1

sql對應的關鍵字信息在CodeMirror/mode/sql/sql.js中,我在項目中用的是相似於oracle的數據庫,所以咱們在sql.js增長了PLSQL的部分關鍵字,以下圖所示(更詳細的信息,請參考個人github

enter description here

而後修改CodeMirror/addon/hint/sql-hint.js中的 CodeMirror.registerHelper("hint", "sql", function (editor, options) {函數中添加以下圖所示的代碼,一樣是判斷token是否=="".
enter description here

問題2及問題3

因爲問題2和問題三都是在cursorActivity中編寫。直接上代碼來描述吧。
獲取全部的表字段
個人項目中,在編寫sql的頁面有全部的字段信息,因此我就用jquery直接獲取了,若是你們的字段不在頁面中,那就用ajax請求在頁面一次加載的時候就保存在頁面的一個全局變量中吧。必定不要讓瀏覽器都去發ajax請求去獲取字段列表,那樣不卡死纔怪。

$(function(){
        console.log("get field");
        $(".field .easytree-title").each(function (){
            var tmp =$(this).html();
            tmp = tmp.substring(0,tmp.indexOf("["));
            window.fields.push(tmp);
        })
    });

上面的代碼獲取了個人項目中的全部的字段,並做爲一個數組存儲在全局變量window.fields上。而後再在cursorActivity中傳遞到sql-hint.js中。
字段傳遞及關鍵字的匹配

editor.on("cursorActivity",function(){
        CodeMirror.ukeys =  window.fields;
        editor.showHint();
    });

sql-hint.js文件的改寫2
CodeMirror.registerHelper("hint", "sql", function (editor, options) {中添加以下圖所示的代碼,就完成了單詞的匹配及後續的匹配到的單詞的顯示。

enter description here

上圖的代碼也就是利用封裝好的函數讀取CodeMirror.ukeys並進行關鍵字匹配,若是匹配成功則加入到result中,最後返回的list就是result.

最後上一個效果圖。全部改動後的文件都在本次示例的項目中。github地址(點我)

enter description here

相關文章
相關標籤/搜索