EC事業部エンジニアの@ku00です。最近は原神の聖遺物厳選に励んでいます。
この記事はECブログリレーの12日目の記事です。11日目はぱーぽーによるグーペの漸進的なフロントエンド改善を振り返るでした。
カラーミーショップにはショップ運営を助ける機能が数多くありますが、その中でも特定のショップ向けの機能(以下「プラチナプラン機能」という)がいくつかあります。この記事ではこれらの機能のうちの一つをAngular Elementsで実装し直した話をします。
Angular Elementsとは
Angular Elementsとは、AngularのコンポーネントをWeb Componentsとしてパッケージ化する機能のことです。Web Componentsはフレームワークに依存せずに新しいHTML要素を定義することができるため、Vue.jsなどの別のフレームワークに組み込むこともできます。Angular ElementsはAngularコンポーネントをCustom Elementsに変換するだけでよいため、Web Componentsというものをあまり意識せずにAngularを書く要領で利用できるメリットがあります。
機能紹介
今回実装し直したプラチナプラン機能である「オーダーメイド機能」について簡単にご説明します。
カラーミーショップには商品ページごとにテキストフォームを設置し、購入者が商品への刻印用の文字などを指定できる「名入れ機能」がありますが、このフォームをテキスト以外のUI(ラジオボタンやセレクトボックスなど)に変更できる機能です。
ショップ側でフォームのUIを設定し、ショップページのテンプレートに以下のようなコードを埋め込むことで利用できました。
<!-- 以下サンプル擬似コード -->
<{if $product.[フォームのラベル名]}>
<{foreach from=$product.[フォームのラベル名] key=key item=val}>
<{if $product.[フォームの種類][$key] == "0"}>
<input type="text" name="product_text[<{$key}>]" value="" />
<{elseif $product.[フォームの種類][$key] == "2"}>
<{foreach from=$product.[フォームの値][$key] item=choice name=loop}>
<label>
<input type="radio" name="product_text[<{$key}>]" value="<{...}>">
<{$choice}>
</label>
<{/foreach}>
<{/if}>
<{/foreach}>
<{/if}>
実際の見た目は次のようになります。
以前の構成
ショップ側の管理画面からフォームのUIを設定とショップテンプレートの編集を行います。そして、ショップページは設定されたフォームのUIとショップテンプレート内容に基づいてフォームをレンダリングしていました。
実装に至った背景
このオーダーメイド機能を全ショップに開放できるようにしてほしいというのが最初の要件でした。この話をいただいた時に最初に考えたのが、今ある既存の機能をそのまま全ショップに向けて提供するのはやめたほうがいいということでした。
まず、オーダーメイド機能はカラーミーショップのあらゆるサービス(ショップページ、ショップ管理画面、ショッピングカート)に密結合なコードとして実装されていました。この機能が実装から数年が経過していた点と、特別な料金プランに依存しており通常契約できるプランと異なり利用者が少ない点から、開発者による保守は最低限で改修などが行われていない状況でした。このため、ドキュメントやテストコードも整備されておらず、機能の仕様や実際の挙動も手動で確認する必要がありました。この問題を放置したまま各サービスをまたがって密結合なコードを修正することはできても、今後もメンテナンスしていくのは困難であると考えました。
また、ショップが実際に操作する機能のUIも実装当初から変わらず古いままだったため、UXの観点から見てもこの機能をこのまま全ショップに開放しても満足に利用してもらえない懸念がありました。
以上の理由から、この要件を一時的な対応としてではなく今後もショップの需要とともに機能を拡張し提供し続けていくことを見据えた新しい技術設計をすべきだと提案したのが始まりです。
Angular Elementsを採用した理由は、Web Componentsを利用すればショップページに任意のHTMLを挿入可能であったことと、カラーミーショップのカートのフロントエンド技術にAngularJS+Angularを利用しておりAngularが私の所属するチームに馴染みのあるフレームワークであったことが決め手でした。Web Compontsを利用してサービスからこの機能をアプリケーションとして分離することができれば、既存コードの影響を抑えつつ最新の環境で開発ができるのではないかと考えたからです。
実装する上での課題
Angular Elementsを利用して実装する上で以下の課題がありました。
- ショップ側でカスタマイズした内容に基づいてフォームを出力したい
- 挿入されたフォームによってショップページが壊れないようにしたい
一点目、単純な静的HTMLを埋め込むのではなく、ショップ側で設定したフォームUIに基づいてフォームを生成・出力する必要がありました。この要件によってカスタマイズ内容をどこかに保存しておく必要が出てきました。以前の実装であればカラーミーショップのDBに保存されており、ショップページをレンダリングする際にそこから参照すれば良かったのでこの点は問題にはなりませんでした。
二点目に、生成・出力されたフォームをショップテンプレートの適切な箇所に挿入しなければショップページが意図した通りに機能しなくなる可能性を考えました。以前の実装ではフォームの挿入はショップ側に委ねられていましたが、Angular Elementsを利用することで機能がアプリケーション単位で分離することになるためフォーム挿入までの作業内容によってはショップによるカスタマイズが現状よりも難しくなることが推測されました。
Angular Elementsを利用した新しい構成
上記課題を解決しつつ提案・実装した新しい構成が以下です。
ショップ側でカスタマイズした内容に基づいてフォームを出力したい
Angularをフロントエンド、Railsをバックエンドとして新しいアプリケーション用の管理画面を用意し、カスタマイズ内容はそこのDBに保存するようにしました。
Angular Elementsを利用してフォームを生成し出力するためには、予めアプリケーションをビルドして一つのJSファイル(以下「フォームJS」という)にしてからカラーミーショップのDBに渡しておく必要がありました。フォームJSのビルドはアプリケーションがデプロイされた時に実施し、ビルドされたフォームJSをDBに保存します。ショップページがレンダリングされる際にフォームJSを読み込むのにはカラーミーショップアプリストアAPIを利用して実現しています。
挿入されたフォームによってショップページが壊れないようにしたい
ショップに設置されている商品フォームの仕様はテンプレートごとにある程度統一されているため、商品フォームの下に挿入されるような仕様にすることでショップ側がショップテンプレートを編集する負担を減らすようにしました。
また、<div id="custom-options-container"></div>
のようなタグをショップテンプレートに埋め込むことでショップ側で任意の箇所にフォームを挿入できる選択肢も用意しました。コードとしては以下のように簡単なものになります。
insertCustomOptions() {
const form = document.querySelector('form[name="product_form"]');
const target = document.querySelector('#custom-options-container');
if (!!target) {
target.appendChild(document.createElement('custom-options'));
} else {
form.insertBefore(
document.createElement('custom-options'),
form.firstChild
);
}
}
その他のメリット
アプリケーションとして分離し、それを別のプラットフォーム(アプリストア)で提供したことで全ショップに開放する際のアナウンスもしやすくなりました。また、これを既存の料金プランに組み込むとなっていた場合は、機能を実装する際に料金プランの判定が必要になるためカラーミーショップ本体と密結合なコードを書かざる得ない状況になっていたかと思います。
まとめ
このようにAngular Elementsを利用することで既存のコードに手を加えることなく、一機能を分離することができる例を紹介しました。既存のコードに依存しないように新しい機能を追加したい場合などにAngular Elements(Web Components)は有効かと思いますので皆さんもぜひ導入してみてはどうでしょうか。