Pythonが動くWebサイトをより快適にしてみた

[前回の記事](https://qiita.com/tomtkg/items/79b3166ef0a590be79c8)では，Pythonが動くWebサイトを作りました．
その時は，Pythonを動かすことが最優先だったため，デザインや快適性は微妙でした．

この記事では，Pythonが動くWebサイトをより快適にする方法を紹介します．
htmlファイルの行数は，[32行](https://raw.githubusercontent.com/tomtkg/Python-Web-Test/refs/heads/main/qiita/index7.html) ⇒[67行](https://raw.githubusercontent.com/tomtkg/Python-Web-Test/refs/heads/main/self-study.html) になります．
以下の画像をクリックして，2つのページを比較してみてください．

<a href="https://tomtkg.github.io/Python-Web-Test/qiita/index7.html"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/281e706e-b02f-4327-b182-0f050ee0382a.png" width="700"></a><a href="https://tomtkg.github.io/Python-Web-Test/self-study.html">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/6d9c3a93-87db-4fad-82e2-50aa982fc0b6.png" width="700"></a>

デザインと快適性が改善されていることが確認できるかと思います．

この記事では，前回から変更した部分を中心に，どのようにhtmlを改善したのか紹介します．
記事で扱うhtmlファイルは，以下からも確認できます．

<details><summary>htmlファイル全文</summary>

```self-study.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Python Self Study</title>
    <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"></script>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"></script>
    <style>
        body { margin: auto; width: 1200px; font-size: 25px; }
        #editor { height: 510px; }
        #exe { font-size: inherit; margin-top: 20px; }
        #loading { display: none; margin-left: 10px; }
    </style>
</head>
<body>
    <h2>Python自習室：自由にPythonを実行して遊ぼう</h2>
    <div id="editor"></div>
    <button id="exe" onclick="main()">実行 (Ctrl + Enter)</button>
    <span id="loading">プログラム実行中...</span>
    <pre id="output"></pre>

    <script type="text/javascript">
        const pyodideReady = loadPyodide().then(async pyodide => {
            await pyodide.loadPackage(["matplotlib", "numpy", "pandas", "scikit-learn", "scipy"]);
            let output = document.getElementById("output");
            pyodide.setStdout({ batched: (msg) => { output.innerText += msg + "\n"; } });
            return pyodide
        });

        require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' } });

        require(['vs/editor/editor.main'], async function () {
            const editor = monaco.editor.create(document.getElementById('editor'), {
                value: 'print("Hello World")',
                language: 'python',
                theme: 'vs-dark',
                fontSize: 25,
                wordWrap: 'on',
                lineNumbersMinChars: 3,
            });

            editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
                if (!document.getElementById("exe").disabled) window.main()
            });

            window.main = async function () {
                document.getElementById("exe").disabled = true;
                document.getElementById("loading").style.display = "inline";
                document.getElementById("output").innerText = "";
                document.querySelectorAll("canvas, img.matplotlib").forEach(x => x.remove());

                let pyodide = await pyodideReady;
                let code = editor.getValue();

                try {
                    pyodide.runPython(code)
                } catch (error) {
                    document.getElementById("output").innerText = error
                }

                document.getElementById("loading").style.display = "none";
                document.getElementById("exe").disabled = false;
            };
        });
    </script>
</body>
</html>
```
</details>

# styleの設定
まずは，Webサイトの見た目を良くするため，styleを設定します．
設定するのは，`body`，`button`，`editor`，`loading`の4つです．
ファイルに記述した設定とその設定の日本語説明は，以下の通りです．
```css: style
body { margin: auto; width: 1200px; font-size: 25px; }
button { font-size: inherit; margin-top: 20px; }
#editor { height: 510px; }
#loading { display: none; margin-left: 10px; }
```
* `body`：Webサイト全体について，横幅1200pxの中央表示，文字サイズは25px
* `button`：文字サイズは`body`と同じ大きさで，上の要素と間隔を20px空ける
* `editor`：高さを510px（15行表示できる高さ）にする
* `loading`：最初は非表示の領域にして，左の要素と間隔を10px空ける

高さや横幅，間隔の調整は，styleで設定します．
`body`を設定するだけでも，印象がかなり良くなります．
長い文章を読ませるサイトでは無いので，文字サイズは大きめに設定しています．
styleの設定は，`head`内に書くか，cssファイルに書くことが望ましいです．

# contentの設定
Webサイト内のコンテンツは，以下の5行のプログラム＝5つの要素で構成します．
```html: content
<h2>Python自習室：自由にPythonを実行して遊ぼう</h2>
<div id="editor"></div>
<button id="button" onclick="main()">実行 (Ctrl + Enter)</button>
<span id="loading">プログラム実行中...</span>
<pre id="output"></pre>
```

* `h2`：見出し（ページタイトル）
* `div`：エディター（Pythonプログラムを入力・編集するための領域）
* `button`：Pythonプログラムの実行ボタン
* `span`：「プログラム実行中...」というメッセージを表示・非表示する領域
* `pre`：Pythonプログラムの実行結果を表示する領域

見出しと実行結果を表示する領域には，styleを適用していません．
タグを活用することで，大きな文字や等幅フォントの文字を表示しています．
見出し以外の4要素は，scriptによる操作を実行します．

# scriptの設定
## editor
editorは，Microsoftから提供されている[Monaco Editor](https://github.com/microsoft/monaco-editor)を使います．
このeditorは，[VS Code](https://github.com/microsoft/vscode)とほぼ同じ基本機能を持っています．

ファイルには，以下を記述します．
```html:editor
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"></script>
...
<script type="text/javascript">
    require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' } });

    require(['vs/editor/editor.main'], async function () {
        const editor = monaco.editor.create(document.getElementById('editor'), {
            value: 'print("Hello World")',
            language: 'python',
            theme: 'vs-dark',
            fontSize: 25,
            wordWrap: 'on',
            lineNumbersMinChars: 3,
        });

        editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
            if (!document.getElementById("button").disabled) window.main();
        });

        window.main = async function () {
            ...
            let code = editor.getValue();
            ...
        };
    });
</script>
```

Monaco Editorを読み込んだ上で，そのpathを設定します．
そして，Monaco Editorに対して，以下の6つを設定します．
* `value`：初期値（最初に見本として記述されるプログラム）
* `language`：プログラミング言語
* `theme`：editorのテーマ（黒背景，白背景など）
* `fontSize`：文字の大きさ
* `wordWrap`：editorの右端での折り返し設定
* `lineNumbersMinChars`：プログラム行数の表示桁数

好みの部分もありますが，右端の折り返しと表示桁数3桁は，おすすめの設定です．
また，Monaco Editorを導入するだけで，以下の設定が適用されます．
* `Tab`：スペース4つを挿入
* `Shft + Tab`：スペース4つを削除
* `Ctrl + /`：選択行をコメントアウト
* `Ctrl + H`：文字の検索，置換，削除

他にも，キーワード等の自動着色，行頭を自動で揃えるなどの豊富な機能が提供されています．

自分は便利な機能として，`Ctrl + Enter`でプログラムを実行する機能を追加しています．
実行ボタンが有効な時のみ，`Ctrl + Enter`が押されたら，`main`関数が動作します．
このような機能追加も，Monaco Editorなら簡単に実装できます．
また，`editor.getValue()`で，記述されたプログラムを簡単に取り出すことが出来ます．

`main`関数（=`window.main`）は，Pythonプログラムの実行ボタンが押された時に動作します．

## pythonの設定
pythonの実行には，[Pyodide](https://github.com/pyodide/pyodide)を使います．
ファイルには，以下を記述します．
```html:pyodide
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"></script>
...
<script type="text/javascript">
    const pyodideReady = loadPyodide().then(async pyodide => {
            await pyodide.loadPackage(["matplotlib", "numpy", "pandas", "scikit-learn", "scipy"]);
            let output = document.getElementById("output");
            pyodide.setStdout({ batched: (msg) => { output.innerText += msg + "\n"; } });
            return pyodide
        });
    ...
    window.main = async function () {
        ...
        let pyodide = await pyodideReady;
        let code = editor.getValue();
        
        try {
            pyodide.runPython(code);
        } catch (error) {
            document.getElementById("output").innerText = error;
        }
        ...
    };
</script>
```

Python実行前に，Pyodideを予め読み込む設定にしています．
Pyodideの読み込みには時間がかかるため，この設定は非常に重要です．
Pythonが実行される毎にPyodideを読み込むと，ユーザーに多くの時間を待たせることになります．

必要パッケージと出力先を設定したPyodideを読み込み，`pyodideReady`として保存します．
Python実行時は，用意しておいた`pyodideReady`を利用し，`runPython`で`code`を実行します．
これで，`pyodideReady`準備後は，Pythonプログラムが素早く動くようになります．

## mainの設定
Monaco Editorに記述されたプログラムを取り出す必要があるため，`main`関数（=`window.main`）は`require(['vs/editor/editor.main']`内に配置します．
ファイルには，以下を記述します．
```javascript:main
window.main = async function () {
    document.getElementById("button").disabled = true;
    document.getElementById("loading").style.display = "inline";
    document.getElementById("output").innerText = "";
    document.querySelectorAll("canvas, img.matplotlib").forEach(el => el.remove());

    let pyodide = await pyodideReady;
    let code = editor.getValue();

    try {
        pyodide.runPython(code);
    } catch (error) {
        document.getElementById("output").innerText = error;
    }

    document.getElementById("loading").style.display = "none";
    document.getElementById("button").disabled = false;
};
```
`main`関数が呼ばれたら，最初にプログラムの実行ボタンを無効化します．
これは，プログラムの連続実行を防ぐための設定です．
次に，プログラムが実行中である旨の文章を画面に表示し，ユーザーに安心感を与えます．
そして，Pythonプログラムの実行前に，前回の実行結果（出力文・出力画像）を削除します．

前処理が終わったら，Monaco EditorとPyodideを利用し，Pythonプログラムを実行します．
Pythonプログラムの実行が終わったら，画面に結果を表示します．
画面に表示する内容は，`main`関数の外で設定済みです．
エラーが発生した場合の表示のみ，`main`関数内で設定しています．

`main`関数の最後は，後処理を記述します．
プログラム実行中の表示を消して，無効化していたプログラムの実行ボタンを有効化します．


# 終わりに
Pythonが動くWebサイトをより快適にしました．
styleをしっかり記述するだけで，サイトの印象が大きく変わりますね．
1度だけ実行すれば良い処理と毎回実行するべき処理を分離することは，非常に重要です．
また，ユーザーが連続実行できないような仕組みを導入する事も大切です．
実行中の表示なども含めて，この辺はしっかり検討したいところですね．

この記事では，[Monaco Editor](https://github.com/microsoft/monaco-editor) + [Pyodide](https://github.com/pyodide/pyodide) の組み合わせを利用しました．
Web Editorは種類がたくさんありますし，Pythonの実行方法も[PyScript](https://github.com/pyscript/pyscript)などの選択肢があります．
他の組み合わせでPythonが動くWebサイトを作成するも面白いかもしれないですね．

次回は，Pythonプログラミング問題サイトの作成に取り組みます．

<br>

関連記事・リポジトリ：

https://qiita.com/tomtkg/items/79b3166ef0a590be79c8

https://qiita.com/tomtkg/items/31e04eb75ebc0b61e3ee

https://github.com/tomtkg/Python-Web-Test