ES6とDOMの件:Typescript編

小さな規模のアプリしか作らないこともあり、フロントエンドに関しては頑なにフレームワークを使わない勢の私は、FastAPI、webpack、wasm(Rust)までに手を出しつつも、未だに素のTypescriptで何とかしようとしている(Typescriptの段階で「素」では無いかも知れないが)。

とくにDOMに関しては以前の記事にあるようなヘルパ関数を使用して、比較的構造的に組み立てられる仕組みを自作し、利用している。下記のように作成・追加したエレメントを編集(属性や子要素を追加する)する関数で、引数の関数の中でさらに要素を追加して属性設定やさらに子要素追加するなど、入れ子構造でコーディング出来る。

/**
 * ① 自身を引数とする関数を引数に取り、処理を行った後、自身を返す
 */
 HTMLElement.prototype.editElement = function(func) {
  func(this);
  return this;
} 
/**
 * ② appendChild(文字列引数の場合はappend)処理を行った後、自身を返す
 */
HTMLElement.prototype.appendChain = function(child) {
  if(child instanceof HTMLElement){
    this.appendChild(child);
  } else if (typeof(child) === "string") {
    this.append(child);
  }
  return this;
}
/**
 * ③ 子要素をクリアしたのち自身を返す
 */
HTMLElement.prototype.clearElement = function() {
  this.innerHTML = "";
  return this;
}
/**
 * ④ 子要素として引数のタグでエレメントを作成。第2引数の関数処理を行ってエレメントを返す
 */
HTMLElement.prototype.appendElement = function(tag, func) {
  const elm = document.createElement(tag);
  if(func) func(elm);
  this.appendChild(elm);
  return this;
}
/**
 * ⑤ 第一引数のタグで単独のエレメントを作成し、第2引数の関数処理を行い作成した要素を返す
 */
Document.prototype.createElementEdit = function(tag, func) {
  const elm = document.createElement(tag);
  if(func) func(elm);
  return elm;
}

  let body = document.querySelector('article').clearElement();
  body.editElement(bdy => {
    bdy.className = 'bg-secondary';
    bdy.appendElement('p', para => {
      para.style.color = 'red';
      para.textContent = 'test string';
    });
  });

<article class="bg-secondary">
  <p style="color: red;">
   test string
  </p>
</article>

これをTypescriptに移植するのは結構面倒だったので、今回のメインネタにしました。究極には全部anyの型にして押し切ってしまえば動くと言えば動くんですが、せっかくならしっかり作ってエディタサポートの恩恵も得られて何ならjavascriptよりも使いやすくしてしまおうということで下記のように移植しました。

/**
 * インターフェース定義:組込みのHTMLElementやDocumentに追加するために declare global する
 */
declare global {
    interface HTMLElement {
        appendElement<K extends keyof HTMLElementTagNameMap>(tag: K, func?: (elem: HTMLElementTagNameMap[K]) => void): HTMLElement
        editElement<T extends HTMLElement>(func: (elem: T) =>  void): T
        appendChain<T extends HTMLElement>(child: HTMLElement | string): T
        clearElement<T extends HTMLElement>(): T
    }
    interface Document {
        createElementEdit<K extends keyof HTMLElementTagNameMap>(tag: K, func: (elem: HTMLElementTagNameMap[K]) => void): HTMLElementTagNameMap[K]
    }
}
/**
 * ① 自身を引数とする関数を引数に取り、処理を行った後、自身を返す
 */
HTMLElement.prototype.editElement = function<T extends HTMLElement>(func: (elem : T) => void) : T {
    console.log(this.constructor.name)
    func(this as T);
    return this as T;
}
/**
 * ② appendChild(文字列引数の場合はappend)処理を行った後、自身を返す
 */
HTMLElement.prototype.appendChain = function<T extends HTMLElement>(child: HTMLElement | string) : T {
    if(child instanceof HTMLElement){
        this.appendChild(child);
    } else if (typeof(child) === "string"){
        this.appendChild(document.createTextNode(child));
    } else {
        throw new TypeError("parameter node: must be string or HTMLElement.");
    }
    return this as T;
}
/**
 * ③ 子要素をクリアしたのち自身を返す
 */
HTMLElement.prototype.clearElement = function<T extends HTMLElement>() : T {
    assertIsInstanceOf(this, HTMLElement);
    this.innerHTML = "";
    return this as T;
}
/**
 * ④ 子要素として引数のタグでエレメントを作成。第2引数の関数処理を行ってエレメントを返す
 */
HTMLElement.prototype.appendElement = function<K extends keyof HTMLElementTagNameMap>(tag: K, func?: (elem: HTMLElementTagNameMap[K]) => void): HTMLElement {
    assertIsInstanceOf(this, HTMLElement);
    const elm : HTMLElementTagNameMap[K] = document.createElement(tag);
    if(func) func(elm);
    this.appendChild(elm);
    return this;
}
/**
 * ⑤ 第一引数のタグで単独のエレメントを作成し、第2引数の関数処理を行い作成した要素を返す
 */
Document.prototype.createElementEdit = function<K extends keyof HTMLElementTagNameMap>(tag: K, func: (elem: HTMLElementTagNameMap[K]) => void): HTMLElementTagNameMap[K] {
    const elm : HTMLElementTagNameMap[K] = document.createElement(tag);
    func(elm);
    return elm;
}

いろいろ試行錯誤した結果、上記のように落ち着きました。Typescirptでメソッド追加するためにはInterfaceを定義する必要があるけど、HTMLElementなど組込みのクラスに対して普通にインターフェースを追加で定義しようとしてもエラーになるようなので、上記のようにdeclare globalで定義する必要があるようです。あとは普通にprototypeで追加していくのだが、これも型定義が割と面倒で、createElement系などは引数のタグ名で戻り値の型が決まる変則ジェネリクスなので、typescriptのパッケージの中身を参考にほぼコピペしました(④、⑤)。

また、①や②は、関数でエレメントを編集したり、子要素を追加して自身を返す関数で、メソッドチェーン用に使用していたのですが、HTMLElementへのインターフェースとして実装している関係でthisが常にHTMLElementになってしまう問題があります。例えば<a></a>のリンクタグを追加しようとしても、createElementしたときはHTMLAncorElementであっても関数内で利用する時はHTMLElementとして扱われるのでthis.hrefメンバーが使えないなど。この場合、一旦unknownにしてHTMLAnchorElementにキャストし直す(element as unknown as HTMLAnchorElement)か、下記のような関数でアサートすることで解決出来ます。

/**
 * val が 指定のクラス cls のインスタンスであることをアサートする関数
 * 例えば HTMLELementにキャストされた下位クラス(HTMLInputELementなど)を、もとのクラスとしてアサート
 */
export function assertIsInstanceOf<T>(val: any, cls: new() => T) : asserts val is T {
    if(!(val instanceof cls)){
        throw new Error(`${val} is not instance of cls`);
    }
}

例)

document.createElement('a').editElement(link => {
   link.href = '#'; // ----  × ここではまだlinkはHTMLElementでhrefメンバがないのでエラー
   assertIsInstanceOf(link, HTMLAnchorElement);
   link.href = '#'; // ----  ○ HTMLAnchorElementとしてアサートされているのでOK
});

とは言ってもイチイチアサートするのも面倒な仕様なので、イマイチ移植できていない感があります。
まぁ、実際使用していると、こういう引数が長くなりがちな関数に対してメソッドチェーンを利用しても読みにくくなってしまうだけなので、これらは使用しないということで封印の方向で進んでいますw
エレメントをサクッと作成するdocument.createElementEdit(tag, func)と、構造的にサクッと子要素を追加するelement.appendElement(tag, fuc)だけあれば全然問題がないという結論に達しつつある感じ。

FastAPI認証機構の追加

小規模djangoページの置き替えの為にFastAPIアプリケーションの勉強と実装実験を行っているが、次に用意したものは管理ページなどへアクセスするためのユーザー認証機構。前述の通りFastAPIはAPIの提供に特化したシンプルなフレームワークなので実用的な機能などは自前で準備する必要がある。かといって何も情報が無いかと言えばそうでも無く、公式のドキュメントやチュートリアルに必要そうなものが一通りまとめてあるので(例えばセキュリティの項目)、それをチマチマ実装していくという実に勉強になる仕組みが用意されている。

ドキュメント・チュートリアルで今回参考(コピペ)にしたのもは主に次の二点

  • セキュリティ
    • パスワード(およびハッシュ化)によるOAuth2、JWTトークンによるBearer
    • ユーザー認証の基本的な構造サンプル
  • SQL database(データベースアクセス)
    • SQLAlchemyを使用してFastAPIのパス構文に接続する方法

以上を組み合わせてユーザー情報をデータベースに保存し、ユーザー認証を行うような仕組みが実現出来る。

ただし、この段階ではAPIとしての実装なので実際に使用できるようにするには、フロントエンドの実装が必要となる。Djangoではバックエンド寄りの実装でHTMLテンプレートを用いてのUIだったが、今回のUIはFastAPI対応ではせっかくなのでシングルページの実装としてjavascript(Typescript)での実装としている。

こんな感じのログイン画面を準備してみました。ユーザー追加の画面や実際の管理画面は未実装だが、同じようなノリで追々追加していきます。

あと、このままではトークンの期限が切れる度に再認証になるので、セッションIDによるリフレッシュトークンの実装で同じ端末からはなるべくログインし続けられるような機構の導入を検討している(実装の手段はともかく見た目くらいはDjangoと同じくらいにしたいw)。

bootstrapをほぼ素で利用しているだけで何となく見た目だけでも良い感じなのが嬉しいですね。

続:ES6とDOMの件

前回はなるべく素のjavascriptでDOMっぽい構文を考えてみようということで、下記のような即時関数を使用して子要素を生成して内容を定義しつつ親要素にアペンドするような形で、文書構造に則したコーディングが出来るのではないかという感じで投稿しました。

((body) => {                            // bodyを引数とした矢印関数①
  body.className='hogehoge';
  body.innerHTML = '';
  body.appendChild(((title) => {        // bodyタグへのappendChildを同様に矢印関数で記述②
    title.innerHtml = "page title";
    return title;
  })(document.createElement('h1')));
  body.appendChild(((content) => {      // 同様に本文ダグも矢印関数で生成と同時にappendChild
    content.innerHtml = "page content";
    return content;
  })(document.createElement('p')));
  return body;                          // ①関数の戻り値(これが無いと結果が空になる)
})(document.querySelector('body'))      // ①関数の引数として body = document.querySelector('body')

結果↓

<body>
<!--
   空のHTML文書
-->
</body>

=>

<body class="hogehoge">
  <h1>page title</h1>
  <p>page content</p>
</body>

慣れてしまえばサクサク書けてしまうが、やっぱり文書構造が大きくなると即時関数の入れ子だらけになって見にくくなってくるのと、即時関数のretrun文を入れ忘れて文書が真っ白になりがちという欠点も見えてくる。

というか、そもそもappendChildなどのメソッドが自身を返してくれればメソッドチェインでもっと便利に使えるのに・・・ということで、あまり大規模にならない程度に下記の拡張メソッドをHTMLElementに追加してみた。

/**
 * ① 自身を引数とする関数を引数に取り、処理を行った後、自身を返す
 */
HTMLElement.prototype.editElement = function(func) {
  func(this);
  return this;
}

/**
 * ② appendChild(文字列引数の場合はappend)処理を行った後、自身を返す
 */
HTMLElement.prototype.appendChain = function(child) {
  if(child instanceof HTMLElement){
    this.appendChild(child);
  } else if (typeof(child) === "string") {
    this.append(child);
  }
  return this;
}

/**
 * ③ 子要素をクリアしたのち自身を返す
 */
HTMLElement.prototype.clearElement = function() {
  this.innerHTML = "";
  return this;
}

これらを定義することで、先の構文は下記の様に書き換えられる(思った以上にスッキリ)

document.querySelector("body")
  .clearElement()
  .editElement(body => body.className = "hogehoge")
  .appendChain(document.createElement("h1").appendChain("page title"))
  .appendChain(document.createElement("p").appendChain("page content"));

良い感じになってきたー。と思ったけど、良く考えたら最近Web製作に関してはTypescriptに移行しつつあり、こんな型の曖昧な仕様を実装するのとても面倒な気がする・・・かといってanyの多用は避けたい。。。ということで次のネタに続く…と。

EC6とDOMの件

EcmaScript6(javascript)の勉強の件で、DOMの記載に関して考えてみた。以前のjavascriptではjQueryなどのライブラリを使ってDOMを簡潔に記載していたが、EC6以降も豊富なライブラリを使う機会が増えているような感じ。

しかし、折角なので、ここはライブラリを使用せずに素のEC6でDOMを分かりやすく記述出来ないものかと考えてみた。通常、素のjavascriptでDOMを記載するときは、例えばBODYタグから下を全てDOMで記述する場合

const body = document.querySelector("body"); // まずBODYタグを見つける
var title = document.createElement("h1");    // タイトル用のH1タグを生成
title.innerHtml = "page title";              // タイトル文字を設定
body.appendChild(title);                     // タイトルをBODYの子要素とする
var content = document.createElement("p");   // 本文段落用タグを生成
content.innerHTML = "page content";          // 本文を設定
body.appendChild(content);                   // 本文をBODYの子要素とする

の様に、既存のタグを見つけるか、新規タグを生成して内容を設定し、親要素に接続する作業を繰り返して文書構造を組み立てていく形になるが、階層が深くなると何が何だか分からなくなってしまう。そのあたりをjQueryでは文書構造に沿った形で記述出来て便利なのだが、”$”関数の数珠つなぎとなりなんだか違う言語のような気がして、個人的には好きにはなれなかった。そこでEC6ではライブラリを一切用いずに次のように記載してみた。

((body) => {                            // bodyを引数とした矢印関数①
  body.className='hogehoge';
  body.appendChild(((title) => {        // bodyタグへのappendChildを同様に矢印関数で記述②
    title.innerHtml = "page title";
    return title;
  })(document.createElement('h1')));
  body.appendChild(((content) => {      // 同様に本文ダグも矢印関数で生成と同時にappendChild
    content.innerHtml = "page content";
    return content;
  })(document.createElement('p')));
  return body;                          // ①関数の戻り値(これが無いと結果が空になる)
})(document.querySelector('body'))      // ①関数の引数として body = document.querySelector('body')

同様の内容を文書構造を意識した記載方法にしてみた。矢印関数を用いてタグを生成(またはquery)し、タグの内容を関数内で記載。子要素も矢印関数内で新たに矢印関数で生成したタグをその場でappendChildする。

かなり冗長な記述になるが、文書構造がHTMLに近い形で多少は見やすくなるのではと考える。

というか、そもそも文書全体をDOM化しようとするからイケないんですけどねw
あと、Lispかっ?!てくらい括弧がメチャメチャ多くなるので、エディタの力を借りないと括弧が一個違うとかでバグ探しも大変なことになりそう。。。vscodeさんありがとう!

IEさようなら

こちらのブログはwordpressを使っているので、まだ大丈夫っぽいですが、ポータルサイトは先月辺りにBootstrap5にアップグレードしたのでいよいよIEでは見られないサイトとなってきました。具体的にBotostrap5のどこがIEでダメなのかはよく調べてないですが、とりあえずjavascriptの記述はmodule式にしたのでIEでは動作はしません。

bootstrap5からjavascriptの形式としてesm(EcmaScriptModule?)が追加されているので、これはモジュールとして使用する必要があります。この場合従来では

<head>
    ・・・・・
    <meta charset="UTF-8">
    <link rel="stylesheet" href="css/bootstrap.css">
    <script type="text/javascript" src="js/bootstrap.bundle.js"></script>
    ・・・・・
</head>

上記のようにヘッダで設定していたものを

<head>
  ・・・・
  <style type="text/css">
    @import '/css/bootstrap.css';
  </style>
  <script type="module">
      import { Alert, Button, Carousel, Collapse, Dropdown, Modal, Offcanvas, Popover, ScrollSpy, Tab, Toast, Tooltip } from "./js/bootstrap.esm.js";
  </script>
  ・・・・
</head>

と、新しい形で書いてあげる必要があります。

まぁ、esmじゃ無い方を使えば従来式で書けるんだけど、まぁ新しいモノ好きということで。。。

Three.jsによる3Dオブジェクト表示

Three.js +Orbitcontrol.js + GLTFLoader.js を使用。モデルはFFXImodelviewer2を使用してmqo形式で取り出してBlenderにて明るさ調整をしつつglbで出力、WEBアプリにて表示している

FF11をやっていた頃の自キャラっぽいモデル。サンプル用だけど一応コピーライト表示しておきますが、不味かったら消します。

少し時間が空いてしまったが、自サイト構築としてdjangoによるポータルの仕組みがある程度出来てきたところで、次はフロント側の勉強をしておりました。

フロント側はjavascriptになるため、ローカル環境で試しながら少しずつ勉強をしてきた。

今回はポータル側のdjangoアプリから埋込みサービスを作成して、こちらのwordpressで表示するテストを行った。

iframeタグにてポータル側のURLを指定してjavascriptのThress.jsライブラリを用いてWEBGL用フォーマットglTFの3Dオブジェクトを表示させている。

wordpressの方をもう少し勉強すれば埋め込みを使用せず、直接javascriptのプラグインを作ってしまえるのだが、それはまたいずれということで、まずはここまで勉強した成果を残すことを優先しました。

いずれ3Dオブジェクトをリアルタイムに操作してサーバー側とのやりとりをしながらのアプリ作成を目論んでこういう仕組みとしてみたということで。。。

このキャラです。同じ帽子を被せたかったのですが、モデルに帽子を被せるとポニーテールが飛び出してしまったので今回は無しで。。。