こんにちは。桐生です。久々の投稿となりました。
最近Next.js
+urql
+chakra-ui
で環境を構築する機会があったのですが、Deno上にも同じような環境が作れないかと思い、Aleph.jsを使っても同じようにやれるのか試してみたので、その内容を共有したいと思います。
そもそもDeno
とは?については、以前ブログを書きましたので、合わせてご覧ください。
Aleph.jsとは
Aleph.js is a fullstack framework in Deno, inspired by Next.js.
Aleph.js
とは、公式ドキュメントにある通り、Next.jsに着想を得たDeno上で動くReactフレームワークです。
公開されてから既に1年以上経っており、いろいろな方々がAleph.jsを試して記事にされていたりするので存在を知っている方も多いのではないでしょうか。
使い方はシンプルで、
# Aleph.jsのインストール
deno run -A https://deno.land/x/aleph/install.ts
# 新規アプリケーション作成
aleph init
# `development` mode で実行
aleph dev
# `production` mode で実行
aleph start
などのコマンドが用意されています。
aleph dev
を実行したときに、esm.sh
のロードの挙動にハマったので後述します。
esm.shとは
A fast, global content delivery network to transform NPM packages to standard ES Modules by esbuild.
esm.sh
とはCDNの一つで、npmパッケージにあるモジュールをesbuildを使ってESMに変換して配信しています。Denoでサードパーティライブラリを使用する際によく使われるCDNで、他にもskypackなどがあります。
ただし、もともとnpmパッケージはDenoで動かすことを想定していないので、Denoで動かない、Denoでそもそもインポート時にエラーになるといったこともよくあります。が、esm.sh
のバージョンが上がるにつれて解消されていろいろ使えるようになっていっています。
Github Issue を覗いてみると Failed to import - <package name>
というタイトルのIssueが多く立っているので、自分の使いたいnpmパッケージでエラーが出るようなことがある場合は、同じようにIssueを立てておくと、今後Fixしてくれるかもしれません。
またAleph.js
のメンバー自体がesm.sh
の開発にも携わっていることもあり、Aleph.sh
でもesm.sh
固有の処理があったりします。
今回の環境
> deno --version
deno 1.17.0 (release, x86_64-apple-darwin)
v8 9.7.106.15
typescript 4.5.2
> aleph -v
aleph.js v0.3.0-beta.19
また、検証時のesm.sh
の最新v61
を使用しました。
Aleph.jsでurqlを使う
これまでApollo Client
を使っていましたが、他のGraphQLクライアントの知見も貯めておきたいと思いurql
を使うことにしました。
https://formidable.com/open-source/urql/docs/comparison/#framework-bindingsにある通りReact Suspense
に対応しているのがいいですね。Suspense
との組み合わせによりロード中の状態を宣言的に記述できるようになって、コンポーネント実装がシンプルになることが期待できます。
ただし、Aleph.js
はデフォルトではSSRモードで動くため、そのままではSuspense
が使えません。そこでaleph.config.ts
というファイルを作って(aleph init
では作られません)でSSRをオフにセットしておく必要があります。
// aleph.config.ts
import { Config } from 'https://deno.land/x/aleph@v0.3.0-beta.19/types.d.ts';
export default <Config>{
ssr: false,
};
それではurql
を使っていきましょう。
まずはesm.sh
からimportするため、import_map.json
に追加します。
// import_map.json
{
"imports": {
...
"urql": "https://esm.sh/urql"
},
}
実はAleph.js
特有の処理はこれくらいで、あとは普通に実装していくだけです。
続いて、Clientの作成とProviderの設定です。フリーのGraphQLエンドポイントとしてSpaceX Land APIを使っています(このAPIでSpaceXの打ち上げデータなどを取得できる)。また、Suspenseを有効にするためsuspense: true
をセットします。
// app.tsx
import React, { FC } from 'react';
import { createClient, Provider } from 'urql';
const client = createClient({
url: 'https://api.spacex.land/graphql/',
// enable suspense
suspense: true,
});
export default function App({ Page, pageProps }: { Page: FC; pageProps: Record<string, unknown> }) {
return (
<Provider value={client}>
<main>
<head>
<meta name="viewport" content="width=device-width" />
</head>
<Page {...pageProps} />
</main>
</Provider>
);
}
Queryする側の実装です。SpaceX
コンポーネントを作りuseQuery
を使ってデータ取得する実装を行います。ローディング中状態はSuspense
に任せることにし、ここで実装はしません。コンポーネント内から分岐処理がなくなりとてもシンプルになりました。素敵ですね。
// components/SpaceX.tsx
import React from 'react';
import { useQuery } from 'urql';
const LaunchesPastQuery = `
{
launchesPast(limit: 10) {
mission_name
launch_date_local
links {
video_link
article_link
}
rocket {
rocket_name
}
details
}
}
`;
export function SpaceX() {
const [result] = useQuery({
query: LaunchesPastQuery,
});
return (
<>
{result.data?.launchesPast?.map(({ mission_name, launch_date_local, links, rocket, details }) => {
return (
<article key={mission_name}>
<h2>Mission: {mission_name}</h2>
<section>
<p>
{new Date(launch_date_local).toLocaleDateString()} | <strong>{rocket?.rocket_name}</strong>
</p>
<p>{details}</p>
<div>
<a href="{links.video_link}" target="_blank" rel="noopener">
video
</a>{' '}
<a href="{links.article_link}" target="_blank" rel="noopener">
article
</a>
</div>
</section>
<hr />
</article>
);
})}
</>
);
}
最後にSpaceX
コンポーネントの組み込みです。Suspense
でラップしてローディング中の状態を実装します。
// pages/index.tsx
import React, { Suspense } from 'react';
import { SpaceX } from '../components/SpaceX.tsx';
export default function Home() {
return (
<Suspense fallback={<p>loading...</p>}>
<SpaceX />
</Suspense>
);
}
aleph dev
で実行してみると、loading...
としばらく表示されたあと、SpaceXの打ち上げ情報がリスト表示されました。無事Aleph.js
上でurql
(とSuspense
)が動いているのを確認できました。
余談1
実は最初、importするURLをhttps
ではなくhttp
と記述していたために、なぜかuseQuery
の実行時にエラーになる、という事象に陥りました。
// import_map.json
{
"imports": {
...
"react": "https://esm.sh/react@17.0.2",
"react-dom": "https://esm.sh/react-dom@17.0.2",
"urql": "http://esm.sh/urql" // http にしてしまっていた
},
}
Aleph.js
の実装を追っかけてみると、esm.sh
経由でimportしたモジュールについては、aleph dev(dev mode)
とaleph start(production mode)
)とで、ロードするモジュールのモードを切り替えている、ということがわかりました。
https://github.com/alephjs/aleph.js/blob/v0.3.0-beta.19/server/aleph.ts#L993-L1001 の実装を見てみてください。
// append `dev` query for development mode
if (this.isDev && specifier.startsWith('https://esm.sh/')) {
const u = new URL(specifier)
if (!u.searchParams.has('dev')) {
u.searchParams.set('dev', '')
u.search = u.search.replace('dev=', 'dev')
specifier = u.toString()
}
}
aleph dev
で実行している場合、https://esm.sh/
から始まるimport urlについてはAleph.js
が?dev
というクエリストリングを付与するようになっています。
一方esm.sh
は、urlにdev
クエリストリングが含まれている場合、Development modeのモジュールを返すという機能があるので、aleph dev
で実行した場合はDevelopment modeのモジュールがロードされるようになっています。
import_map.json
をもう一度確認すると、
// import_map.json
{
"react": "https://esm.sh/react@17.0.2",
"react-dom": "https://esm.sh/react-dom@17.0.2",
"urql": "http://esm.sh/urql"
}
react
はhttps://esm.sh
にマッチするので、aleph dev
でDevelopment modeのモジュールがロードされます。
一方で、urql
はhttp://esm.sh
だったので上記条件にマッチせず、Production modeのurql
がロードされるようになっていました。さらに、urql
はreact
を依存モジュールとして持っていたので、同じくProduction modeのreact
がロードされることになりました(ここがややこしかった)。
これにより、Aleph.js本体は dev mode の react で実行されているにもかかわらず、urql
およびその依存モジュールであるreactは prod mode で同居する形になり、その結果 Context が共有されなくなり useQuery
をコールしたタイミングで実行時エラーが出ていた、というわけでした。
本当につまらないミスで、エラー解消までに多大な時間と労力を消費してしまいました。とはいえ、これがきっかけでAleph.js
の内部処理を知ることができたので、良しとしましょう。
Aleph.jsでchakra-uiを使う
気を取り直して chakra-ui
を入れてみましょう。まずはimport_map.json
に以下のように追加します。
// import_map.json
{
"imports": {
...
"chakra-ui": "https://esm.sh/@chakra-ui/react",
"emotion/react": "https://esm.sh/@emotion/react",
"emotion/styled": "https://esm.sh/@emotion/styled",
"framer-motion": "https://esm.sh/framer-motion"
},
}
Aleph.js固有の処理はこれだけで、あとは通常通り実装していくだけです。
続いてChakraProvider
の設定です。特別なことはありません。
// app.tsx
import React, { FC } from 'react';
import { createClient, Provider } from 'urql';
import { ChakraProvider } from 'chakra-ui';
...
export default function App({ Page, pageProps }: { Page: FC; pageProps: Record<string, unknown> }) {
return (
<Provider value={client}>
<ChakraProvider>
<main>
<head>
<meta name="viewport" content="width=device-width" />
</head>
<Page {...pageProps} />
</main>
</ChakraProvider>
</Provider>
);
}
最後にchakra-ui
を使ってSpaceX
コンポーネントをスタイリングしていきます。こちらも特別なことはなし。
import React from 'react';
import { useQuery } from 'urql';
import { Badge, Flex, Heading, HStack, Link, Text, VStack } from 'chakra-ui';
...
export function SpaceX() {
const [result] = useQuery({
query: LaunchesPastQuery,
});
return (
<VStack spacing={4} align="stretch" p={4}>
{result.data?.launchesPast?.map(({ mission_name, launch_date_local, links, rocket, details }) => {
return (
<Flex as="article" direction="column" gap={2} p="4" borderWidth="1px" borderRadius="lg" key={mission_name}>
<Heading as="h2" size="lg">
{mission_name}
</Heading>
<Flex direction="column" gap={2}>
<HStack spacing={2}>
<Text fontSize="sm">{new Date(launch_date_local).toLocaleDateString()}</Text>
<Badge colorScheme="blue" borderRadius="full">
{rocket?.rocket_name}
</Badge>
</HStack>
<Text>{details}</Text>
<HStack spacing={2}>
<Link href={links.video_link} isExternal color="blue">
video
</Link>
<Link href={links.article_link} isExternal color="blue">
article
</Link>
</HStack>
</Flex>
</Flex>
);
})}
</VStack>
);
}
以上で実装終わりで、aleph dev
で実行してみると、きちんとスタイリングされた状態でUIが表示されました。素晴らしい。
余談2
実は年末に何度かchakra-ui
の適用にチャレンジしていたのですが、断念していました。その時は、当時の最新 esm.sh v58
のchakra-ui
を使っていたのですが、どうやってもうまくいかずでした。
Aleph.js
にはPluginという機能があり、Aleph.js
の各ライフサイクルのタイミングで処理をHookすることができるので、chakra-ui
用のPluginを書けばうまくいくのかも、なんてぼんやりと思っていたのですが、今年に入ってesm.sh
のv61
が出たのでそちらで改めてトライしたら、見事動くようになっていました。
Denoでesm.sh
やskypack
経由のモジュールを使ってエラーが発生した場合は、まずそれらCDNのIssueを確認してみたり、該当するものがなければIssueを登録するなどしていくのが良さそうですね。
また、使うCDNによっても結果は違ってきたりするので、諦めずに別のCDNからインポートしてみると良さそうです(ちなみに年末はskypack
からのインポートも試しましたがダメでした、そういうこともありますよね)。
おわりに
今回やってみて、urql
とchakra-ui
が割とすんなり使えることがわかりました。特に言及していませんでしたが、ビルドも速くHMRなども効いており、そこまでストレスなく開発できる感触を得ました。
今回の検証Repoは以下のリンクから見れますので、興味ある方は覗いてみてください。 https://github.com/tkiryu/evaluate-aleph
ところで、Aleph.js
の将来性はどうなんでしょうか?
実は、GitHub の最終コミットが 20 Oct 2021
となっており、3ヶ月近く更新がない状態です。開発がアクティブでなければ安心して使っていくのは難しいところですが、どうやらリデザイン中のようで、GitHub Issue でコメントされていました。
https://github.com/alephjs/aleph.js/issues/429#issuecomment-967794820
at alephjs side, i decided to re-design the framework, the new system will be powdered by wasm that can run any edge network, for example deno deploy, and it will support any UI frameworks like react/vue/sevlte... i almost finish the compiler layer MVP, will publish it soon.
https://github.com/alephjs/aleph.js/issues/409#issuecomment-979803656
i am redesigning the framework to support deno deploy, in fact it will support any edge worker for example cloudflear
今後大きく変わる可能性があるため、今すぐ実践投入するのはやめておいたほうがよさそうですが、個人的には今後の動向に注目していきたいフレームワークです。何かアップデートがあれば、またブログにしたためようかと思います。
今回は以上です。