tag:crieit.net,2005:https://crieit.net/tags/WebComponents/feed 「WebComponents」の記事 - Crieit Crieitでタグ「WebComponents」に投稿された最近の記事 2019-12-23T03:48:26+09:00 https://crieit.net/tags/WebComponents/feed tag:crieit.net,2005:PublicArticle/15633 2019-12-22T23:20:13+09:00 2019-12-23T03:48:26+09:00 https://crieit.net/posts/ionic-angular-WebComponents @ionic/angularがWebComponentsでテンプレートチェックを効かせるためにやっていることを調べる <h1 id="AngularでWebComponetsライブラリを使う時の基本"><a href="#Angular%E3%81%A7WebComponets%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E4%BD%BF%E3%81%86%E6%99%82%E3%81%AE%E5%9F%BA%E6%9C%AC">AngularでWebComponetsライブラリを使う時の基本</a></h1> <p>CustomElementsは基本的にはHTMLElementです(正確にはHTMLElementを継承して作ったもの)。<code>customElements.define()</code>することでグローバルに存在するようになるので、なんらかの方法でライブラリをインクルードすれば、あとはtemplateに書いておけば動きます。Angularから見れば、<code>input</code>タグでも<code>custom-input</code>タグでも同じ話なわけです。</p> <pre><code class="html"><input type="text"/> <custom-input type="text"></custom-input> </code></pre> <p>ただし、Angularのテンプレートチェックに<code><input></code>はわかるけど<code><custom-input></code>は知らないやつだなあと怒られるので対策が必要です。</p> <pre><code class="ts">@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], ←templateチェックを無効にする }) export class SomeModule {} </code></pre> <p>これで自由にHTML要素を書いてもよくなりますが、テンプレートの事前チェックが無効化されて実行時の存在判定に後送りされるため、他のタイポなども道連れで検出できなくなります。</p> <pre><code class="html"><typo-input type="text"></typo-input> ←どこにも定義されてないがビルド時にエラーにならない </code></pre> <p>このようにAngularでWebComponetsを使うには<strong>誓約と制約</strong>があります。</p> <h1 id="@ionic/angularがやっていること"><a href="#%40ionic%2Fangular%E3%81%8C%E3%82%84%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%81%93%E3%81%A8">@ionic/angularがやっていること</a></h1> <p>さて、ionicのv4はWebComponentsベースで書き直され、Angularだけでなくreactやvueでもできるようになりましたが、Angularは引き続きテンプレートチェックがビシッと効いています。</p> <p>不思議なので中身を調べてみましょう。</p> <p>なんと、同じ名称のAngularComponentが定義されていました❗️</p> <p>ソースを読むとプロキシという扱いのようなので、これをプロキシコンポーネントと呼びます。<br /> どうやらこいつがビルド時にAngularに差し出されているから、テンプレートの型チェックが効いてるようです。</p> <p><code><ion-input></code>を例に引っ張ってきました。現バージョンだと<code>@ProxyCmp</code>という新デコレータが作られていて、<code>proxyMethods</code>や<code>proxyInputs</code>の記述は省力化されていますが、構造がわかりやすいので旧スタイルで掲載しています。</p> <pre><code class="ts">export declare interface IonInput extends StencilComponents<'IonInput'> {} @Component({ selector: 'ion-input', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: [ 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'spellcheck', 'step', 'type', 'value', ], }) export class IonInput { ionInput!: EventEmitter<CustomEvent>; ionChange!: EventEmitter<CustomEvent>; ionBlur!: EventEmitter<CustomEvent>; ionFocus!: EventEmitter<CustomEvent>; protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef) { c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']); } } proxyMethods(IonInput, ['setFocus', 'getInputElement']); proxyInputs(IonInput, [ 'color', 'mode', 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'spellcheck', 'step', 'size', 'type', 'value', ]); </code></pre> <h2 id="全体構造"><a href="#%E5%85%A8%E4%BD%93%E6%A7%8B%E9%80%A0">全体構造</a></h2> <p>通常のAngularComponentに則ったデコレータとclassです。<code>proxyOutputs</code>、<code>proxyMethods</code>、<code>proxyInputs</code>はionicが用意しているユーティリティ関数です。</p> <h2 id="生のWebComponentsに対してバインドする方法"><a href="#%E7%94%9F%E3%81%AEWebComponents%E3%81%AB%E5%AF%BE%E3%81%97%E3%81%A6%E3%83%90%E3%82%A4%E3%83%B3%E3%83%89%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95">生のWebComponentsに対してバインドする方法</a></h2> <h3 id="接続"><a href="#%E6%8E%A5%E7%B6%9A">接続</a></h3> <p>コンポーネントデコレータのselectorをcustomElementと同じ名称にしています。相当ハックだと思ったのですが、Angularのコンポーネントはカスタム要素としてHTMLにそのまま出力するので、selectorをHTMLに存在している要素名にするとHTMLではHTMLに存在している要素として解釈するわけなんですよね。もちろん<code>_nghost-wke-c2</code>のような属性は付きます。良い子は真似するな案件ですが、customElementに限らず標準HTML要素の<code>input</code>とか<code>span</code>とかでも同じことできます。</p> <h3 id="プロキシ"><a href="#%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7">プロキシ</a></h3> <p>コンポーネントでは、elementRefから引っ張ったnativeElementの生DOM参照をクラスプロパティに保持しています。<br /> proxyInputsとproxyMethodsは、そのクラスプロパティに保持したnativeElementを通して生の<code>ionic-input</code>に対して、HTML属性のget/setやionic内部に用意されているメソッドを生やしています。</p> <pre><code class="ts">export const proxyInputs = (Cmp: any, inputs: string[]) => { const Prototype = Cmp.prototype; inputs.forEach(item => { Object.defineProperty(Prototype, item, { get() { return this.el[item]; }, set(val: any) { this.z.runOutsideAngular(() => (this.el[item] = val)); } }); }); }; export const proxyMethods = (Cmp: any, methods: string[]) => { const Prototype = Cmp.prototype; methods.forEach(methodName => { Prototype[methodName] = function () { const args = arguments; return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args) ); }; }); }; </code></pre> <p>proxyOutputsは、rxjsの<code>fromEvent</code>(中でaddEventListenerやremoveEventListenerしながらObservable化するやつ)を、コンポーネントの各EventEmitter型のプロパティにセットしているものです。コンポーネントclassに同名のクラスプロパティが用意されています。</p> <pre><code class="ts">export const proxyOutputs = (instance: any, el: any, events: string[]) => { events.forEach(eventName => instance[eventName] = fromEvent(el, eventName)); } </code></pre> <h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1> <p>プロキシするコンポーネントを用意することで、グローバルに存在しているWebComponentsライブラリでも、Angularのテンプレートの中でまるでAngularComponentかのように扱えるようになり、テンプレートの型チェックが効くようになっています。</p> studioTeaTwo tag:crieit.net,2005:PublicArticle/14489 2018-07-25T06:25:34+09:00 2018-12-18T09:53:17+09:00 https://crieit.net/posts/Vue-Web-Components VueでWeb Componentsミニゲーム認証サンプル有り <p>Vue.jsでWeb Componentsが作れるようになっているようですね。VueがWeb Componentsっぽい、ということではなく、本物のWeb Componentsを生成できます。試しにちょっとしたミニゲームを含めたサンプルを作ってみましたので実際にどのように作成できるのかを解説してみます。</p> <p>今回はvue-cli3で作成したプロジェクトで実装しています。</p> <h2 id="成果物"><a href="#%E6%88%90%E6%9E%9C%E7%89%A9">成果物</a></h2> <p>下記のようなものを作りました。これは何かというと、よくWebサイトのお問い合わせフォームなどに「私はロボットではありません」のようなチェックを入れないと送信できない機能がありますが、あれです。</p> <p><a href="https://crieit.now.sh/upload_images/0fe6836506e7d710d619551ffa0da9b65b54041cd1697.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0fe6836506e7d710d619551ffa0da9b65b54041cd1697.png?mw=700" alt="web component.png" /></a></p> <p>画像の下にあるのは単なるSubmitボタンで、上部画像はよくある一般的な15パズルです。15パズルを解かないとSubmitボタンが有効にならないようになっています。</p> <p>引き続き解説していきますが、記事の最後に動かせるGitHubページへのリンクがあるので読むのが面倒な方がいれば、僕は全然気にしませんのでそちらを見てしまってください(泣きながら)</p> <h2 id="普通のVue.jsのコンポーネントと何が違うの?"><a href="#%E6%99%AE%E9%80%9A%E3%81%AEVue.js%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A8%E4%BD%95%E3%81%8C%E9%81%95%E3%81%86%E3%81%AE%EF%BC%9F">普通のVue.jsのコンポーネントと何が違うの?</a></h2> <p>普通のVue.jsのコンポーネントであればHTMLは下記のようになっていると思います。</p> <pre><code class="html"><div id="app"> <puzzle-button src="/img.png"></puzzle-button> </div> </code></pre> <p><code>id="app"</code>のdivでVueコンポーネントを動作させる形ですね。</p> <p>これと違って、Web Componentsは自分で独自タグを定義するものなので、このdivが不要になります。</p> <pre><code class="html"><puzzle-button src="/img.png"></puzzle-button> </code></pre> <h2 id="どうやるの?"><a href="#%E3%81%A9%E3%81%86%E3%82%84%E3%82%8B%E3%81%AE%EF%BC%9F">どうやるの?</a></h2> <p>今はもうVueに<code>@vue/web-component-wrapper</code>というものが含まれているため、基本的にはそれを使うだけです。vue-cliを使う前提で作られているようですが、直接webpackでも可能なようです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/vuejs/vue-web-component-wrapper">vuejs/vue-web-component-wrapper: Wrap a Vue component as a web component / custom element.</a></p> <p>通常Vueを起動する際はVueのインスタンスを作成すると思いますがその作業は不要で、代わりに上記でWeb Componentsを生成してそれを登録するだけになります。</p> <pre><code class="javascript">import Vue from 'vue' import wrap from '@vue/web-component-wrapper' import PuzzleButton from './components/PuzzleButton.vue?shadow' window.customElements.define('puzzle-button', wrap(Vue, PuzzleButton)); </code></pre> <p>これだけでpuzzle-buttonタグが使えるようになります。</p> <h2 id="通常のVue.jsによる開発と違う点"><a href="#%E9%80%9A%E5%B8%B8%E3%81%AEVue.js%E3%81%AB%E3%82%88%E3%82%8B%E9%96%8B%E7%99%BA%E3%81%A8%E9%81%95%E3%81%86%E7%82%B9">通常のVue.jsによる開発と違う点</a></h2> <p>いくつかちょっと通常のVue.jsによる開発方法とは違う点があるため、解説してきます。</p> <h3 id="プロパティの型"><a href="#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E5%9E%8B">プロパティの型</a></h3> <p>Vue.jsのコンポーネントの場合、プロパティに型が指定されている場合は下記のようにしてbindする必要があります。</p> <pre><code class="html"><calc :value="100000"></calc> </code></pre> <p>上記のような感じで、valueがNumberの場合はbindして直接数値を指定してあげる必要があります。ただ、Web CompoentsはVue.jsの構文とは関係がないため、コロンを付けてbindすることはできません。普通に属性を指定するだけになります。</p> <pre><code class="html"><calc value="100000"></calc> </code></pre> <p>前述した<code>@vue/web-component-wrapper</code>のREADMEにも書かれていますが、ちゃんとプロパティの定義に型が指定されていれば、コンポーネント内ではその型の値として初期化され使用することができるようになっています。</p> <h3 id="CSSの定義"><a href="#CSS%E3%81%AE%E5%AE%9A%E7%BE%A9">CSSの定義</a></h3> <p>Vue.jsではvueファイル内にてCSSを定義することができます。ところが、Web ComponentsにはShadow DOMという概念があり、これによってWeb ComponentsのDOM内を大元の呼び出し元のDOMと隔離することができるようになっています。これによりscopedの有無に関わらず、vueファイル内で定義したCSSが反映されなくなってしまいます。なぜかというとこのstyleタグが呼び出し元のHTMLのhead内に挿入されるためです。</p> <p>そのため、下記のような設定により、styleタグがShadow DOMの内側に挿入されるように設定して上げる必要があります。</p> <p>(vue.config.jsというファイルを作成)</p> <pre><code class="javascript">module.exports = { chainWebpack: config => { config.module .rule('vue') .use('vue-loader') .loader('vue-loader') .tap(options => { options.shadowMode = true; return options; }); config.module .rule('css') .oneOf('vue') .use('vue-style-loader') .tap(options => { options.shadowMode = true; return options; }); } } </code></pre> <p>serveしてホットロードする時は問題ないのですが、productionビルドの時はちょっとCSSのビルド方法が変わってしまうためエラーになります。そのため、<code>VUE_CLI_CSS_SHADOW_MODE</code>という環境変数を<code>true</code>にしてビルドする必要があります。</p> <p>僕はcross-envを使ったので、同じようにするのであればまずcross-envをインストールします。</p> <pre><code class="sh">yarn add cross-env </code></pre> <p>あとはpackage.jsonのbuildのscriptsにcross-envの記述を追加します。</p> <pre><code class="json"> "scripts": { "serve": "vue-cli-service serve", "build": "cross-env VUE_CLI_CSS_SHADOW_MODE=true vue-cli-service build", "lint": "vue-cli-service lint" }, </code></pre> <p>これでbuildも正常にできるようになります。</p> <h2 id="成果物"><a href="#%E6%88%90%E6%9E%9C%E7%89%A9">成果物</a></h2> <p>ソースはGitHubに公開してあります。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/vue-web-components-sample">vue-web-components-sample - GitHub</a></p> <p>動かせるサンプルもあります。polyfillは入れていませんので、対応しているブラウザで試す必要があります。僕はChromeとかAndroidとかでは確認できました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://dala00.github.io/vue-web-components-sample/">動かせるサンプル</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>Web Componentsは細かいパーツとか機能を使い回すのに非常に適していると思いました。ビルドして配布すれば、誰でも読み込んでタグを設定するだけでその機能を利用することができます。</p> <p>よくWebサイトの制作でjQueryを読み込んで、jQueryのプラグインを読み込んで色々な機能を実装できますが、Web Componentsがどのブラウザでも標準的に動作するようになれば、こういったものはすべてWeb Componentsに置き換えられていく可能性もありそうです。設置も非常に楽ですし、プロパティで自然に設定できるのでわざわざscriptを書かなくてもよく直感的ですし。</p> <p>ちらっと調べたところ他にも色々とWeb Components開発のフレームワーク的なものはあるようですが、Vueは元々使える人が多いと思うのでそういう意味でも選択肢にあがる可能性は高そうな気がします。</p> <p>あとはクローラがどう解析してくれるのか、Vuexのstoreで複数のコンポーネントにて共通のstateを使ったりできるのか(Web Componentsとしてのメリットとは矛盾していそうですが)、色々まだ試してみると面白そうです。</p> だら@Crieit開発者