Vue.js+Bootstrap+linq.jsでサークルの販売物のダッシュボードをサクッと作ってみた話
お久しぶりです。あんまり更新できてなくてごめんなさい。今回は「じゅ~しぃ~すくりぷと」の販売物の一覧(ダッシュボード)をWebあんまり詳しくない自分が作ってみましたという話です。
目次
動機
販売物の種類が増えすぎた
本1種類ぐらいでしたらツイッターの固定ツイートにリンク書いておけばいいんですが、本の種類が5~6個になって、さらに本ごとに異なる販売サイトがあって、正直固定ツイートだけじゃ管理できなくなりました。
ツイートで宣伝するにしても、最新の物を2個ぐらい書いて「詳しくはこちら」のようにお品書きサイト(ダッシュボード)に誘導する必要性が出てきました。製作期間は2日+αぐらいでしたので、サクッとの部類に入るかと思います。
要件
ダッシュボードのサイトだけでなく、ダッシュボードの参照しているデータを使い回すことができる形式で実装したい。ダッシュボードのデータをブログパーツ(このページの下)でも使いまわしたい。
また、品数が増えたときに、データや画像を追加するだけにして保守コストを下げたい。例えば、品数が増えたときにダッシュボードサイトと、ブログパーツを両方変更して……ということはしたくないです。なぜかというと、特定のサイトで完売した場合に、更新することを考えると、2つの箇所のHTMLをゴリゴリいじるのはやってられないからです。大元のデータだけいじって終わりという形にしたいです。
これを実装するには、例えば大元のデータをJSON(静的APIのようなもの)として管理し、それからシステマティックにページを作成するということが考えられます。今回はページ作成をVue.jsで行いました。大本のデータ(マスタデータと呼ぶことにします)は普通のJSファイルにおいて、scriptタグで読み込みました。データはこちらです(文字化けしていますのでご注意ください)
成果物
ジャンルごとにタブで切り替わります。新刊が上にくるように、発行日ごとにソートしています。
マスタデータのデータ構造
現時点でのJSONの構造は次のとおりです。これは1つの商品のデータです。
{
title: "モザイク除去から学ぶ 最先端のディープラーニング",
key: "mosaic_2020",
publish_date: "2020/3",
genre: "tech",
shops: [
{
shop_url: "https://koshian2.booth.pm/items/1835219",
shop_type: "booth",
caption: "電子+物理2500円、電子1800円"
},
{
shop_url: "https://ec.toranoana.shop/tora/ec/item/040030818462",
shop_type: "tora",
caption: "物理3300円",
sold: true
},
{
shop_url: "https://www.melonbooks.co.jp/detail/detail.php?product_id=1031017",
shop_type: "melon",
caption: "物理4400円、電子3300円"
},
{
shop_url: "https://note.com/koshian2/n/n8ebe5345306c",
shop_type: "note",
caption: "電子1850円"
},
{
shop_url: "https://www.dlsite.com/home/work/=/product_id/RJ314824.html",
shop_type: "dlsite",
caption: "電子2090円"
}
]
}
「title」はそのまま本のタイトルですね。「key」というのは、説明文のHTMLと表紙の画像を参照するためのファイル名です。
説明文は商品ごとに1つのHTMLファイルとし、objectタグで読み込むことにしました。最初はfetch関数で動的にDOMを描画してやろうとか考えましたが、ブラウザのCORSの制限に引っかかってうまくいきませんでした(Node.jsのようなサーバー環境だと問題ないんですけどね)。結局objectタグでHTMLを読み込むとうまくいきました。DOMを描画したいのだったら、マスタデータにHTMLを埋め込んでもよかったかもしれません。商品ごとに1つのHTMLファイルにしたのは、HTMLのほうがVSCodeの編集が楽だったからです。ここの甲乙については後述します。
「publish_date」は発行順でソートするためのものです。新刊かどうかのフラグを編集するのも面倒だったので、「発行から1年以内なら新刊フラグ」をつけるようにしました。このためにも日付のデータは必要でした。
「genre」はジャンルでフィルタリングするためのものです。技術書なら「tech」、グッズなら「goods」などですね。
「shops」は販売サイトです。販売URL、販売サイト、補足説明を書いています。完売フラグは「sold」で管理しています。完売だとバナー画像を替えたり、ブログパーツだとリンクを飛ばさないような処理を入れています。
他には作品単位ですが、ダッシュボードには入れたいが、ブログパーツには(スペースの関係で)入れたくない用に「exclude_blog」というパラメーターを追加しました。
objectタグの良し悪し
CORSの問題があり、外部のHTMLを読み込むのが無理だった(商品説明)ため、objectタグで読み込みました。正直これは良し悪しあります。
良いところ。説明の分量に関わらず、縦幅を固定できる(CSSでheightをいじる)。長い場合だとスクロールバーが出る。
悪いところ。objectタグ内部のスタイルを動的にいじれない。例えばダッシュボードだとフォントサイズがちょうどいいが、ブログパーツにしたときにフォントサイズが大きすぎるという問題が出ます。objectタグのスタイルをいじる方法はよくわからなかったので、小さくしたいときはCSSのtrasnformでいじるというかなり怪しい方法で実装しました。
これはブログパーツのCSSです。
object.promo-item-description {
width: 130%;
max-width: 130%;
height: 300px;
transform: scale(0.8) translateX(-10%) translateY(-30px);
margin-bottom: -50px;
}
めっちゃ怪しい実装。これ「デバイスによって表示がおかしくなる問題あるんじゃ」と思ってスマホで見てみました。ブログパーツの例です。
「CSS完全に理解した」やん! まあスマホ横に倒したら正しい表示になったからいいや。このブログそんなにスマホから見ている人多くないし
BootstrapのHonoka使いました
あんまりBootstrap使っている感じないんですが、知っているテーマがこれだったので。コピペでスタイル使えるの楽でいいですね。タブをクリックするとアニメーションが出るのはこのBootstrapのものです。
Bootstrap Honoka
https://honokak.osaka/
linq.js
ジャンルでのフィルタリングや、日付ソートの部分、「そのままJavaScriptで書くのもだるいな」と思ったので、C#のLINQをJavaScriptで使えるlinq.jsというライブラリを使いました。
JavaScriptの書き方としてはあまり一般的ではないですけど、C#使っている人間にとっては見慣れた書き方になります。個人的には美しいです。タブをクリックしたときのジャンルセレクトの部分のコードです。toArray()いらなかったかもしれないですね。
selectGenre(genreKey) {
return Enumerable.from(bookMaster).where(x => x.genre == genreKey)
.orderByDescending(x => parseDate(x.publish_date)).toArray();
}
これをHTML側では次のように呼びます。
<div class="tab-content">
<div class="tab-pane fade show active" id="tech">
<book v-for="(item, index) in selectGenre('tech')" v-bind:vm="item" v-bind:index=index></book>
</div>
<div class="tab-pane fade" id="eki">
<book v-for="(item, index) in selectGenre('eki')" v-bind:vm="item" v-bind:index=index></book>
</div>
<div class="tab-pane fade" id="goods">
<book v-for="(item, index) in selectGenre('goods')" v-bind:vm="item" v-bind:index=index></book>
</div>
</div>
わーめっちゃシンプル。linq.js+Vue.jsのパターン強そうですね。
ハマりどころ1:new Date(文字列)の結果がブラウザごとに違う
マスタデータの「publish_date」が「2020/12」のような文字列で持っていたので、ソートや日付判定の部分でDateのオブジェクトに変換する必要が出てきます。
Chromeだとnew Dateでそのまま放り込めばいいのですが、Firefoxだとこれがうまくいきません。結果、Firefoxで見ると「日付ソートされない」「新刊フラグが出てこない」というアレな結果になってしまいました。つまり、「new Date(2020, 12)」のように数値に変換して初期化しないといけないわけです。これもlinq.jsで解決します。
function parseDate(dateString) {
// FireFoxでnew Date(dateString)しても正常に動作しない対策
const year_month = Enumerable.from(dateString.split("/")).select(x => parseInt(x));
return new Date(...year_month);
}
※new Date(year, month)のmonthは0インデックスなので、この方法だとnew Date(2021, 8)は2021/9/1、new Date(2021, 12)は2022/1/1を返します。後から気づきました。ただ、7/31も8/1も1日しか違わないので今回はいいかなと思ってスルーしました。厳密さが求められる場合は注意してください。
配列を引数内で「…」と入力すると、Pythonの「func(*array)」のように展開される機能がJavaScriptにもありました。スプレッド構文というそうです。最近のJavaScriptはすごい。
ハマりどころ2:Dark Readerでヘッダー画像がホラーになる
白背景を黒背景にするDark Readerという拡張機能があるそうです。白背景がチカチカして見づらい人向けの拡張機能。
あんこちゃんがホラーになってる。実際に拡張機能をインストールしてみると、CSSをいじるだけでなく、白色の多いような画像はネガポジ反転する機能が入っているそうです。白色背景ではなく、キャラ部分以外の背景を透過pngにしたら、ネガポジ反転されなくなりました。同様の理由でロゴも透過させました。
ハマりどころ3:objectタグ内でのリンクに注意
objectタグはHTMLの中に別のウィンドウを表示している感じなので、そのままリンクをクリックするとobject内にリンク先ページが表示されます。小さくてぶっちゃけ見づらいです。別ウィンドウで表示させる等の配慮が必要です。
OGPを設定する
OGP(Twitterで出てくるカード)を入れ忘れてたので後で入れました。OGPを入れるとSNSで投稿したときにクリック面積が大きくなるので、クリック率的に有利です。WordPressでプラグイン使っているとOGPは自動挿入されるのですが、ダッシュボードではHTML直書きなので手動で入れます。headタグ内にこんな感じに。
<!--OGP-->
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<title>じゅ~しぃ~すくりぷと 公式ダッシュボード</title>
<meta property="og:title" content="じゅ~しぃ~すくりぷと 公式ダッシュボード" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://shikoan.com/" />
<meta property="og:image" content="https://shikoan.com/dashboard_resource/assets/logo_ogp.jpg" />
<meta property="og:site_name" content="じゅ~しぃ~すくりぷと" />
<meta property="og:description" content="じゅ~しぃ~すくりぷと(こしあん)著の書籍・通販一覧です" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@js_koshian2" />
FacebookのOGPはIDの取得がだるかったのでスルーしました。実際にツイートしてみると、
おっいい感じ。OGPって面倒そうに見えるけど、実際はそこまで大変じゃないんですね。
謎現象:Stickyレイアウトで切り替えるとちらつく
ダッシュボードページで、ヘッダー部分は常に上に張り付くようstickyレイアウトを使っています(CSSのposition:sticky)。タブが入っても表示はうまくいくのですが、下のほうまでスクロールしてタブを切り替えると、切替時にStickyで隠れたコンテンツが表示されてちらつきます。
環境依存なのでもともとこういう仕様なのか不明ですが、正直面倒そうだったので放置しました。
結論
細かいところはアレですが、ほぼ想定通りのものが2-3日でサクッと作れてよかったです。Vueで作ろうとするとボトムアップになるんで、最初は大変ですが、ボトムのパーツが完成するとあとは加速度的に完成度が上がっていくのがいいですね。
タブの切替で多少アニメーションをつけようかなと思っていたのですが、BootstrapのCSSでその機能がついていたので楽できました。
これであとは「続きはこちら」としてダッシュボードのページを告知すればいいわけです。品数増えても告知に手間取らないのは楽ですね。
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー