Vue コンポーネントをクライアントサイドでテンプレートエンジンとして使う

forum.vuejs.org に回答したやつ。

コンポーネントを描画した結果の HTML 文字列を取得したいときってあるの?とか思われそうなんですが、たまにあるんですよね(ウィンドウポップアップでプレビューを静的なHTMLで表示する必要があったり、v-html で表示する内容の一部を差し替える必要があったり )。

やりかたとしては、以下の手順です。

  1. コンポーネントを登録するオプションオブジェクトを Vue.extend に渡してサブコンストラクタを定義する
  2. サブコンストラクタのインスタンスを生成する(このとき、propspropsData プロパティ経由で指定する。データを変更したい場合はインスタンスのプロパティアクセスで変更する)
  3. $mount メソッドで描画する
  4. $el プロパティで DOM 要素が取得できるので、outerHTML プロパティにアクセスして、HTML 文字列を取得する
const Foo = Vue.extend({
  props: ['name'],
  template: `<p>Hello, {{name}}</p>`
});
const foo = new Foo({
    propsData: {
      name: 'Vue.js'
    }
});
foo.$mount();
const html = foo.$el.outerHTML;
console.log(html);

↑はコンパイラを含む Vue のビルドを使わないとエラーになります。ランタイムオンリービルドを使う場合は、.vue からインポートしたオブジェクトを Vue.extend に渡してください。

SNS を絶つために StayFocusd っていう Chrome 拡張を使い始めた

これ

chrome.google.com

気がついたら、すぐにアドレスバーに「twitter」と入力するので、SNS 中毒を治すために StayFocusd という Chrome 拡張を使い始めた。

指定したドメインの1日あたりのトータル閲覧時間を設定できる。時間を超えたら「仕事してんじゃないの?」っていう煽りページが表示される。正直、イラッとする。
けっこうよくできていて、一度、閲覧が制限された後に、解除しようと制限がリセットされる時間を現在時刻からすぐ後の時間にしようとしても「OK。明日以降はその時間に解除するよ」というメッセージが表示されて防がれる。ぶっちゃけアンインストールすればいいんだけど、「お前さんそこまで自制心ないのか」という心の声が聞こえるので、アンインストールする気持ちが起きない。

なかなかいいツールだな、と思ったんだけど、イケてないところもけっこうある。

  • Chrome 拡張からスクリプトが挿入されるので、それがレンダリングをブロックして、ページが表示されるのが遅くなった
  • サジェストされたドメインamazon.com があって、制限された後に AWS のコンパネが開けなくなってしまった (サブドメを許可リストに追加すればいいんだけど、制限が解除されるまで追加できない)
  • 上と同じで TwitterAPI ドキュメントページが開けなくなってしまった
  • 指定したドメインのトータルの閲覧時間ではなく、ドメイン毎に閲覧時間を設定できるようにしたい
  • 毎日決められた時間に制限をリセットされるのではなく、制限された数時間後に制限を解除する、ような仕様にしてほしい。数時間に一回程度は開きたい

自分で作ってみてもいいんだけど、もし良い Chrome 拡張を知っていたら、教えてください。

13デイズを観た

WOWOW で放送してたので作業しながら観た。

13デイズ [Blu-ray]

13デイズ [Blu-ray]

小学生の頃、金曜ロードショーでやってたのを一度観たんだけど、政治的な駆け引きが当時の自分には難しくてあまり面白く感じなかった。歳を取ってから見ると面白かった。

一番おもしろかったのは、物語の開始直後、航空写真でミサイルが確認され、ケネディ兄弟と主人公のオドネルが初手を迫られるシーンだった。 彼らは、対応策を考えるために、各省の精鋭数名からなる機関を立ち上げる。その判断を下す際のやりとりで以下のような意図や思惑が見えた。

  • 不確実性が高い状況においては、人数を増やせば増やすほど意思決定に時間がかかり、かつ、得られる結論もとんがった部分のない平凡なものでまとまってしまう
  • 大統領の顔色を伺わないように、機関のミーティングには、必要なとき以外、大統領を呼ばない。これは心理的安全の確保に繋がる

その後は、後のケネディ暗殺の黒幕が軍部であることを匂わせるような演出があったり、ケネディ(兄)のカリスマ性に酔って酔って酔いまくってください、というかんじだった(あんまりちゃんと観てなかった)。

WebComponent を Vue コンポーネントツリーの末端として使う

JSフレームワークの末端がWebComponentsになるのか、なれるのか、検証してみた - Qiita の記事を読んで、Vue だとどうなるかな、と思って軽く検証した。自分の手に馴染んているのが Vue というだけの理由で、決して React dis ではないです( React は React で、そのうちいいかんじの仕組みが入るような気がする )。

結論、問題なく使えると思う。

そもそも WebComponent を末端で利用するモチベーションは?

現状、CSS フレームワーク、UI フレームワークがあり、そのフレームワークをラップして View フレームワーク( React, Vue, Angular )のコンポーネントとして提供するライブラリをよく見かける( react-xxx とか vue-xxx みたいなの )。こういったライブラリの組み合わせは、UI フレームワークの数を M, View フレームワークの数を N とすると M * N の組み合わせになる。
各 View フレームワーク向けに 1 から UI フレームワークコンポーネント化して、このようなライブラリを提供するのは効率が悪い。
また、View フレームワークの機能や仕様の差があるため致し方ない部分もあるが、提供されるコンポーネントの I/F が統一感のないものになってしまう。

これらの問題は、再利用可能な単位で WebComponent( Custom Element )を定義すれば、解決するような気がする。View フレームワークから直接利用してもよいし、そのコンポーネントを View フレームワークコンポーネントしてラップしたライブラリを提供する場合でも、以前ほどのコストはかからず、フレームワーク間である程度統一された I/F になる。

ここでの WebComponent は、Python でいう アプリケーション・サーバーとフレームワークの間を取り持つ WSGIRuby でいう Rack のような立ち位置に近い。

Vue で WebComponent を使う

WebComponent で独自の要素を定義して使う。独自の要素といっても、普通の abutton といった要素と同じ I/F なので属性で値を渡して、要素の中で発生したことはイベントを通して知ればよい。

次の様なクリックされたらカスタムイベント my-click を発火する WebComponent ( Custom Element ) を定義しておいて、

import { html, render } from '/node_modules/lit-html/lib/lit-extended.js';

const template = props => html`
  <style>
    :host {
      display: block;
    }
  </style>
  <div>
    <button on-click="${props.onClick}">${props.text}</button>
  </div>
`;

export default class MyButton extends HTMLElement {
  static get observedAttributes() {
    return ['text'];
  }
  constructor() {
    super();
    this._shadowRoot = this.attachShadow({mode: 'open'});
  }
  connectedCallback() {
    this.render();
  }
  attributeChangedCallback() {
    this.render();
  }
  render() {
    const text = this.getAttribute('text');
    render(template({
        text,
        onClick: this.onClick,
    }), this._shadowRoot);
  }
  onClick() {
    this.dispatchEvent(new CustomEvent("my-click", {bubbles: true, composed: true}));
  }
}

window.customElements.define('my-button', MyButton);

Vue のコンポーネントで以下のように使う。

import Vue from '/node_modules/vue/dist/vue.esm.browser.js';

Vue.component('home', {
    data() {
        return {
            value: 0,
        };
    },
    mounted() {
        setInterval(() => {
            this.value += 1;
        }, 1000);
    },
    methods: {
        onClick() {
            console.log('onClick', this.value);
        },
    },
    template: `
        <my-button v-bind:text="value" v-on:my-click="onClick"></my-button>
    `,
});

v-bind ディレクティブで、カウントしている値を text 属性の値としてバインディングして、v-on ディレクティブで my-button コンポーネントから発火される my-click イベントをハンドリングする。

Vue でのイベントハンドラの扱い

v-on ディレクティブは、カスタムイベントを含む DOM イベントをハンドリングすることができる( 細かいけど、Vue コンポーネントの場合は Vue の持っているイベント機構で発火されたイベントもハンドリングできる )。
このディレクティブが肝で、React と違って、イベントハンドラの関数をコンポーネントに渡す必要がない(渡すこともできるが、あえてそうする必要がない)。
なので、WebComponent ( Custom Element ) へ関数を渡すことを考えなくてもよい。

最初に書いたモチベーションの場合、WebComponent を書く人間と、それを使う人間が分かれることがほとんどになるので、
WebComponent は、自身を使おうとしている View フレームワークや、それが関数を渡そうとしていること、あるいは関数を渡すための仕掛けについて、関心を持つべきではないし、コンポーネント自体の再利用性も鑑みると関数を引き渡すことは諦めたほうがいいのではないかと思う。将来的に双方で関数を渡したい・受け取りたいニーズが高まれば別。

WebComponent でのイベントリスナーの扱い

DOM 操作したくないので button 要素のクリックのイベントハンドラの管理も含め lit-extended でだいぶ楽をしたが、extended ではない lit-html ( addEventListener )を使う場合はこんなかんじになる。

import { html, render } from '/node_modules/lit-html/lit-html.js';

const template = props => html`
  <style>
    :host {
      display: block;
    }
  </style>
  <div>
    <button>${props.text}</button>
  </div>
`;

export default class MyButton extends HTMLElement {
  static get observedAttributes() {
    return ['text'];
  }
  constructor() {
    super();
    this._shadowRoot = this.attachShadow({mode: 'open'});
    this._shadowRoot.addEventListener('click', (e) => {
        if (e.target.tagName === 'BUTTON') {
            this.onClick();
        }
    });
  }
  connectedCallback() {
    this.render();
  }
  attributeChangedCallback() {
    this.render();
  }
  render() {
    const text = this.getAttribute('text');
    render(template({
        text,
        onClick: this.onClick,
    }), this._shadowRoot);
  }
  onClick() {
    this.dispatchEvent(new CustomEvent("my-click", {bubbles: true, composed: true}));
  }
}

window.customElements.define('my-button', MyButton);

button 要素にイベントリスナーを登録すると lit-html が要素を差し替えることを考慮して(この例では考えなくていいかもだが)、適切なフックで都度 addEventListener, removeEventListener を呼ぶ必要がある。そこで、shadowRoot に対してイベントリスナーを登録し、イベントを発生された要素に応じて、カスタムイベントを発火させるようにする。
shadowRoot に登録するイベントリスナーが分岐だらけになりそうな気もするのだけど、コンポーネントの責務さえしっかり決まっていればさほど問題にならないような気がする。コンポーネントの状態管理が複雑になるのは、また別の問題。

正直、この例だったら lit-html を使わず DOM API でも実装できるが、実際にコンポーネントを作ろうとしたら、もう少し複雑なものになって DOM API ではつらくなり lit-html を使うことになると思うので試した。

色々書いたけれども、Vue では WebComponent が普通に使えると思うので、今のうちから各種 CSS フレームワーク、UI フレームワークの WebComponent( Custom Element )を定義したライブラリを作っておけば、後で重宝されるかもしれないですね。

マネー・ショート を観た

観た。

サブプライム住宅ローン危機の発生を予測し、事前に空売り(実際は空売りではないんだけど)することで巨額の利益をあげた男たちの物語。実際のモデルがいる。

邦題の「華麗なる大逆転」や黄色いパッケージから、アウトロー達がウォール街のエリートたちに一泡吹かせる痛快ストーリーかと思ったが、全くそんなことはない。 そこにあるのは、アウトローたちの心の葛藤である。

彼らの全てが喜々としてトレードしたわけではない。世界経済の崩壊が起こることを確信し、このような事態を招いた連中への怒り、絶望といった負の感情を噛み締めながらトレードするのである。

物語の終盤では、自分たちの成功が、多数の人々が職・資産を失い露頭に迷う先にあることを自覚し、良心の呵責に苛まれる。また、危機がはじまり、いざ売り抜こうとする際に「ここで売って巨額の利益を上げたら、自分たちが一番憎んでいた連中と同じになるのではないか」と苦悩する。

スティーヴ・カレルクリスチャン・ベールブラッド・ピットの徹底的な役作り、巧みな演技がこれらの心の葛藤を見事に描いている。特にスティーヴ・カレルの演技が表情・声のひとつひとつの変化のレベルで良かった。また機会があったら見たい。

デヴィッド・リンチ:アートライフ を観た

観た。

映画『デヴィッド・リンチ:アートライフ』公式サイト

不気味で、得体の知れないデヴィッド・リンチの発想はどこから生まれるのか。彼の生い立ちや絵の創作活動を通して探る作品。

彼が語る過去のエピソードに合わせて、彼の版画作品が映される。幼い頃の恐怖体験、住んでいた住居、フィラデルフィアの工業地帯、彼の創造力の源泉が過去にあることが分かる。ひとつひとつの作品に過去の体験に基づく物語があるのだ。

数日前に、ヒカリエでやっていたデヴィッド・リンチの版画展に行ってきたのだけど、正直、作品からそういった背景を掴み取ることがまったくできなかった。この映画を観た後に行けば、彼の過去の物語を思い浮かべながら作品を鑑賞できるかもしれない。

フィラデルフィアの風景の映像やそのときの暮らしぶりの話は、イレイザーヘッドを観たことがある人にとって、たまらないものに違いない。廃墟、差別主義、モノクロ、子供が生まれたばかりの夫婦の生活。あの不気味な映画は、その頃のリンチの心象を映像にしたものだ、ということがしっくりくる。

Vue.js で特定のプロパティを変更の追跡の対象外にする

きっかけ

とある案件で、とあるサードパーティSDK を利用していた。その SDK が提供するコンストラクタから生成したインスタンスを Vue.js のインスタンスのデータとして扱おうとしたら、Security Error のような例外が投げられて困った。 (回避するために、インスタンスのデータとして扱わないように試みたのだけど、致し方ない理由で諦めた)

原因

Vue.js では、データの変更を追跡するためにプロパティを getter/setter に上書きする。ココらへんの話は リアクティブの探求 — Vue.js が詳しい。この上書きの処理はプロパティがオブジェクトの場合は掘り下げて再帰的に行われる。おそらく問題の起きたインスタンスでは、セキュリティの観点からこういった処理を防ぐ機構が入っていたのだと推測。すごいね。

解決

これまた公式ドキュメントなのだけど、Object.freeze で追跡の対象外にできることを知ったので以下のようなコードを書いた( Vue インスタンス — Vue.js )。

const foo = new ThirdpartyFoo();

const container = {
  foo,
};

const vm = new Vue({
  data() {
    return {
      container: Object.freeze(contianer),
    },
  },
});

vm.container.foo;

はじめ foo 自体を Object.freeze したのだけど、foo のメソッド呼び出しに伴うプロパティの変更ができなくなってしまった。そこで、容れ物のオブジェクトを用意して、元のオブジェクトを包むことにした。こうすることで、foo のプロパティの getter/setter への上書きは防ぎつつ、これまで通り foo のメソッドを呼び出すことができる。

これまで特に意識せず、様々なオブジェクトを Vue のインスタンスのデータとして扱っていたのだけど、変更を追跡する必要がない、オブジェクトのプロパティの階層が深い( getter/setter への上書きのコストが高い )オブジェクトは Object.freeze で追跡の対象外にしてよいかもしれない。