Dev Study
← Rust で小さなブラウザを作る コース

28. 全部つなぐ — main.rs、ローカルサーバー、途中の木を覗くオプション

完成版の は、 かファイルを受け取り、 に HTML を渡し、文字のを表示します。Page::load の中は 5 段で、HTML → (E 章)、DOM の <style> → (F 章)、DOM の <script> → 実行(H 章)、DOM + CSSOM → と座標(G 章)、箱の木 → (G 章)です。URL のときはその前に B 章と C 章が走ります。

手元で動かす手順です。zip を展開するか GitHub の rust-tiny-browser を git して、(78 本)。 -- sample/index.html で同梱のページが描けます。別の端末で sample フォルダに入り 8000 を動かし、cargo run -- http://localhost:8000/index.html とすると、C 章の経路で同じページが取れます(Python の簡易サーバーは HTTP/1.0 と で返します)。cargo run -- http://example.com/ も通ります(こちらは )。

途中の木を覗くオプションを用意してあります。--tokens は D 章の列、--dom は E 章の DOM、--layout は G 章の箱の木(座標つき)、--display-list は表示リスト、--headers は C 章のレスポンス。自分で書いた HTML を --file で渡して、どの段で何が起きるかを追ってください。たとえば <p> の閉じ忘れを書いて --dom、 を書いて --layout、長い文を書いて --display-list、という具合です。

ここから先に足すなら、仕様の節とレッスンの対応表が道しるべになります。文字参照なら HTML Standard 13.2.5.72 以降のを D 章に、結合子つきなら parse_selector_list とマッチングを F 章に、margin / padding なら と layout_block を G 章に、if / for / 配列なら ・・runtime の 3 か所を H 章に。どれも「仕様の該当節を読み、同じ名前で の腕か関数を足す」形です。

本物のブラウザでは: は「JavaScript の実行、DOM、CSS、レイアウト計算を含む重要なことの大半がで起きる」単一スレッドの型で(platform/scheduler/TaskSchedulingInBlink.md)、仕事は 付きのタスクとしてフレームのタスクランナーに積まれ、入力タスクが最優先、背景タブは凍結や間引きがかかります。文書の状態は DocumentLifecycle(core/dom/document_lifecycle.h)で管理され、スタイル → レイアウト → の更新は HTML Standard のの「」で 1 フレームごとに回ります。この回の Page::load() が 1 回だけ通す流れを、本物は仕事が来るたびに必要な段からやり直します。

開発のどこに活かすか: 全体を一度つないだあとに「途中の木を出すオプション」を持っておくのは、業務のパイプライン処理でも効きます。段ごとの出力を見られると、不具合がどの段のものかを切り分ける時間が桁で変わります。また、仕様の節番号をコードのコメントに書いておく習慣は、将来の自分や同僚が「なぜこの分岐があるか」を仕様に戻って確かめられるようにします。

点線の付いた語はタップすると説明が出ます。用語集を開く →

サンプルコード(フレームワーク環境が必要なため表示のみ)

// src/main.rs
//! コマンドラインの入口。
//!
//! ```text
//! tinybrowser http://localhost:8000/index.html      # HTTP で取ってきて描く
//! tinybrowser --file page.html                       # ローカルのファイルを描く
//! 追加オプション: --dom(DOM を表示) --tokens(HTML のトークンを表示) --layout(箱の木を表示)
//!                 --display-list(表示リストを表示) --headers(HTTP のヘッダーを表示)
//! ```

use std::process::ExitCode;

use tinybrowser::browser::Page;
use tinybrowser::html::dom::Node;
use tinybrowser::html::tokenizer::HtmlTokenizer;
use tinybrowser::renderer::layout::dump_layout;
use tinybrowser::renderer::paint::dump_display_list;
use tinybrowser::{http, url};

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let flags: Vec<&str> = args.iter().filter(|a| a.starts_with("--")).map(|s| s.as_str()).collect();
    let positional: Vec<&str> = args.iter().filter(|a| !a.starts_with("--")).map(|s| s.as_str()).collect();

    let html = if flags.contains(&"--file") {
        let Some(path) = positional.first() else {
            eprintln!("使い方: tinybrowser --file <path.html>");
            return ExitCode::from(2);
        };
        match std::fs::read_to_string(path) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("読めません {path}: {e}");
                return ExitCode::from(1);
            }
        }
    } else {
        let Some(target) = positional.first() else {
            eprintln!("使い方: tinybrowser <http://host[:port]/path> | --file <path.html>");
            return ExitCode::from(2);
        };
        let parsed = match url::parse(target) {
            Ok(u) => u,
            Err(e) => {
                eprintln!("URL が不正です: {e}");
                return ExitCode::from(1);
            }
        };
        println!("GET {}  (host={} port={} path={})", parsed, parsed.host, parsed.port, parsed.request_target());
        let response = match http::fetch(&parsed) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("{e}");
                return ExitCode::from(1);
            }
        };
        println!("{} {} {}", response.version, response.status_code, response.reason_phrase);
        if flags.contains(&"--headers") {
            for h in &response.headers {
                println!("  {}: {}", h.name, h.value);
            }
        }
        response.body
    };

    if flags.contains(&"--tokens") {
        println!("── HTML トークン ──");
        for t in HtmlTokenizer::new(&html) {
            println!("{t:?}");
        }
    }

    let mut page = Page::new();
    page.load(&html);

    if flags.contains(&"--dom") {
        println!("── DOM ──");
        if let Some(doc) = &page.document {
            print!("{}", Node::dump(doc));
        }
    }
    if flags.contains(&"--layout") {
        println!("── 箱の木 ──");
        if let Some(root) = &page.layout_root {
            print!("{}", dump_layout(root));
        }
    }
    if flags.contains(&"--display-list") {
        println!("── 表示リスト ──");
        println!("{}", dump_display_list(&page.display_list));
    }
    for line in &page.console {
        println!("[console] {line}");
    }
    for e in &page.script_errors {
        eprintln!("[script error] {e}");
    }
    println!("{}", page.ascii());
    ExitCode::SUCCESS
}

手元で動かす(配布物)

図やコードに出てくる用語(タップで説明)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗