Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16707] ファイルの概要

このコミットは、Go言語の公式ウェブサイトに組み込まれているGo Playgroundのフロントエンドロジックを司るJavaScriptファイル doc/play/playground.js と、その出力表示に関連するスタイルシート doc/style.css の更新を含んでいます。主な変更は playground.js の大規模なリファクタリングであり、Go Playgroundの機能性、特にコードの実行、フォーマット、共有、そして出力の表示方法に影響を与えます。

コミット

このコミットは、Go PlaygroundのJavaScriptコードを大幅に更新し、よりモジュール化された設計と改善されたエラーハンドリングを導入しています。特に、コードの実行と出力表示のメカニズムが抽象化され、HTTPベースとWebSocketベースの2種類の通信トランスポートが導入されました。これにより、Go Playgroundの基盤がより堅牢で拡張性の高いものになっています。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1856286fc23ec4332b7ccc46d9838fbb96fd4ce0

元コミット内容

doc: update playground.js

R=dsymonds
CC=golang-dev
https://golang.org/cl/10933044

このコミットメッセージは簡潔に「playground.jsを更新する」と述べており、具体的な変更内容については触れていません。しかし、コードの変更差分を見ると、単なる軽微な更新ではなく、大規模なリファクタリングと機能強化が行われていることがわかります。R=dsymonds はレビュー担当者を示し、CC=golang-dev はGo開発者メーリングリストに通知されたことを意味します。https://golang.org/cl/10933044 は、Goプロジェクトがコードレビューに利用しているGerritシステムにおける変更リスト(Change-ID)へのリンクです。

変更の背景

このコミットの最も重要な背景は、doc/play/playground.js の冒頭に追加されたコメントに示されています。

// This is a copy of present/js/playground.js from the repository at
// https://code.google.com/p/go.talks
// Please make changes to that repository.

このコメントは、golang/go リポジトリ内の playground.js が、go.talks という別のリポジトリ(Google CodeでホストされていたGo関連のプレゼンテーションやツールのリポジトリ)の present/js/playground.js のコピーであることを明確にしています。

この変更の背景には、以下の目的があったと考えられます。

  1. コードの再利用と一元化: go.talks リポジトリは、Go言語に関するプレゼンテーションやデモンストレーションでGo Playgroundの機能を利用していました。これらの異なる場所で同じようなPlayground機能が必要とされるため、共通のコードベースを持つことで、開発とメンテナンスの効率化を図ったと考えられます。
  2. 機能の改善と標準化: go.talks リポジトリで開発されたPlaygroundの機能(例えば、WebSocketサポートやより洗練された出力処理)を、公式のGoウェブサイトにも取り込むことで、ユーザー体験の向上と機能の標準化を目指した可能性があります。
  3. アーキテクチャの改善: 以前の playground.js は、通信ロジックとUIロジックが密結合していました。このコミットでは、通信部分を「トランスポート」として抽象化し、出力表示を PlaygroundOutput 関数に分離することで、よりモジュール化された、保守しやすいアーキテクチャへの移行を図っています。これにより、将来的な機能追加や異なる通信プロトコルへの対応が容易になります。
  4. エラー表示の強化: コンパイルエラーや実行時エラーの表示を改善し、ユーザーが問題箇所を特定しやすくすることも目的の一つです。

前提知識の解説

このコミットの変更内容を理解するためには、以下の技術的知識が役立ちます。

  • Go Playground: Go言語のコードをブラウザ上で記述、実行、共有できるオンラインツールです。Go言語の公式ウェブサイトに組み込まれており、Go言語の学習やデモンストレーションに広く利用されています。バックエンドでGoコードをコンパイル・実行し、その結果をフロントエンドに返します。
  • JavaScript: ウェブページの動的な挙動を実装するためのプログラミング言語です。Go PlaygroundのフロントエンドはJavaScriptで記述されています。
  • jQuery: JavaScriptライブラリの一つで、DOM操作、イベントハンドリング、アニメーション、Ajax通信などを簡潔に記述できます。この playground.js はjQueryに依存しています。
  • Ajax (Asynchronous JavaScript and XML): ウェブページ全体をリロードすることなく、非同期でサーバーとデータをやり取りする技術です。Go Playgroundでは、コードのコンパイルやフォーマットのためにHTTPベースのAjax通信が利用されます。
  • WebSocket: クライアントとサーバー間で永続的な双方向通信チャネルを確立するためのプロトコルです。HTTPとは異なり、一度接続が確立されると、サーバーとクライアントは互いに自由にデータを送受信できます。リアルタイムな出力表示やインタラクティブな機能に適しています。このコミットでは、WebSocketを介した通信をサポートする SocketTransport が導入されています。
  • HTML5 History API: ブラウザの履歴をJavaScriptで操作するためのAPIです。これにより、URLを変更してもページ全体をリロードせずにコンテンツを更新したり、ブラウザの「戻る」「進む」ボタンの挙動を制御したりできます。Go Playgroundでは、共有されたコードのURLを履歴にプッシュするために利用されています。
  • コードのリファクタリング: ソフトウェアの外部的な振る舞いを変更せずに、内部構造を改善するプロセスです。可読性、保守性、拡張性を向上させることを目的とします。このコミットは、まさに playground.js の大規模なリファクタリングに当たります。
  • モジュール化と抽象化: ソフトウェア設計の原則で、プログラムを独立した小さな部品(モジュール)に分割し、それぞれの部品が特定の機能に責任を持つようにすることです。また、詳細な実装を隠蔽し、インターフェースを通じてのみアクセスできるようにすることを抽象化と呼びます。このコミットでは、通信ロジックをトランスポートとして抽象化し、出力表示ロジックを PlaygroundOutput にモジュール化しています。

技術的詳細

このコミットの技術的詳細は、主に doc/play/playground.js の大幅な変更に集約されます。

  1. トランスポート層の導入:

    • 以前の playground 関数は、直接Ajaxリクエストを送信し、出力の再生ロジックを持っていました。
    • 新しいバージョンでは、HTTPTransportSocketTransport という2つの「トランスポート」が導入されました。これらは、Go Playgroundのバックエンドとの通信方法を抽象化する役割を担います。
    • playground 関数は、初期化時に opts.transport オプションを通じて、使用するトランスポートインスタンスを受け取るようになりました。デフォルトは HTTPTransport です。
    • HTTPTransport:
      • 従来のAjaxベースの通信をカプセル化します。
      • /compile エンドポイントにPOSTリクエストを送信し、Goコードのコンパイルと実行を要求します。
      • サーバーからの応答(data.Events)を受け取り、playback 関数を使って出力イベントを順次再生します。
      • エラーが発生した場合は、error 関数を通じて stderr 出力として表示します。
      • playback 関数は、各出力イベント(e.Message)を output コールバックに渡し、e.Delay に応じて遅延を設けています。
    • SocketTransport:
      • WebSocket (ws:// + window.location.host + /socket) を介したリアルタイム通信をサポートします。
      • onmessage イベントハンドラでサーバーからのメッセージ(JSON形式)を受け取り、output コールバックに渡します。
      • send メソッドを通じて、run (コード実行) や kill (実行停止) などのコマンドをサーバーに送信します。
      • これにより、よりインタラクティブで低遅延なGo Playgroundの体験が可能になります。
  2. 出力表示の標準化 (PlaygroundOutput):

    • PlaygroundOutput 関数は、Go Playgroundの出力エリア (el) にメッセージを表示するための統一されたインターフェースを提供します。
    • この関数は、write オブジェクト(KindBody を持つ)を受け取ります。
    • Kind には 'start', 'stdout', 'stderr', 'end' があり、それぞれ出力の開始、標準出力、標準エラー出力、終了を示します。
    • 'start' の場合は出力エリアをクリアします。
    • 'stdout''stderr' の場合は、メッセージをHTMLエスケープし、適切なCSSクラス(stdout または stderr)を付けて表示します。
    • 'end' の場合は、「Program exited」というメッセージと、オプションで終了ステータスを表示します。
    • 特殊なケースとして、IMAGE: で始まるメッセージはBase64エンコードされた画像データとして解釈され、<img> タグとして出力エリアに挿入されます。
    • \x0c (フォームフィード) 文字は、画面クリアの指示として解釈され、出力エリアの内容をリセットします。
    • 出力エリアの自動スクロールも管理されます。
  3. エラー行のハイライト機能:

    • lineHighlight 関数が導入され、コンパイルエラーメッセージ(例: prog.go:10: ...)から行番号を抽出し、対応するコードエディタの行に lineerror クラスを追加してハイライト表示するようになりました。
    • highlightOutput ラッパー関数は、PlaygroundOutput を呼び出す前に lineHighlight を実行し、エラーメッセージに基づいて行をハイライトします。
    • lineClear 関数は、以前のハイライトをクリアします。
  4. playground 関数のリファクタリング:

    • playground 関数は、UI要素(コードエディタ、出力エリア、実行ボタンなど)の初期化とイベントハンドラの設定に専念するようになりました。
    • コードの実行 (run) やフォーマット (fmt) のロジックは、それぞれ transport.Run やAjaxリクエストに委譲されます。
    • loading() 関数は、実行開始時に出力エリアに「Waiting for remote server...」と表示し、以前のエラーハイライトをクリアします。
    • setError() 関数は、エラーメッセージを表示し、必要に応じて実行中のプロセスを停止させます。
  5. CSSの変更 (doc/style.css):

    • div.play .output .exit セレクタが削除され、代わりに .output .stderr.output .system という新しいセレクタが追加されました。
    • .output .stderr はエラーメッセージを赤みがかった色 (#933) で表示し、.output .system はシステムメッセージ(例: 終了メッセージ)を灰色 (#999) で表示します。これにより、出力の種類に応じた視覚的な区別が可能になりました。

これらの変更により、Go Playgroundのフロントエンドは、よりクリーンなコードベース、改善されたエラー報告、そして将来の機能拡張のための柔軟なアーキテクチャを獲得しました。

コアとなるコードの変更箇所

doc/play/playground.js

このファイルは、全体的に大幅な変更が加えられています。

  • ファイルの冒頭: go.talks リポジトリからのコピーである旨のコメントが追加されました。
  • playground 関数のシグネチャと初期化:
    • opts オブジェクトの構造が変更され、transport オプションが追加されました。
    • var transport = opts['transport'] || new HTTPTransport(); の行で、使用するトランスポートが決定されます。
  • HTTPTransport 関数の新規追加:
    • Ajax (/compile) を使用したコード実行ロジックと、playback 関数がこの中にカプセル化されました。
  • SocketTransport 関数の新規追加:
    • WebSocket (/socket) を使用したリアルタイム通信ロジックが追加されました。
  • PlaygroundOutput 関数の新規追加:
    • 出力表示のロジックがこの関数に集約されました。Kind (start, stdout, stderr, end) に応じた処理、画像出力、画面クリア (\x0c)、HTMLエスケープ、自動スクロールなどが実装されています。
  • lineHighlighthighlightOutputlineClear 関数の新規追加:
    • エラー行のハイライト表示とクリアのロジックが追加されました。
  • run 関数と fmt 関数の変更:
    • これらの関数は、直接Ajaxリクエストを送信する代わりに、transport.Run$.ajax を呼び出すようになりました。
    • 出力処理は highlightOutput(PlaygroundOutput(output[0])) に委譲されます。
  • setError および loading 関数の新規追加:
    • エラー表示とロード中の状態表示を統一的に扱うための関数です。
  • 旧バージョンのコードの削除:
    • 以前の playback 関数、setOutput 関数、および直接的なAjax呼び出しの多くが削除されました。

doc/style.css

  • .output .stderr の追加:
    .output .stderr {
    	color: #933;
    }
    
  • .output .system の追加:
    .output .system {
    	color: #999;
    }
    
  • .output .exit の削除:
    -div.play .output .exit {
    +.output .stderr {
    	color: #933;
    }
    +.output .system {
     	color: #999;
     }
    

コアとなるコードの解説

playground.js におけるトランスポートの抽象化

新しい playground 関数は、opts.transport を介して通信メカニズムを注入できるようになりました。

function playground(opts) {
  var code = $(opts.codeEl);
  var transport = opts['transport'] || new HTTPTransport(); // ここでトランスポートが選択される
  var running;

  // ... (省略) ...

  function run() {
    loading();
    // 実行ロジックはトランスポートに委譲される
    running = transport.Run(body(), highlightOutput(PlaygroundOutput(output[0])));
  }

  // ... (省略) ...
}

HTTPTransport の実装(抜粋)

HTTPTransportは、Ajaxリクエストを介してコードを実行します。

function HTTPTransport() {
	'use strict';

	function playback(output, events) {
		var timeout;
		output({Kind: 'start'}); // 出力開始を通知
		function next() {
			if (events.length === 0) {
				output({Kind: 'end'}); // 出力終了を通知
				return;
			}
			var e = events.shift();
			if (e.Delay === 0) {
				output({Kind: 'stdout', Body: e.Message}); // 標準出力
				next();
				return;
			}
			timeout = setTimeout(function() {
				output({Kind: 'stdout', Body: e.Message});
				next();
			}, e.Delay / 1000000); // 遅延を考慮して出力
		}
		next();
		return {
			Stop: function() {
				clearTimeout(timeout);
			}
		}
	}

	function error(output, msg) {
		output({Kind: 'start'});
		output({Kind: 'stderr', Body: msg}); // 標準エラー出力
		output({Kind: 'end'});
	}

	var seq = 0;
	return {
		Run: function(body, output, options) {
			seq++;
			var cur = seq;
			var playing;
			$.ajax('/compile', { // /compile エンドポイントに Ajax リクエスト
				type: 'POST',
				data: {'version': 2, 'body': body},
				dataType: 'json',
				success: function(data) {
					if (seq != cur) return;
					if (!data) return;
					if (playing != null) playing.Stop();
					if (data.Errors) {
						error(output, data.Errors); // エラー処理
						return;
					}
					playing = playback(output, data.Events); // 成功時は playback で出力
				},
				error: function() {
					error(output, 'Error communicating with remote server.');
				}
			});
			return {
				Kill: function() {
					if (playing != null) playing.Stop();
					output({Kind: 'end', Body: 'killed'});
				}
			};
		}
	};
}

PlaygroundOutput の実装(抜粋)

PlaygroundOutput は、出力の種類に応じてHTML要素を生成し、出力エリアに挿入します。

function PlaygroundOutput(el) {
	'use strict';

	return function(write) {
		if (write.Kind == 'start') {
			el.innerHTML = ''; // 出力開始時はクリア
			return;
		}

		var cl = 'system';
		if (write.Kind == 'stdout' || write.Kind == 'stderr')
			cl = write.Kind; // stdout または stderr クラスを設定

		var m = write.Body;
		if (write.Kind == 'end')
			m = '\nProgram exited' + (m?(': '+m):'.'); // 終了メッセージ

		if (m.indexOf('IMAGE:') === 0) {
			var url = 'data:image/png;base64,' + m.substr(6);
			var img = document.createElement('img');
			img.src = url;
			el.appendChild(img); // 画像表示
			return;
		}

		// ^L clears the screen.
		var s = m.split('\x0c');
		if (s.length > 1) {
			el.innerHTML = ''; // 画面クリア
			m = s.pop();
		}

		m = m.replace(/&/g, '&amp;'); // HTMLエスケープ
		m = m.replace(/</g, '&lt;');
		m = m.replace(/>/g, '&gt;');

		var needScroll = (el.scrollTop + el.offsetHeight) == el.scrollHeight;

		var span = document.createElement('span');
		span.className = cl; // クラスを適用
		span.innerHTML = m;
		el.appendChild(span); // 出力要素を追加

		if (needScroll)
			el.scrollTop = el.scrollHeight - el.offsetHeight; // 自動スクロール
	}
}

style.css の変更

出力の種類に応じたスタイルが定義されました。

.output .stderr {
	color: #933; /* 赤みがかった色 */
}
.output .system {
	color: #999; /* 灰色 */
}

これらの変更により、Go Playgroundのフロントエンドは、より堅牢で、拡張性が高く、ユーザーフレンドリーな出力表示機能を持つようになりました。

関連リンク

参考にした情報源リンク