AWTK 實(shí)時(shí)預(yù)覽插件 (vscode) 工作原理

AWTK 實(shí)時(shí)預(yù)覽插件 (vscode) 工作原理

1. 背景

很早就計(jì)劃寫(xiě)一個(gè) vscode 插件來(lái)預(yù)覽 AWTK 的 UI XML 文件赢笨。遲遲沒(méi)有動(dòng)手茧妒,主要是既不太熟悉 vscode 插件的開(kāi)發(fā)左冬,也沒(méi)有想清楚這個(gè)插件的架構(gòu)。如何達(dá)到期望的目標(biāo)梅忌,同時(shí)保證工作量可以接受呢除破?

直到完成了 AWTK 自動(dòng)測(cè)試框架,發(fā)現(xiàn)以服務(wù)的方式來(lái)實(shí)現(xiàn)界面預(yù)覽是最好的選擇:不但預(yù)覽服務(wù)的工作可以重用現(xiàn)有的代碼瑰枫,而且可以保證插件本身的代碼最小光坝,以后為其它 IDE 增加預(yù)覽的功能也相當(dāng)簡(jiǎn)單。

在開(kāi)發(fā) AWTK 自動(dòng)測(cè)試框架 時(shí)逊谋,我們順帶實(shí)現(xiàn)了 AWTK RESTful HTTP 服務(wù)框架 土铺,這使得現(xiàn)實(shí)一個(gè) Restful HTTP 服務(wù)器超級(jí)簡(jiǎn)單。

效果大概如下圖:

demo.png

2. 工作原理

工作原理是這樣的:在 vscode 中究恤,通過(guò)命令激活 預(yù)覽 后后德,如果當(dāng)前文檔是 AWTK XML UI 文件,就在側(cè)邊打開(kāi)一個(gè) webview理张,webview 讀取文檔的內(nèi)容,并將文檔內(nèi)容通過(guò) HTTP 請(qǐng)求發(fā)往 Preview 服務(wù)悟耘,Preview 服務(wù)在后臺(tái)渲染织狐。然后 webview 在指定的 URL 讀取截圖的 png 文件,并顯示到 webview 中旺嬉。

基本架構(gòu)圖:

vscode-plugin.jpg

3. 插件實(shí)現(xiàn)

3.1 注冊(cè)命令

在插件激活時(shí)厨埋,注冊(cè)一個(gè)命令,它負(fù)責(zé)打開(kāi)預(yù)覽的 webview雨效。

    context.subscriptions.push(
        vscode.commands.registerCommand('awtk.preview', () => {
            if (isAwtkUiFile(vscode.window.activeTextEditor)) {
                UIPreviewPanel.createOrShow(context.extensionUri);
            }
        })
    );

如果預(yù)覽的 webview 已經(jīng)存在則更新它亲善,否則創(chuàng)建一個(gè)新的 webview蛹头。始終在側(cè)邊創(chuàng)建戏溺,需要使用參數(shù) vscode.ViewColumn.Two。

3.2 創(chuàng)建 webview

    public static createOrShow(extensionUri: vscode.Uri) {
        const column = vscode.ViewColumn.Two;

        if (UIPreviewPanel.currentPanel) {
            UIPreviewPanel.currentPanel._panel.reveal(column);
            UIPreviewPanel.currentPanel.update();
            return;
        } else {
            const panel = vscode.window.createwebviewPanel(
                UIPreviewPanel.viewType, 'AWTK UI Previewer', column,
                {
                    enableScripts: true,
                }
            );
            UIPreviewPanel.currentPanel = new UIPreviewPanel(panel, extensionUri);
        }
    }

3.3 webview 的 HTML 文件

這個(gè)很簡(jiǎn)單:一個(gè) image 對(duì)象來(lái)顯示預(yù)覽的效果耕拷,還有一些設(shè)置和提交設(shè)置的按鈕托享。值得一提的是用了一個(gè)隱藏的 input 來(lái)保存 XML 內(nèi)容。

    private _getHtmlForwebview(webview: vscode.webview, source: string, appRoot: string) {
        const scriptPathOnDisk = vscode.Uri.joinPath(this._extensionUri, 'media', 'main.js');
        const scriptUri = webview.aswebviewUri(scriptPathOnDisk);
        const escapeSource = escape(source);
        const nonce = getNonce();

        return `<!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>UI Preview</title>
              <script nonce="${nonce}" src="${scriptUri}"></script>
            </head>
            <body>
            <image id="screenshot"/>
            <table style="width: 100%;">
                <tr><td>Width:</td><td><input type="text" id="width" value="320"></td></tr>
                <tr><td>Height:</td><td><input type="text" id="height" value="480"></td></tr>
                <tr><td>Language:</td><td><input type="text" id="language" value="en"></td></tr>
                <tr><td>Country:</td><td><input type="text" id="country" value="US"></td></tr>
                <tr><td>Theme:</td><td><input type="text" id="theme" value="default"></td></tr
                <tr><td>App Root:</td><td><input type="text" id="app_root" value="${appRoot}"></td></tr>
            </table>
      <input id="source" type="hidden" value="${escape(source)}" >
      <input id="apply" type="submit" value="Apply">
            </body>
            </html>`;
    }

3.4 webview 的 JS 文件

它的主要職責(zé)是在 UI 文件內(nèi)容變化時(shí),向 preview 服務(wù)發(fā)送請(qǐng)求碧查。為了保存發(fā)送不會(huì)過(guò)于頻繁,在上一個(gè)請(qǐng)求完成后才發(fā)起新的請(qǐng)求忠售。

function clientUpdateUI(escapedXml, app_root, width, height, language, country, theme) {
  const xml = unescape(escapedXml);

  const reqJson = {
    xml: xml,
    app_root: app_root,
    width: width,
    height: height,
    language: language,
    country: country,
    theme: theme
  }

  console.log(reqJson);

  try {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xml, "text/xml");
    reqJson.xml = xmlDoc.documentElement.outerHTML;

    if (reqJson.xml.indexOf('parsererror') > 0) {
      console.log("invalid ui xml:", reqJson.xml);
      screenshot.width = width;
      screenshot.height = height;

      screenshot.src = "http://localhost:8000/screenshot?timestamp=" + Date.now();

      return;
    }
  } catch (e) {
    console.log("invalid ui xml", e);
    return;
  }

  const req = JSON.stringify(reqJson, null, '\t');

  function onRequestResult() {
    const screenshot = document.getElementById('screenshot');
    const width = document.getElementById('width').value;
    const height = document.getElementById('height').value;

    screenshot.width = width;
    screenshot.height = height;
    screenshot.onload = function () {
      s_pendingLoad = false;
      console.log("screen shot loaded");
      return;
    }
    s_pendingLoad = true;
    screenshot.src = "http://localhost:8000/screenshot?timestamp=" + Date.now();

    console.log(this.responseText);
  }

  let oReq = new XMLHttpRequest();
  oReq.addEventListener("load", onRequestResult);
  oReq.open("POST", "http://localhost:8000/ui");
  oReq.send(req);

  return;
}

3.5 XML UI 文件更新事件處理

注冊(cè)文檔變化事件卦方,如果是當(dāng)前預(yù)覽的文檔腐螟,向 webview 發(fā)送 updateSource 事件:

    context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(e => {
        let uri = e.document.uri.toString();
        if (UIPreviewPanel.currentPanel) {
            const doc = e.document;
            let source = doc.getText();

            if (UIPreviewPanel.currentPanel.uri == uri) {
                UIPreviewPanel.currentPanel.updateSource(source);
            }
        }
    })
    );

...
    public updateSource(source: string) {
        this._panel.webview.postMessage({ type: 'updateSource', source: source });
    }

webview 里設(shè)置變化的標(biāo)識(shí),在下一次定時(shí)任務(wù)中發(fā)送更新請(qǐng)求衬廷。

  window.addEventListener('message', event => {
    const message = event.data; // The json data that the extension sent
    switch (message.type) {
      case 'updateSource':
        {
          console.log('updateSource:', message);
          const source = document.getElementById('source');
          if (source.value != message.source) {
            source.value = message.source;
            s_pendingUpdate++;
          } else {
            console.log('not changed')
          }
          break;
        }
      default: break;
    }
    });

4. 服務(wù)實(shí)現(xiàn)

在 preview 服務(wù)中汽绢,實(shí)現(xiàn)了兩個(gè)請(qǐng)求:

    1. 用于更新設(shè)置和 XML。
    1. 用于獲取預(yù)覽的效果圖跌宛。
static const http_route_entry_t s_ui_preview_routes[] = {
    {HTTP_POST, "/ui", ui_preview_on_update_ui},
    {HTTP_GET, "/screenshot", ui_preview_on_get_screenshot}};

ret_t ui_preview_start(int port) {
  httpd_t* httpd = httpd_create(port, 1);
  return_value_if_fail(httpd != NULL, RET_BAD_PARAMS);

  httpd_set_routes(httpd, s_ui_preview_routes, ARRAY_SIZE(s_ui_preview_routes));
  httpd->user_data = app_info_create();

  s_httpd = httpd;
  return httpd_start(httpd);
}

詳細(xì)實(shí)現(xiàn)請(qǐng)參考 awtk-previewer

5. 其它問(wèn)題

對(duì)于實(shí)時(shí)預(yù)覽疆拘,比較麻煩的問(wèn)題是:當(dāng)前的 XML 可能是無(wú)效的寂曹,無(wú)效體現(xiàn)在兩個(gè)方面:

  • XML 本身不是 Well Formed 的。這個(gè)我們可以通過(guò) DOMParser 進(jìn)行解析漱挚,如果失敗,并不更新 XML渺氧。
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xml, "text/xml");
    reqJson.xml = xmlDoc.documentElement.outerHTML;

    if (reqJson.xml.indexOf('parsererror') > 0) {
      console.log("invalid ui xml:", reqJson.xml);
      screenshot.width = width;
      screenshot.height = height;

      screenshot.src = "http://localhost:8000/screenshot?timestamp=" + Date.now();

      return;
    }
  • XML 是 Well Formed 的,但是某些值是無(wú)效的白华。比如:

比如輸入 label 時(shí)贩耐,剛輸人 l,這并不是一個(gè)有效的控件名稱鸟赫,它會(huì)觸發(fā) AWTK 的 assert,讓 preview 服務(wù)奔潰台谢。

<l />

為此需要修改 AWTK岁经,去掉 assert,打印一個(gè)警告就好了缀壤,同時(shí)創(chuàng)建一個(gè) view 控件,臨時(shí)代替未知控件筋夏。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末图呢,一起剝皮案震驚了整個(gè)濱河市蛤织,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌指蚜,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绽媒,死亡現(xiàn)場(chǎng)離奇詭異柱宦,居然都是意外死亡播瞳,警方通過(guò)查閱死者的電腦和手機(jī)赢乓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蚓炬,“玉大人躺屁,你說(shuō)我怎么就攤上這事⊙被鳎” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵沪斟,是天一觀的道長(zhǎng)暇矫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)李根,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮冀续,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻蹬。我一直安慰自己凭需,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布顺献。 她就那樣靜靜地躺著枯怖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肿轨。 梳的紋絲不亂的頭發(fā)上蕊程,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音驹暑,去河邊找鬼。 笑死优俘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的臂港。 我是一名探鬼主播视搏,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼浑娜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了筋遭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤编饺,失蹤者是張志新(化名)和其女友劉穎响驴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秽誊,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锅论,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年楣号,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耘纱。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毕荐,死狀恐怖艳馒,靈堂內(nèi)的尸體忽然破棺而出员寇,到底是詐尸還是另有隱情第美,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布扳缕,位于F島的核電站别威,受9級(jí)特大地震影響省古,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豺妓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一琳拭、第九天 我趴在偏房一處隱蔽的房頂上張望训堆。 院中可真熱鬧白嘁,春花似錦、人聲如沸姑躲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至奄抽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逞度,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工俊戳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人燥滑。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓阿逃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親羽历。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淡喜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359