基盤チームの右京です。
BASE ではショップのデザインを誰でも簡単にできるような、いわゆるノーコードな機能を提供しています。
この記事では、そんなノーコードなシステムの裏側について、簡単にですが解説しています。
ショップページ配信の基盤システム
ノーコードの前にまず BASE のショップページ(ShopFront と呼んでいます)がどのようなシステムかを知っておく必要があります。BASE のショップページは特定の URL にアクセスすると必要なデータをデータベースから取得し、テンプレートエンジンを使ってサーバーのプログラムで HTML を生成して返す、よくある伝統的な Web ページとして実装されています。ただし、ショップ毎にページのデザインは全く異なるため、1 つの固定のテンプレートを利用しているわけではなく、ショップ毎に動的に切り替えるような仕組みになっています。
このショップ毎のテンプレートを作る手段の 1 つが今回紹介するノーコードでのデザイン編集機能になります。ノーコードの他にも HTML に近い形式でテンプレートが記述できる仕組みもあり、現在ではそのどちらかを選択できるような形になっています。いずれかの方法で生成された何かしらのデータは「変換エンジン」を通して、ShopFront で実行可能なテンプレートとブラウザから呼び出されるアセット類に変換されます。
複数の形式から実行可能なテンプレートを生成する「変換エンジン」
「変換エンジン」はショップの管理画面と ShopFront をつなぐ中核的な存在で、例えば BASE Template として記述されたショップページのテンプレートを ShopFront が実行可能な形式へ変換します。ShopFront 自体はここまでにあったようにテンプレート自体は持っておらず、テンプレートのレンダリングの際に使用される変数の生成のみを行っています。この変数は JSON で表すと以下のようになっており、URL 毎に共通部分以外の内容が異なります。
{ // この URL で表示しているページ種別の識別子 "page": "item", // どの URL でも使用できる、ショップの基本的な情報 "shop": { "name": "テストショップ", "logo": "https://...", "description": "テストで作成したショップです。" }, // 商品ページの URL で使用できる、その URL の示す商品の詳細な情報 "item": { "title": "テスト商品", "image": "https://...", "stock": 10, "price": 500 } , // ... 他にも多くの変数が存在する }
ShopFront で使用できるテンプレートエンジンは Twig に似た形式で、社内では Cot と呼んでいます。一方で BASE Template は独自の記法を採用しているため、ShopFront で実行するには Cot の形式へ変換する必要があります。そこで、次のような変換行うのがこの変換エンジンです。
BASE Template | 変換後(Cot) |
---|---|
<html> {LogoTag} {block:ItemPage} <h2>{ItemTitle}</h2> <img src="{ItemImage0URL-origin}"> <p>¥ {ItemPrice}</p> <p>残り {ItemStock} 個<p> {/block:ItemPage} {block:AboutPage} <p>{ShopIntroduction}</p> {/block:AboutPage} </html> |
<html> <img src="{{ shop.logo }}"> {% if page == 'item' %} <h2>{{ item.title }}</h2> <img src="{{ item.image }}"> <p>¥ {{ item.price }}</p> <p>残り {{ shop.stock }} 個<p> {% endif %} {% if page == 'about' %} <p>{{ shop.description }}</p> {% endif %} </html> |
これによって BASE Template という形式から、ShopFront で実行可能な形式へ変化しました。これをストレージに保存し、ショップを表示する際に ShopFront がテンプレートして利用することでショップ毎のデザインの切り替えが実現されています。ようするに、とにかく Cot という形式にさえしてしまえば、元のデータはなんでもよいような設計になっています。ノーコードも変換エンジンを利用する実装の 1 つで、BASE Template とは全く異なるデータをもとにして Cot を生成しています。1
ノーコードでのデザイン編集
コンセプトと概念
「デザインをかんたんに、もっと自由に。」というコンセプトで開発されたデザイン編集機能は、「パーツ」と呼ばれる小さな部品をショップページに配置することで、ノーコードで自由な構成のショップページを作成できます。これはページ全体を細かく GUI から変更できるような機能ではなく、ある程度デザイン済みのページに対して必要な要素を追加、組み合わせていくものです。
これを設計していく上で「全体を大きく編集可能な部分とそうではない部分に分離し、可能な部分に対して任意の子要素を追加できる」を最も基本的な考え方としています。これは右の図のような概念になっていて、赤い枠で囲われている編集可能な部分を「コンテナ」、そこへ追加される黄色い要素を「パーツ」と呼んでいます。
「パーツ」はそれぞれが独立してデザインされており、例えばリストのカラム数や画像の回り込みのようなスタイルを個別に変更することも可能です。「コンテナ」の配置パターンと灰色の編集不可能エリアのデザインを複数用意し、それにショップに合わせた必要なパーツを乗せることで様々なデザインのショップを実現しています。
レイアウト用テンプレートと複数のパーツ用テンプレートを組み合わせて表現
これの実現方法として 2 種類のテンプレートを用意して、その組み合わせを Web 上で編集したものを(ここがノーコードな部分)、変換エンジンで 1 つのテンプレートとしてまとめています。1 つは大枠のデザインとコンテナを定義する「レイアウト」、もう 1 つは「パーツ」でそれぞれは次のようなコードになっています。
レイアウトは HTML 全体と、編集可能な箇所とする「コンテナ」を独自で定義した <cot-container>
というタグを使って表現しています。この <cot-container>
の子要素として様々なパーツが追加されていきます。
<body> <h1><img src="{{ shop.logo }}" alt="{{ shop.name }}"></h1> <div> <div class="main"> <cot-container name="main" /> </div> </div> </body>
パーツはメタ情報が含まれているため少し長めになっていますが、商品パーツの例です。Vue.js の SFC と似たような構造になっています。
props
はこのパーツに渡すことができる変数で React や Vue.js でいう props と同等のものとイメージしてください。この例では layout
に row
を設定することで、縦方向と横方向でレイアウトを切り替えられるような実装をしています。
<script name="metadata"> { name: "item", props: { layout: { ui: 'select' } } } </script> <template> <div class="container" data-layout="[[layout]]"> <div class="head"> <h2>{{ item.title }}</h2> <img src="{{ item.image }}"> </div> <div class="body"> <p>¥ {{ item.price }}</p> <p>残り {{ shop.stock }} 個<p> </div> </div> </template> <style> .container { display: flex; } .container[data-layout="row"] { flex-direction: column; } </style> <script> function main ($el) { $el.addEventListener(...) } </script>
これらを GUI 上でどのように組み合わせているかについては後ほど記載するのですが、最終的には次のような JSON として管理しています。デザイン編集という機能は、この JSON のエディターだと考えると今後がイメージしやすくなります。
{ "containers": { "main": [{ "name": "image", "props": { "src": "https://....", "fit": "cover" } }, { "name": "item", "props": { "layout": "column" } }] } }
そして、この JSON を変換エンジンに渡すことで ShopFront で利用可能な Cot を生成しています。<cot-container>
のあった場所に、props
を展開したパーツのテンプレート部分が追加されています。
コードで見る
<style> .container { display: flex; } .container[data-layout="row"] { flex-direction: column; } </style> <body> <h1><img src="{{ shop.logo }}" alt="{{ shop.name }}"></h1> <div> <div class="main"> <div name="main-container"> <!-- 画像パーツ --> <div id="zzzz" data-fit="cover"> <img src="https://...."> </div> <!-- 商品パーツ --> <div id="xxxx" class="container" data-layout="column"> <div class="head"> <h2>{{ item.title }}</h2> <img src="{{ item.image }}"> </div> <div class="body"> <p>¥ {{ item.price }}</p> <p>残り {{ shop.stock }} 個<p> </div> </div> </div> </div> </div> <script> (function ($el) { $el.addEventListener(...) })(document.querySelector('#xxxx')) </script> </body>
このテンプレートを ShopFront で変数を用いてレンダリングすると、デザイン編集の GUI で作ったデザインが反映される、という仕組みです。
コア部分以外のデザインやデータは変換時点で静的に解決する
1 つに結合されたテンプレートには、デザインに関する CSS と必要な JS の殆どが埋め込まれているような形になります。これは ShopFront 単体では変数は動的な必要があるものに絞る、特にデザインに関する変数をほとんど持たせないような設計を取っているためです。デザインに関する部分は変換エンジンの時点でほとんどが静的な HTML となるため、ShopFront の仕様を小さく保つことにも一役買っています。
例えば「リアル店舗の地図を表示するパーツ」を作りたい場合は、地図のウィジェット埋め込みまでをパーツとして完結させることで ShopFront に変数を追加することなく機能を増やしていく、という方針を取っています。
JSON エディターとしてのデザイン編集
先程、ノーコードでのデザイン編集はようするに JSON エディターだと説明しました。BASE のフロントエンドは現在主に Vue.js で実装されており、デザイン編集も例外ではありません。では、レイアウトやパーツといった独自の形式のものを Vue.js 上でどのように扱っているかといいますと、それぞれを Web Components (!) へ変換し、Vue.js を使ってそれを動的に組み合わせるようにして実装されています。Web Components として、実際のショップページで使用されるものと同等のものをショップ管理画面でもレンダリングしてしまうことで、実際のショップページに近い見た目を再現しています。(説明のしやすさのため、この JSON をデザインデータと呼ぶことにします。)
Web Components の slot の内容を Vue.js で制御し、編集を可能に
実装がどうなっているかを少し解説しますと、コンテナへ動的にパーツを追加できるよう、コンテナを Web Components の slot
に置き換えて、その中にパーツの Web Components を追加しています。<cot-container>
を slot
に置き換えた Web Components を作成し(layout-component
)、その slot の内容を Vue.js で生成しています。
<body> <h1><img src="{{ shop.logo }}" alt="{{ shop.name }}"></h1> <div> <div class="main"> - <cot-container name="main" /> + <slot name="main"></slot> </div> </div> </body>
そしてこの Web Components をプレビュー兼編集可能な GUI としてそのまま表示します。少しややこしいのですが、次のような構造になっています。
<VueContainer> <layout-component> <div slot="main"> <DraggableContainer> <component :is="`${p.name}-component`" v-for="p in designData.container.main" :props="p.props" /> </DraggableContainer> </div> </layout-component> </VueContainer>
DraggableContainer
は DnD を実装するためのラッパーだとしてください。新しいパーツを左側のエリアからここへドラッグすると、デザインデータへパーツの情報が追加されます。追加されると再レンダリングが起こり、v-for
によってそのコンテナに追加されているすべてのパーツが Web Components で表示されます。
パーツの見た目の変更
props
に変更があった場合も同じように再レンダリングが起こり、Web Components が更新されます。これは Vue.js の機能ではなく Web Components で attribute の変更を監視し、必要なら自身の内容をレンダリングしなおすように実装されています。
class extends HTMLElement { // ... static get observedAttributes() { return ['props']; } attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === 'props' && this._shadow) { this._props = newVal this._shadow.innerHTML = this.render() } } }
最後にここで編集したデザインデータを保存すると、変換エンジンがデザインデータをもとに ShopFront 用の Cot を生成して ShopFront から参照できるストレージへ保存します。これで、ノーコードでのデザイン変更が完了します。
まだまだたくさんの課題があります
ざっくりではありますが、BASE が提供するノーコードなデザイン編集機能ついて解説してきました。この機能はリリースされてからもうすぐ二周年となるのですが、実現していきたい機能や解決していきたい課題がまだまだあります。技術的なチャレンジで言えば..。
- そもそも Web Components としてパーツを作ることができればよいのでは?しかし SSR も捨てがたい...
- 商品パーツのように、どうしても機能が山盛りになりがちなものをどう開発保守していこうか?
- 外部デベロッパー向けに仕様を公開して、よりカスタマイズやサービス連携がしやすいようなプラットフォームにできないか?
などなど..。
サービスとしても自由度と制約による保守のしやすさのバランスが大事だったり、ショップのブランディングがより幅広く可能なものを考えたりと、非常にやりがいのある分野となっています。そんなわけで BASE ではあらゆる方面のエンジニアを募集していますので、興味を持っていただたら幸いです。
-
今回は省略していますが、テンプレートエンジンだけでは補いきれないものに関しては WebAPI が ShopFront に実装されています。商品リストのページングや、関連商品、レビューの遅延取得などはこの WebAPI 群を使用しています。↩