- Writing JavaScript Views the Hard Way : フレームワークなしで純粋なJavaScriptだけを使ってビューを構築する方法を説明した記事
- 直接的な命令型アプローチによって、パフォーマンス、保守性、移植性を確保
- 状態更新とDOM更新を明確に分離し、それぞれの役割に応じて厳格な命名規則と構造的パターンに従う
- この方式はデバッグしやすく、すべてのブラウザ互換性を保証し、0 dependenciesという大きな利点がある
- 初心者には難しいかもしれないが、学習することで実際のシステムの動作方式に対する深い理解を得られる
JavaScriptビューを「Hard Way」で書く
これは何か?
- この方式は、React、Vue、lit-htmlのようなフレームワークなしにJavaScriptだけでビューを構成するパターン
- 特定のライブラリやツールではなくコーディングパターンそのものであり、スパゲッティコードの問題を防ぐ
- 直接的な命令型方式を使うことで、抽象化を減らし直感性を高める
フレームワークと比べた利点
- パフォーマンス: 命令型コードのため不要な演算なしに動作し、ホットパスとコールドパスの両方に適している
- 0 dependencies: ライブラリアップグレードや互換性問題から自由
- 移植性: 書いたコードはどのフレームワークにも移植可能
- 保守性: 明確なセクション構造と命名規則により、コードの位置を把握しやすい
- ブラウザ対応: IE9以上のほとんどのブラウザと互換性があり、IE6までも一部修正で対応可能
- デバッグ容易性: 中間レイヤーなしで浅いスタックトレースを提供
- 関数型構造: 不変ではないが、すべての構成要素が関数ベースで構成される
構造の説明
全体構造
template → clone() → init() 関数で構成される
init() 関数は、状態変数、DOM参照、更新関数、イベントリスナーなどを含む1つのビューインスタンスを生成する
コード構造の例 (Hello World)
const template = document.createElement('template');
template.innerHTML = `<div>Hello <span id="name">world</span>!</div>`;
function clone() {
return document.importNode(template.content, true);
}
function init() {
let frag = clone();
let nameNode = frag.querySelector('#name');
let name;
function setNameNode(value) {
nameNode.textContent = value;
}
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
function update(data = {}) {
if(data.name) setName(data.name);
return frag;
}
return update;
}
init() 関数の内部構成
1. DOM変数
frag は clone() から生成されたテンプレート断片
- 内部要素は
querySelector() で参照し、変数名は fooNode 形式を使う
2. DOMビュー
- 別のビューを含む部分(再利用可能なサブビュー)
- 例:
let updateChildView = childView();
3. 状態変数
- ビュー内で変更されうるデータ値
- DOM更新を効率化するため、現在値と比較して必要なときだけDOMを変更する
4. DOM更新関数
function setNameNode(value) {
nameNode.textContent = value;
}
5. 状態更新関数
- 状態変更ロジックと、それに伴うDOM反映を含む
- 変更されていない値は無視して不要なDOM変更を防止
- 例:
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
template と clone() 関数
template
<template> 要素で静的なHTML構造を生成
- DOMに直接挿入されず、cloneによって複製を生成する
clone()
document.importNode(template.content, true) で複製
- 必要に応じて
.firstElementChild を使い、ルート要素を返せる
相互作用の方式
親 → 子のデータフロー
- 親は子の
init() を呼び出して更新関数を取得し、update({ name: 'foo' }) 形式で呼び出す
イベントベースのデータ伝播
- 基本的にprops down, events upモデルに従う
- 下位ビューはイベントを上位へディスパッチして通信する
Reactとの比較
constructor() (React) → init() (Hard Way)
render() (React) → update(data) (Hard Way)
this.setState() (React) → setX(value) (Hard Way)
props (React) → update(data) で渡された値 (Hard Way)
- JSX / Virtual DOM (React) → HTMLテンプレート + DOM API (Hard Way)
- 宣言的UIの代わりに、手動のDOM操作とテンプレートを使う
結論
- この方式は、慣れたフレームワークに比べて初期参入障壁は高いが、次のような強みがある:
- パフォーマンス最適化
- 完全な制御権
- 学習による深い理解
- 役割ごとの関数分離と命名規則によって、フレームワークなしでも保守可能なUIを構成できる
互換性
- 最新の例はモダンブラウザ向けAPIを使っているが、IE9以下までも関数ベースの代替によって対応可能
- イベントの代わりにpropsとして関数を渡す方式を使えば、IE6まで拡張可能
3件のコメント
結局はWeb Componentsへ……
おめでとうございます。またひとつ、jsフレームワークが生まれました。
Hacker Newsのコメント
多くのJS開発者にとっては異端かもしれないが、
state変数はアンチパターンだと思うstate変数を追加する代わりに、DOM要素のvalue/textContent/checkedなどを唯一の信頼できる情報源として使うこのアプローチは非常に保守しやすいと記事では説明されているが、同意しない
最近、viteと一緒に純粋な"バニラ" TypeScriptでアプリケーションを書いていて、フロントエンドの"最高"の実践についてますます疑問を持つようになっている
このアプローチは古いbackbone jsライブラリを思い出させる
最近、似たようなものを思いついたが、template要素は使っていない
innerHTMLに入れたり、新しいdiv要素を作ってそこに入れたりしているこのコードは、リアクティブなビューライブラリが置き換えようとしている手動更新コードとまったく同じに見える
20年近くプログラミングをしてきたが、フロントエンドフレームワークにはどうしても慣れない
React.createElementに似たヘルパーを使っている
HTMLベースのツール向けJSツールキットを構築しようとして、deja-vu.junglecoder.comで作業中
大学卒業後の最初の正式な職場で、DelphiソフトウェアのWeb版を作る仕事をしていた