みなさんこんにちは。
NTTテクノクロスの上原です。業務ではGatsbyを使って内製キュレーションサイトの構築や運用などを行なっています(関連で一昨年書いた記事→Gatsbyの真の力をお見せします)。
これは、NTTテクノクロスアドベンドカレンダー2020 の20日目の記事です。昨日の記事は @y-ohnuki さんによる「iPhone12ProのLiDARスキャナを試してみる」でした。
去年はこんな記事(React-SpringのHooks APIでブラウザアニメーションを基本から極めよう!)を書いてたわけですが、今年は新型コロナでいろいろとたいへんだった年でした。みなさまは、いかがお過しだったでしょうか。一年があっという間ですね。来年は何をしているのだろうか。
今年はノーコード・ローコード開発サービスであるAirtableのAPPS機能をプログラマ目線で紹介します。
対象読者
- ノーコード開発ツールに興味がある技術者
- マイクロフロントエンド的にReactアプリを結合させて機能させることに興味がある人
- 未来のアプリ開発のやりかたについてインスピレーションを得たい人
Airtableって何?
Airtableはブラウザから利用できるスプレッドシートのインターフェースを持ったオンラインデーターベースサービスです。カレンダーやカンバン的に表示したり、スケジュール管理に使ったり、ワークフローを組み合わせたりもでき、スプレッドシートの自然な拡張として、専用の業務アプリが提供するような機能を、非プログラマでも直感的かつ柔軟に達成できるサービスであり、アプリ開発環境でもあります。
Airtableは2013年からの老舗でありますが、最近流行りの「ノーコード開発プラットフォーム」の中でも頭一つ抜けた存在とも言え、2020年9月にも$185Mという大規模な資金調達を成功させ、評価額25億8,500万米ドルを誇るユニコーン企業としても注目を集めています。
Airableはもともとはコードを一切書かない・書けない「ノー」コード開発ツールでしたが、2020年9月にリリースされたIFTTTライクなAutomation、JavaScript/TypeScriptによる機能拡張を可能にするAPPS(およびScripting APPS)の提供によってローコード開発ツールに展開されたことになります。
ビジネスとして、またツールの有用性としてAirtableの成功は興味深いものですが、「未来のWebアプリ開発」を見通すことに関してソフトウェアデザイン、エコシステムとしても非常に興味深いものです。本記事ではプログラマ目線でノーコード開発ツールの一つとして紹介し、印象をお伝えしたいと思います。
Airableサービスはどんな感じのものか?
こんな感じの画面。並んでいるアイコンは「ベース」と呼ばれる単位で、Excelでいうxlsファイル(ブック)に相当します。ベースをクリックすると以下のような、タブで切り替えられる「テーブル」の集合を見ることができます。
ベース:
ブラウザ上で、まずはOffice 365のWeb版ExcelやGoogle Spread Sheetのように直感的に使うことができます。ベースは概念的に以下のような構造をもっています。
ベースはテーブルの集合で、Excelでいえばシートをあつめたものがブックになる、というイメージです。ただしAirtableでは常に「ビュー」というレイヤを1層挟んでテーブルをユーザが見たり操作したりすることになります。
今までに出てきた用語を表にまとめると以下のとおりです。
Airtableの用語 | Excelでの対応物 | 意味 |
---|---|---|
ワークスペース | ブックが格納されているフォルダ | 共同作業者(コラボレータ)と共有できる単位のベースの集合。プロジェクト。 |
ベース | ブック(.xlsx) | テーブルの集合。1画面。テープルをタプで切り替えられる。 |
テーブル | ワークシート | レコードの集合 |
レコード | ワークシートの1行 | テーブルの1行 |
ビュー | 対応物なし | テーブルの見せかた。不要なものを隠蔽したりソートしたり フィルタしたり。kanbanやカレンダーの ビューなどに対応。 |
APPS | 対応物なし(あえて言え ばグラフ領域やピボット テーブル領域) |
ミニアプリ。 |
Airtableの特徴はなんなの?
以下が特徴になります。
- データモデリングを出発点かつ中心としたツール。
- レコードの集合としてのテーブルを定義していく。レコードのフィールドは静的な型を持っている。
- SQLが不要なリレーショナルデータベースでもある。 リレーションやカーディナリティ(1:n、m:n…)を設定できる。
- テーブル間の関係をExcelのセル操作のようなレベルでとても直感的に設定できる。
- Excelライクなわかりやすいユーザインターフェースで扱う
- 機能の大半はExcelのような汎用インターフェースで達成する。一般アプリとまったく同じ使い勝手のUIはめざさない。
- その他
- 実データを活用したRest APIと参照ドキュメントがリアルタイムに生成される。実際のデータを元にしてサンプル出力などを表示するのでわかりやすい。
- APPS(後述)で拡張可能。一例としてGraphQL APIも生成できる(BaseQL APP)。
「ノーコード開発ツール」といってもいろいろですが、Airtableの方向性は明確です。それは、現実社会で一般に、業務の多くがExcelのような表計算でまかなえていることに着目し、その延長・拡張として機能実現することです。考えてみましょう、一般の事務職や会社員が、いかにExcelとファイル共有という汎用インターフェースだけで多くの業務をこなしてきているかを。このコンセプトによってAirtableは圧倒的なとっつきの良さ、開発速度と機能カバーを実現しています。
なお、本記事で言及しているのはAirtableのごく一部です。Gatsbyのデータソースに使えるなど、連携機能も豊富です。
ノーコード開発ツールについて少しだけ
字面から言えば、「ソースコード」を一切書き下さずにソフトウェアを開発するものがノーコード開発ツールであり、コードを書く量が少ないものの、ゼロではないものがローコード開発ツールです。とはいえ境界はあいまいです。
現代的な意味のノーコード・ローコード開発ツールは、一般にクラウドサービスとして提供されていることが必要です。いわゆる「ビジュアルプログラミング言語」であればノーコード開発ツールと言えるかというと多分違うのであって、データ管理や実行環境を含めてクラウドサービスとして機能を利用できることが従来からのツールとの違いであり、わざわざ「ノーコード・ローコード」と新しい名前で呼ぶ理由の一つであると言えます(例外はたぶんありますが)。
つまり、ソフトウェアライフサイクルのうちのコーディングだけではなく、ビルド、デプロイ、テスト支援といった開発支援系機能を統合サービスとしてクラウド上で提供するものです。
とはいえ、従来からあったもののリブランデイングで呼び名が新しいだけ、という面もあるのもたぶん確かです。
Airtable APPS(アプリ)
APPSはReactで書く「ミニアプリ」です。
AirtableがExcelだとしたら、Excel中に埋め込める「グラフ領域」や、「ピボットテーブル領域」を想像してみると少し近いです。実際、Airtableのピボットテーブルやグラフ機能はAPPSとして利用できます。なお、APPSは有償のPro Planのみで使用できる機能です(ただ2020年12月現在、登録後2週間はPro Plan無料で利用できるようです)。ただし、自分でビルドして利用するカスタムAPPSは、今のところ無料版(Free Plan)でも開発したり使用することができるようです(ただし保証や将来も利用できるかものかなどは不明)。なお、APPSは以前はBlocksという名前でしたが、名前が変更されました。以降で時々出てくるblockはAPPSと同義です。
APPSの実行の様子は以下のとおりです。
APPSは「ダッシュボード」の中にまとめて表示でき、利用者はダッシュボードをカスタマイズしてそれを表示することでノーコードプラットフォームとしてのAirableをAPPSを通じて利用することができます。
Appsは個別にAirtableのベースに追加し、Airtableの中で実行して使用します。単独では使用できません。
ReactでAPPSを作ろう!! 🚀🚀🚀
以上は前置きでした。以降が本題です。早速カスタムAPPSを作ってみましょう。APPSはFirebaseのようにリアルタイムデータ更新を扱えますので、チャットアプリを作ってみます。
データモデリング
Airtableの開発の流儀としてデータモデリングを最初にやります。ここでは「発言内容」「作成者」などのフィールドを持ったテーブルを定義し、それにチャットアプリをアタッチできるようにします。
準備として、まずベースを新規作成(ベース名「チャット」、アイコンを適当に設定)を作成し、デフォルトで作成されているテーブルを編集し、テーブル名「発言一覧」にして、レコードヘッダの「Customize Field Type」を選択して以下のようなテーブルを作成します。
フィールド名 | Type | 内容 | GUI上での入力内容 |
---|---|---|---|
ID(Primary Field) | Autonumber | - | |
日時 | Formula | CREATED_TIME() | |
内容 | Long text | - | |
発言者 | Created By | - |
以上より、以下のような「発言一覧」テーブルが作成されます。
ここでは使用しませんでしたが、フィールドに「Link to another record」という型を指定することで、他のテーブルのフィールドの値をRDBで言うところの外部キーとして使用することなどができます。
ひながた生成
では自作のAPPSである「カスタムAPPS」を作成していきましょう。以下の操作を行います。
- 準備として、画面右上の「Account」メニューから「Account」を選び、表示されるAPIキー(★1)を確認します。
- ベースの画面、右側の「APPS」をクリック
- 「Install an app」をクリック
- 「Build a custom app」をクリック
- テンプレートギャラリーから「Hello world (TypeScript)」をチェック
- 「App name」に「Chat」を入力、「Creating App」ボタンをクリック
以降、表示されるガイダンスに従います。
- ターミナルで「npm install -g @airtable/blocks-cli」を実行
- 同じくターミナルでAPPSの雛形の生成を行います。(なお現在のblock initはProxy背後ではエラーになるかも)
block init appO9XXXXXXXXXXXX/blkYYYYYYYYYYYYYY --template=https://github.com/Airtable/apps-hello-world chat
- 初回は以下が表示されるので、(★1)で用意していたAPIキーを入力します。
? Please enter your API key. You can generate one at https://airtable.com/account
これでうまくいけばAPPSアプリのソースコード雛形が作成されます。
Using your existing API key from /Users/uehaj/.config/.airtableblocksrc.json
Initializing block using https://github.com/Airtable/apps-hello-world-typescript template
[npm]
[npm] > core-js@3.8.1 postinstall /Users/uehaj/work/lowcodenocode/airtable/air_chat/node_modules/core-js
[npm] > node -e "try{require('./postinstall')}catch(e){}"
[npm]
[npm] added 225 packages from 225 contributors and audited 225 packages in 55.392s
[npm]
[npm] 23 packages are looking for funding
[npm] run `npm fund` for details
[npm]
[npm] found 0 vulnerabilities
[npm]
✅ Your block is ready! cd air_chat && block run to start developing, and npm run lint to lint.
カスタムAPPSの実行
一旦以下を行いAPPSを実行してみます。
cd chat
block run
うまく行けば以下が表示されます。
-
block runで表示されたURL、さっきの場合だと「https://localhost:9000 」を入力します(ポート番号は異なることがあり)
-
ブラウザでは以下のようにアプリが実行されています。
ソースを修正して保存すると自動リロードが走ります。
ReactコードとしてのAPPS
APPS SDK(Blocks SDK)が提供するのはReactをベースとしたSDKです。ただし開発できるアプリには以下の制限や特徴があります。
- Airtable画面の一部の矩形領域パーツ(iframe)として実行される。メインメニューや自身の複雑なレイアウトはもたない。
- アプリのデプロイとホスティングは気にしなくてよい。ローカルビルドのときはlocalhostでdev serverが動作するが、いったんblock releaseすればAirtableのサーバ内でホスティングされる。
- webpackの設定など、こまかいことはできない。
- 任意のnpmモジュールの使用が可能。
- スタイルシステムはAirtable提供のもの(loadCSSromStringなど)を使用する。CSS in JSなどもやればできると思うが、全体のスタイルと不一致となるし、画面を占有できないし、一工夫が必要だと思われる。(あまり凝るべきではないのかもしれません)
APPSにはUIデザインガイドラインがあり、Airtableの中でよりよく機能を発揮するように、コンポーザブル(合成可能)で、柔軟性があり、協調的に動作する、といった指針が定義されています。
hello world APPSのコード
APPSの開発を始める前に、block buildコマンドでデフォルトとして生成されたHello Worldアプリ(flontend/index.tsx)の中身を見てみましょう。
import {initializeBlock} from '@airtable/blocks/ui';
import React from 'react';
function HelloWorldTypescriptApp() {
// YOUR CODE GOES HERE
return <div>Hello world 🚀</div>;
}
initializeBlock(() => <HelloWorldTypescriptApp />);
Create React Appで生成したReactアプリのようなReactDOM.render()を行なわないことに気付きます。その代わりにinitializeBlock()」にコンポーネントを渡します。それ以外は基本的には普通のRactアプリです。
Chatアプリ開発
ではチャットアプリを作っていきます。フォルダ・ファイル構成はこんな感じです。
chat/frontend
├── components
│ ├── ChatPanel.tsx
│ └── Setup.tsx
├── index.tsx
└── useConfig.tsx
./index.tsx
トップレベルに置くindex.tsxを置き換えて以下の内容のとおりにします。
import {
Box,
initializeBlock,
} from '@airtable/blocks/ui';
import React from 'react';
import ChatPanel from './components/ChatPanel';
import Setup from './components/Setup';
function ChatApp() {
return (
<Box flexDirection='row' display="flex">
<Box flex="8" padding={3} ><ChatPanel /></Box>
<Box flex="auto" ><Setup /></Box>
</Box>
);
}
initializeBlock(() => <ChatApp />);
index.tsx解説
ここで使用しているBoxはdivに展開されるコンポーネントです。flexboxの制御のためのパラメータを指定できます。ここらへんのUI部品群はMaterial UIなど既存のものではなく、Airtable独自のもののようです。
./useConfig.ts
アプリの設定情報にアクセスするためのhook。
import { useGlobalConfig } from '@airtable/blocks/ui';
const configKeys = [
'selectedTableId',
'selectedViewId',
'selectedMessageFieldId',
] as const;
type ConfigKeys = typeof configKeys[number];
export default function useConfig() {
const globalConfig = useGlobalConfig() as {
get(key: ConfigKeys): string;
};
const selectedTableId = globalConfig.get('selectedTableId');
const selectedViewId = globalConfig.get('selectedViewId');
const selectedMessageFieldId = globalConfig.get('selectedMessageFieldId');
return {
selectedTableId,
selectedViewId,
selectedMessageFieldId,
};
}
useConfig.ts解説
globalConfigはBlock SDKの特徴的な機能の一つで、APPSのインスタンスごとにサーバサイドに確保される設定情報を保存するストレージだと思ってください。キー名のハッシュとして任意の値を保存・取得できるのですが、型つきで扱うために、キーをas constした文字列配列にしています。
ここで保存しているのは、アプリと、テーブルのビュー・カラムに対する紐付け情報です。具体的には、APPSの初回実行時に下図のように「どのテーブル」「どのビュー」「どのフィールド」かなどを処理対象として指定して紐付けます。その対応は上記の処理によって明示的にglobalConfigに保存します。
ちなみにGlobalConfigが壊れた場合にアプリが起動しなくなるなどがありえます。あるいはフィールドの紐付けをやりなおしたい場合は、その機能を作り込まなくてもAirableのUI(APPSの「Glocal config」)からClearを行うことができます。
./components/Setup.tsx
前述のGlobalConfigを設定するためのUIです。
import React, { useState } from 'react';
import {
TablePickerSynced,
ViewPickerSynced,
FieldPickerSynced,
FormField,
Box,
useBase,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';
export default function Setup() {
const { selectedTableId } = useConfig();
const base = useBase();
const table = base.getTableByIdIfExists(selectedTableId);
return (
<>
<Box padding={3} borderBottom="thick">
<FormField label="テーブル">
<TablePickerSynced globalConfigKey="selectedTableId" />
</FormField>
<FormField label="ビュー">
<ViewPickerSynced table={table} globalConfigKey="selectedViewId" />
</FormField>
<FormField label="Created byフィールド">
<FieldPickerSynced
table={table}
globalConfigKey="selectedCreatedByFieldId"
placeholder="Pick a 'created by' field..."
/>
</FormField>
<FormField label="Messageフィールド" marginBottom={0}>
<FieldPickerSynced
table={table}
globalConfigKey="selectedMessageFieldId"
placeholder="Pick a 'message' field..."
/>
</FormField>
</Box>
</>
);
}
Setup.tsx表示
以下のように表示されます。
Setup.tsx解説
Airtable APIには以下のような、存在するテーブル/ビュー/フィールドをそれぞれ選択するための専用のGUI部品があり、それにGlobalConfigのキー名文字列を指定するだけでGloalConfig領域への読み書き含めて行なってくれます。
- TablePickerSynced
- ViewPickerSynced
- FieldPickerSynced
いずれも、存在するテーブルやビュー名から選択するSelectユーザインターフェースで設定できます。
./components/ChatPanel.tsx
チャットのメイン画面です。
import React, { useState } from 'react';
import {
useBase,
useRecords,
Box,
Input,
loadCSSFromString,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';
loadCSSFromString(`
.base {
background-color: #34569b;
padding: 0.5rem;
border-radius: 10px;
}
.balloon {
position: relative;
display: block;
margin: 0.5rem 100px 1.0rem 10rem;
padding: 10px 10px 20px 10px;
min-width: 120px;
max-width: 100%;
margin-left: 20px;
color: #555;
font-size: 16px;
background: #e0edff;
border-radius: 15px;
}
.balloon:before {
content: "";
position: absolute;
top: 50%;
left: -15px;
margin-left: -10px;
margin-top: -15px;
border: 15px solid transparent;
border-right: 15px solid #e0edff;
z-index: 0;
}
.balloon p {
margin: 0;
padding: 0;
}
`);
export default function ChatPanel() {
const {
selectedTableId,
selectedViewId,
selectedCreatedByFieldId,
selectedMessageFieldId,
} = useConfig();
const base = useBase();
const table = base.getTableByIdIfExists(selectedTableId);
const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
const messageField = view
? table.getFieldByIdIfExists(selectedMessageFieldId)
: null;
const records = useRecords(view, {
fields: [selectedCreatedByFieldId, selectedMessageFieldId],
});
return (
<Box className="base">
{messageField && (
<Box>
<input
onKeyPress={(e: any) => {
if (e.key === 'Enter') {
table.createRecordsAsync([
{ fields: { [selectedMessageFieldId]: e.target.value } },
]);
e.target.value = '';
e.preventDefault();
return false;
}
}}
placeholder="発言をどうぞ"
size={50}></input>
{records &&
records
.slice()
.sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
.map((msg) => (
<div className="balloon">
<p>{msg.createdTime.toLocaleString()}</p>
<p>
{(msg.getCellValue(selectedCreatedByFieldId) as any).name}
</p>
<p>{msg.getCellValue(selectedMessageFieldId)}</p>
</div>
))}
</Box>
)}
</Box>
);
}
ChatPanel.tsx表示
以下のように表示されます。
ChatPanel.tsx解説
以下、個別に説明します。
loadCSSFromString(`
.base {
:
});
loadCSSFromString()でクラス名指定のスタイルシートを設定します。CSS in JS的な方法もおそらくは適用できるのでしょうが調べきれず。
const {
selectedTableId,
selectedViewId,
selectedCreatedByFieldId,
selectedMessageFieldId,
} = useConfig();
GlobalConfigからの読み込みを処理を行うカスタムHook、useConfig(前述)を使用して設定項目を取り出します。
const base = useBase();
useBaseはAirableのベースを取得します。useStateの様に動作し、すなわち変更があったときだけ新しい値でレンダリングが行なわれます。
const table = base.getTableByIdIfExists(selectedTableId);
const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
const messageField = view
? table.getFieldByIdIfExists(selectedMessageFieldId)
: null;
baseから順に、テーブル、ビュー、フィールド名を取得します。
const records = useRecords(view, {
fields: [selectedCreatedByFieldId, selectedMessageFieldId],
});
最終的に表示したい発言のリストを、使用したいフィールドを指定して取得します。
<input
onKeyPress={(e: any) => {
if (e.key === 'Enter') {
table.createRecordsAsync([
{ fields: { [selectedMessageFieldId]: e.target.value } },
]);
e.target.value = '';
e.preventDefault();
return false;
}
}}
placeholder="発言をどうぞ"
size={50}></input>
JSXではonKeyPressハンドラを設定した入力フィールドを用意します。Inputではなくinputを使っているのはonKeyPressハンドラが指定できなかったためですが原因不明。
{records &&
records
.slice()
.sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
.map((msg) => (
<div className="balloon">
<p>{msg.createdTime.toLocaleString()}</p>
<p>
{(msg.getCellValue(selectedCreatedByFieldId) as any).name}
</p>
<p>{msg.getCellValue(selectedMessageFieldId)}</p>
</div>
))}
発言を作成日時でソートして表示します。ビュー側でソートすることもでき、ビューモデルとしてはその方が正しいかもしれませんが、ここではクライアント側でソートしておきます。
Chatアプリの実行🚀🚀🚀
上記のソースコードを保存すると、刻々とAirtableのAPPS領域のダッシュボード内に表示されるアプリが更新されていくと思います。完成したならば、Setupで表示されるフィールドに実際のベースのテーブル、ビュー、発言フィールドを選択させます。先に作成していたテーブルであれば、テーブルに「発言一覧」、ビューに「Grid View」、「Created Byフィールド」に「内容」、Messagesフィールドに「内容」を設定します。
するとアプリが結びついてチャットができるようになります。
Chatアプリのリリース🌺🌹🌷🌼💐
動作確認が済んだら以下を実行してアプリをAirbleのサーバサイドにリリースすることができます。
block release
リリースするとblock runをローカル実行する必要がなくなります。一回リリースすると、修正には再リリースが必要になります(localhostに立てたdev serverを使ってのリアルタイム更新はできなくなる)。
なお、カスタムAPPSをマーケットプレースに公開することもできるし(レビューあり)、Gibhubにソースコードを公開しておくことで他者にカスタムAPPSとしてビルドしてもらう前提で公開することもできます。本稿で作成したアプリは以下に公開しております。カスタムAPPSのBuild An Appのときに「Remix from GitHub」を選びURLを入力することで選択できる雛形として利用することができます。(やってることはgithubの指定プロジェクトの最新のソース内容を展開し、ベースとの紐付けのIDを含んだ.block/remote.json作成することです)
Airtableでのプログラミング、設計、開発について
Airtableを調査するにあたってプログラマとして気づいたことを列挙してみます。
(1)データモデリングから始めよう
一般に、Airtableでのアプリ開発ではデータモデリングから開始します。Airtableのデータモデリングとは、テーブルのフィールドの「型」を設定していくことです。これは常に実データを見ながらDBテーブルのスキーマ定義を行うことです。場合によってはAPPSを使用せずに業務が完結するかもしれません。APPS開発をするとしたら、アジャイル開発で求められる条件「最初の段階からミニマムな機能が動いていること」が達成できています。
(2) フォーミュラは副作用なしメソッド
Airtabbleのフィールドに設定できる型は多彩ですが、特徴的な型の一つに「フォーミュラ(Formula)」があります。これはExcelのセル式に対応するように見えますが、カラム全体の設定であることが大きな違いです。その違いが何を生んでいるかというと、レコードを「クラス定義」とみなしたとき、フォーミュラがメソッド定義の機能を担えることです。副作用がないのでデバッグは容易です。CQRS(コマンドクエリ責務分離)のクエリとも言えます。
APPSを組み合せるときでも、ビューまわりのための加工はフォーミュラとしてサーバサイドで実行してしまうことができます。SPAと組み合わせる場合、ビューモデルをサーバサイドで作っているということでもあります。
(3) REPLのように
実データをもとに、「動くデータ」を元にして、処理結果を見ながらアプリを開発していきます。
おわりに
クラウドの隆盛と開発技術の進展で、ツールやMbaaSなどを活用して、新規コード開発以外の方法で効率良く機能達成を行なうことが求められている時代を迎えています。プログラミングが授業で教えられる時代であり、アプリ利用とアプリ開発の垣根が下っていくことにも間違いありません。しかし、その先にあるものはプログラマとして見ても、あるいはプログラマ視点で見るからこそ、依然として豊かで興味深いものです。私はAirtaleのAPPSの例を通じて近未来のアプリ開発のありかたの一端を感じました。その一端が伝わればと思います。
ちなみにノーコード・ローコード開発の分野は奥が広く、Outsystems、Mendix、ほかさまざまな別の方向でそれぞれに別世界が開かれています。
明日は@nakasho-devさんの記事です。
では良いお年を!
参考
- 上では使用していないが、従来のAPIベースのJSクライアントSDK: https://github.com/Airtable/airtable.js