はじめに
筆者は、これまでデモページなどデザイン性を必要としないサイトのスタイルシートは、長らくBootstrapを使ってきた。1bootstrap自体は15年以上続く枯れた技術であり、UI/UXの観点からも完成しているためである。また、Tailwind.cssのようにクラスが長くなるというデメリットもない。JavaScript部分は使わず、スタイルシート部分だけを使っていた。
しかしながら、最近更新が滞っておりscssのコンパイルで警告が出るようになったというのと、使うコンポーネントを厳選してもBootstrap単体でペイロードが130kbあるので冗長かなと考えるようになった。
そこで、白羽の矢が立ったのがWeb Awesome(旧Shoelace)である。
Web Awesomeと、Web Componentsとは?
Web Awesomeとは、Web Componentsベースのフレームワークである。Bootstrapやtailwind.cssなど多くのフレームワークは、クラスを使用してデザインを定めるが、Web Componentsベースのフレームワークでは、VueやAstroのような独自タグを用いてデザインを決める。Vueと異なるのは、これらは全てブラウザのネイティブのShadow DOMという仕組みを使って描画される。
Vueで言うところのTeleportのようなものでDOMツリー上は別のところに存在するが、描画段階でWeb Awesomeの独自タグ側にリンクされる。
VueやAstro(の開発モード)は、HTMLの糖衣構文のフレームワークであるのに対して、Web Componentsではブラウザの標準機能を使って動作させるためオーバーヘッドが少ない。
Sticky footer with fixed navbarで比較するBootstrapとWeb Awesomeの違い
まずは、bootstrapの定番デザインであるSticky footer with fixed navbarをWeb Awesomeで作って違いから考えてみよう。下記コードをコピペしてローカルに起き比較してほしい。
Bootstrapの場合
<!doctype html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sticky Footer Navbar Template</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
</head>
<body class="d-flex flex-column h-100">
<header>
<!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Fixed navbar</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item"><a class="nav-link" href="#">Link</a></li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
/>
<button class="btn btn-outline-success" type="submit">
Search
</button>
</form>
</div>
</div>
</nav>
</header>
<!-- Begin page content -->
<main class="flex-shrink-0 py-5">
<div class="container">
<h1 class="mt-5">Sticky footer with fixed navbar</h1>
</div>
</main>
<footer class="footer mt-auto py-3 bg-body-tertiary">
<div class="container">
<span class="text-body-secondary">
Place sticky footer content here.
</span>
</div>
</footer>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
</body>
</html>
Web Awesomeの場合
これを、Web Awesomeで表現するとこうなる。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sticky Footer Navbar Template</title>
<link
rel="stylesheet"
href="https://ka-f.webawesome.com/webawesome@3.8.0/styles/webawesome.css"
/>
<style>
html,
body {
min-height: 100%;
padding: 0;
margin: 0;
}
footer {
background-color: var(--wa-color-surface-lowered);
}
</style>
</head>
<body>
<wa-page mobile-breakpoint="768" flex-direction="column">
<header
slot="header"
class="wa-dark wa-cluster wa-split wa-align-items-center"
>
<div slot="header-start" class="wa-cluster">
<h1 class="wa-heading-l">Fixed navbar</h1>
<nav>
<wa-button appearance="plain" href="#" target="_blank" size="small">
Home
</wa-button>
<wa-button appearance="plain" href="#" target="_blank" size="small">
Link
</wa-button>
<wa-button
appearance="plain"
href="#"
target="_blank"
size="small"
disabled
>
Disabled
</wa-button>
</nav>
</div>
<div
slot="header-end"
class="wa-cluster wa-gap-s wa-align-items-center"
>
<form class="wa-desktop-only navbar-search">
<wa-input
type="search"
size="small"
form="search-form"
placeholder="Search..."
clearable
>
<wa-button
type="submit"
slot="end"
size="small"
appearance="plain"
>
<wa-icon name="magnifying-glass"></wa-icon>
</wa-button>
</wa-input>
</form>
<wa-button class="wa-mobile-only" variant="neutral" size="small">
<wa-icon name="magnifying-glass"></wa-icon>
</wa-button>
</div>
</header>
<main>
<h1 class="mt-5">Sticky footer with fixed navbar</h1>
</main>
<footer slot="footer">
<p>Place sticky footer content here.</p>
</footer>
</wa-page>
<script
type="module"
src="https://ka-f.webawesome.com/webawesome@3.8.0/webawesome.loader.js"
></script>
</body>
</html>
data-bs-*のような属性の代わりに<wa-page>のような独自タグとslotという属性になっていることがわかるだろう。記述量も少ない。つまり、クラスの肥大化やレイアウトのためだけの必要のないタグが原理的に発生しづらい。故にSEOにも有利である。
この<wa-page>はほとんどのサイトで必要なコンポーネントのレイアウトが最初から定められており、DOMの順番通りに書かなくても適切にslotを使用すれば、見たままのように描画される。これは、Dojo Toolkitや、Ext (Sencha)の発想に近い。
<wa-page>はこの他にも、トップのバナーや折りたたみ式のサイドバー等といった逆U字型のデザインが含まれており、必要に応じて追加することができる。2
実際の使用例
今回は実験用としてOpenLayersでSecondLifeのマップを実装したol-slmap3を人柱にしてみた。
ディレクトリ構造はルートにindex.htmlがあり、実装はsrc/main.tsで行っているという極めてシンプルな構成である。
さて、自分は4月下旬にRsbuildスタックが2.0になり、linterやtesterなどが含まれたエコシステムが完成したことを受けて、随時viteスタックから置き換えを行っている。Rsbuildスタックの特徴についてはなぜRsbuidスタックなのか?を参考にしてほしい。
.
├── LICENSE
├── ReadMe.md
├── biome.json
├── index.html
├── node_modules/
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── rsbuild.config.ts
├── rslint.config.ts
├── src
│ ├── env.d.ts
│ ├── main.ts
│ └── styles.scss
└── tsconfig.json
まずはbootstrapの削除とwebawesomeのインストールである。
% pnpm uninstall bootstrap
% pnpm install @awesome.me/webawesome
rubuild.config.tsは、特に変更する必要はない。疎結合しているのでVuetifyのようにビルドシステムに組み込むと言った仕組みはない。main.tsに必要なコンポーネントだけを入れていく。ここはbootstrapのscssの必要なコンポーネントを@import文で読み込む発想と一緒である。
+ // Web Awesome imports
+ import { setBasePath } from '@awesome.me/webawesome/dist/utilities/base-path.js';
+ // Web Awesome styles
+ import '@awesome.me/webawesome/dist/styles/webawesome.css';
+ // Import the components you want to use
+ import '@awesome.me/webawesome/dist/components/page/page.js';
+ import '@awesome.me/webawesome/dist/components/input/input.js';
+ import '@awesome.me/webawesome/dist/components/button/button.js';
+ import '@awesome.me/webawesome/dist/components/icon/icon.js';
+ import '@awesome.me/webawesome/dist/components/card/card.js';
+
+ // Set the base path to the Web Awesome assets (e.g. icons, themes)
+ setBasePath('@awesome.me/webawesome/dist');
元々bootstrapを読み込んでいた下記の行は削除する。
- // Toggle global options
- $enable-gradients: false;
- $enable-shadows: true;
-
- $offcanvas-box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
-
- // Include functions first
- @import 'bootstrap/scss/functions';
-
- // ...中略
-
- // Utilities
- @import 'bootstrap/scss/utilities/api';
次にHTMLを書き換える。
bootstrap版(オリジナル)
Web Awesome版
だいぶシンプルである。
特徴的なのは、<html>タグでテーマの色を設定し、<wa-page>タグでページのレイアウトを定め、slot属性で流し込む場所のDOMを決めるというところであろう。Bootstrapと異なり、position-relativeのような便利なクラスはないので、ここは自分で実装する必要がある。
下記サイトから実働デモをみることができる。
リポジトリ:https://github.com/logue/ol-slmap
出来上がったもの:https://logue.dev/ol-slmap
Font Awesomeの罠
さて、これまでで「Web Awesome」という言葉を聞いてピンときた人も多いのではないだろうか?そう、Bootstrap2〜3の頃glyphiconに代わってよく使われたFont Awesomeである。
Font Awesome 7はFreeプランでもnpmから利用可能だが、Web Awesomeの<wa-icon>で使えるのはFreeの範囲のみで、魅力的なアイコンを使おうとするとPro+専用の有償契約が必要になる。
つまり、<wa-icon>を使い続けるほど、Pro+への課金動線へ乗せられる構造になっている。
タダより高いものはないのだ。安易に技術選定するとCentOSの怪で話したような罠が待ち受けている。
Iconifyを使おう
FontAwesomeは、Freeでも十分使えるが、気になる人にはこれを回避するため個人的には、<wa-icon>の代わりにIconifyを使うことを提案したい。
もう使っている人も多いと思うが、実はIconifyもWeb Components設計である。スクリプトタグやimport文で読み込ませ、アイコンを使いたい箇所に
<iconify-icon icon="mdi:alert"></iconify-icon>
と書き込むだけである。こちらのほうが使えるアイコン数も多いので安心である。
技術選定するときは、前例にとらわれてはいいけない事はもちろん、機能や利便性だけでなく、なぜこの実装なのかや作っている団体の素性まで見たうえで吟味しよう。
こういうことは、説明書を読むだけではわからないし、リテラシーがないと、AIに質問することさえできないのだ。
終わりに
さて、これは14年前の自分のポストである。jQuery MobileとDojo Mobileを比較して思ったことを端的に書いただけである。
jQuery Mobileでは、巨大なライブラリとCSSを読み込み、下記のようにdata-role属性を使って表現していた。
<div data-role="page">
<div data-role="header">...</div>
<div data-role="content">...</div>
</div>
一方Dojo mobileでは、data-dojo-type属性を用いて必要なときに必要なコンポーネントだけを動的に読み込む実装になっていた。このため、レスポンス上はdojo mobileの方が上である。
<div id="foo" data-dojo-type="dojox.mobile.View">
<h1 data-dojo-type="dojox.mobile.Heading">View 1</h1>
<ul data-dojo-type="dojox.mobile.RoundRectList">
<li data-dojo-type="dojox.mobile.ListItem" moveTo="bar" label="Hello" icon="dojox/mobile/tests/images/i-icon-1.png"></li>
</ul>
</div>
いずれも、程なくして廃れてしまった。結局のところ「技術的に優れたものが勝つとは限らない」好例である。しかし、本物は思わぬ形で生き残る。今回のWeb Awesomeの構文は、単にdata-*がslotに変わっただけである。
<wa-page>
<header slot="header">...</header>
<main slot="main">...</main>
<footer slot="footer">...</footer>
</wa-page>
かつてのJava Appletや、Flashが廃れたように、見てくれが良かったり、多機能だからと安易に新技術に飛びつくのではなく、「その技術の本質とは何か」や「なぜその技術がいいのか」で選ぶことは、とても重要なことである。
Webに限らず、結局のところ技術とは魔法と同じで、フリーレンのゾルトラークの説明のように、特殊なことをせず、標準的なことだけで実装するのが最も強いのである。
余談
筆者は先日v-tokyo Meetup #25にオンライン参加し、Vite+の概要の説明を受けた。ざっと感じたことを率直に言うと、これはVue CLIの再来で、自分の見立てだとあと5年ぐらいでReact+Next.jsやVue+Nuxt.jsスタックは、今で言うjQueryのようなポジションの技術的負債の塊になるだろう。
これは、Vite+が悪いと言うより、技術選定をする立場の人間が、中身をよく理解しないまま、ただ「便利そうだ」とあまり考えずに採用してしまうからである。結果、どんなに設計がいいフレームワークや言語であっても、使い方次第で他の人はおろか、作った当人でさえメンテできないゴミの山になる。
そういうリファクタリング作業は、AIに任せればいい?
「君はじつに馬鹿だな」
物事の背景や本質を理解していない状態で、AIに適切にリファクタリングの指示ができるわけない。前にも述べたようにAIは文脈を理解しない。結果、余計にわけのわからない制御不能なシロモノができあがるだけである。
それ以前に、ちょっとしたリファクタリングでさえ、諸般の事情で行えないのが常態化しているのに、AIのリファクタリングの責任を誰が取りますか? という話になる。
こうなるのは仕方がないとして、できることは、設計段階でいかに複雑さや依存関係を少なくし 「1つのプログラムは1つのことだけをやり、それを完璧にこなすようにせよ」というUNIX哲学の基本原則 を守ることである。
「何でもできる」ということは、「自由度が高い」ではなく、「どうとでもなってしまう」ことであるという事をもう少し理解したほうがいい。
追記:6/22
今朝、Web Awesomeのサイトを確認したところ、comboboxなどのコンポーネントがPro契約でないと使えないようになっていた。ShoelaceがWeb Awesomeになったとき、薄々嫌な予感がしていたが、どうやら的中してしまったらしい。
アイコンだけでなく、ComboboxやDate pickerなどの実用上よく使うコンポーネント自体もPro有償になりつつある。Kickstarterで 「全部無料・OSSで継続」と謳っておきながら、徐々に有償化していくやり方はFont Awesome 6→7の流れと同一である。
本プロジェクトは実験的なものであるため、本格採用の前に気づけてよかったと思う。
-
最もポートフォリオサイトは、カスタマイズしまくったbootstrapを使っているが…。 ↩
-
<main>タグ部分は、自動的に左右に余白が生成されるためマップなどを前面に置きたい場合はスタイルシートでmain { padding: 0; }のような指定が必要になる。 ↩ -
作った動機は、最近見かけるマップを表示するプログラムがLeaflet一辺倒だったので、普段使っているOpenLayersで実装したらどうなるか?という単純な技術者としての知的好奇心からである。それ以上でもそれ以下でもない。 ↩