フューチャー技術ブログ

Deno × Vueを触ってみた(2024年冬)

本記事はVue.js連載5本目の記事です。

はじめに

HealthCare Innovation Group(HIG)所属の山本です。

Vue.jsは素晴らしいフロントエンドのフレームワークとして、世界中で広く使用されています。公式ドキュメントでも非常に整っておりインストール手順やチュートリアルについて記載されていますが、Node.jsを利用することが前提となっているものが多いです。

実際に私が関わったVue.jsを使用したプロジェクトでも、Node.jsおよびnpmを使用しており、かなり一般的な選択肢ではないかと感じています。

そんな中、Node.jsに替わるJavaScriptランタイムとして新たに注目を集めているのがDenoです。2024年にはDeno v2がリリースされ、フォーマットやテストなどなどのDenoの機能の改善以外にも、Node.jsおよびnpmとの下位互換性を取り入れたことがとても大きな変更としてあります。

この記事では、Deno v2とVue.jsを組み合わせた公式チュートリアルの実施を通して、2024年冬時点での現状や特徴について解説していきます。

Denoについて

JavaScript実行環境のNode.jsの開発者であるRyan Dahlが2018年に発表した、JavaScript/TypeScript実行環境がDenoです。

Node.jsの設計上の問題点を解決するために0から作り直されたことが特徴です。

実際に、JSConf 2018にて「Node.jsに関する10の反省点」としてRyan Dahl自身が登壇し、以下のようなことを述べています。

https://www.youtube.com/watch?v=M3BM9TB-8yA

  1. Promiseを使い続けなかったため、Nodeの非同期APIに課題があること
  2. セキュリティ設計について。例えばlinterのnetworkフルアクセスなど
  3. ビルドシステム(GYP)を継続使用したこと
  4. package.jsonの存在、肥大化について
  5. node_modulesの存在、モジュール解決アルゴリズムの複雑化について
  6. require(“module”)で.jsの拡張子なしで読み込み可能としたこと
  7. index.jsの存在、モジュール読み込みが複雑化したこと

これらの設計に関する課題について述べたあと、Ryan Dahlはまだまだプロダクトレベルであることを前置きしたうえで、新しく開発したDenoを紹介しています。

そこからときが経ち2024年、Deno v2のリリースが発表されました。

https://deno.com/blog/v2.0

Denoの設計思想に基づき、ネイティブのTypeScriptのサポート、Promiseなどのサポート、組み込みのフォーマッタや型チェック、セキュリティの考慮などが特徴である他、v2からはNode.jsおよびnpmの下位互換性が追加されています。

Deno × Vue.jsについて

ここからは、実際にDeno × Vue.jsでアプリ構築を実際に試していきます。
記事執筆時点では、公式ドキュメントのチュートリアルとして以下があるので、その手順に従っていきます。

https://docs.deno.com/runtime/tutorials/how_to_with_npm/vue/

1. インストール

Deno自体のインストールについては、下記のドキュメントに従います。

https://docs.deno.com/runtime/getting_started/installation/

curl -fsSL https://deno.land/install.sh | sh

シェルスクリプトの一発でインストールできてお手軽ですね。記事執筆時点では、v2.1.2がインストールされました。

Denoのインストール後、以下のコマンドでプロジェクトのセットアップを進めます。

deno run -A npm:create-vite

フレームワークの選択が求められるので、Vueを選択します。

❯ deno run -A npm:create-vite
✔ Project name: … vite-project
? Select a framework: › - Use arrow-keys. Return to submit.
Vanilla
❯ Vue
React
Preact
Lit
Svelte
Solid
Qwik
Angular
Others

こちらも選択が求められますが、せっかくなので、DenoのネイティブのTypeScriptのサポートを確認するために、TypeScriptを選択します。

TypeScriptがネイティブでサポートされているため、トランスパイル設定などの手間が不要なのが嬉しいポイントです。

❯ deno run -A npm:create-vite
✔ Project name: … vite-project
✔ Select a framework: › Vue
? Select a variant: › - Use arrow-keys. Return to submit.
❯ TypeScript
JavaScript
Customize with create-vue ↗
Nuxt ↗

次に、プロジェクトフォルダに移動して依存関係のインストールを行います。

deno install

これでインストールは完了です。deno installの実行時点で、node_modulesフォルダが作成されます。
フォルダの内容としては以下のようになっています。

node_modules/
├── typescript -> .deno/typescript@5.6.3/node_modules/typescript
├── vite -> .deno/vite@6.0.1/node_modules/vite
├── @vitejs
│   └── plugin-vue -> ../.deno/@vitejs+plugin-vue@5.2.1/node_modules/@vitejs/plugin-vue
├── vue -> .deno/vue@3.5.13/node_modules/vue
└── vue-tsc -> .deno/vue-tsc@2.1.10/node_modules/vue-tsc

npm installなどのコマンド実行を行うことなく、npmレジストリよりパッケージをインストールすることができていますね。

ここまででテンプレートは作成できており、以下のコマンドでアプリ起動ができます。

deno task dev

2. バックエンドの追加

ここからもチュートリアあるに従っていきます。以下コマンドで、バックエンドに必要なものを追加していきます。

deno add jsr:@oak/oak jsr:@tajpouria/cors

構成としては、プロジェクト配下にapiフォルダを作成して、以下2つのファイルを作成します。

  • main.ts
  • data.json
main.ts
import { Application, Router } from "@oak/oak";
import { oakCors } from "@tajpouria/cors";
import data from "./data.json" with { type: "json" };

const router = new Router();

router
.get("/", (context) => {
context.response.body = "Welcome to dinosaur API!";
})
.get("/dinosaurs", (context) => {
context.response.body = data;
})
.get("/dinosaurs/:dinosaur", (context) => {
if (!context?.params?.dinosaur) {
context.response.body = "No dinosaur name provided.";
}

const dinosaur = data.find((item) =>
item.name.toLowerCase() === context.params.dinosaur.toLowerCase()
);

context.response.body = dinosaur ? dinosaur : "No dinosaur found.";
});

const app = new Application();
app.use(oakCors());
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

サンプルの量が多かったので一部抜粋しますが、以下のようなjsonファイルを作成します。

data.json
[
{
"name": "Aardonyx",
"description": "An early stage in the evolution of sauropods."
},
{
"name": "Abelisaurus",
"description": "\"Abel's lizard\" has been reconstructed from a single skull."
}
]

これで恐竜のデータを取得するバックエンドapiの構築は完了です。以下のコマンドでサーバーを起動することができます。

deno run --allow-env --allow-net api/main.ts

起動できると、例えばlocalhost:8000/dinosaurs/Aardonyxへのアクセスで以下のようなレスポンスが帰ってくることが確認できます。

{name: "Aardonyx", description: "An early stage in the evolution of sauropods."}

ここまでで無事にバックエンドサーバーの構築・起動までチュートリアルで確認できました。


先程実行した、起動コマンドについてですが起動時に与えている引数がDenoの大きな特徴の一つです。

デフォルトではホストマシンまたはネットワークへのアクセス権限は最小限にする思想ですね。

これらの明示的に与えられるPermissionについては以下のようなものがあります。

  • –allow-read : ファイルの読み取りアクセス
  • –allow-write : ファイルシステムの書き込みアクセス
  • –allow-net : ネットワークアクセス
  • –allow-env : 環境変数へのアクセス
  • –allow-run : サブプロセスの実行
deno run --allow-env --allow-net api/main.ts

例えば上記のコマンドでは、--allow-envでは環境変数へのアクセス権、--allow-netではネットワークアクセスの許可をapi/main.tsに与えるように指定しています。

試しにネットワークアクセスを許可しないまま起動しようとすると、以下のようにエラーが発生します。

❯ deno run api/main.ts
Permission flags have likely been incorrectly set after the script argument.
To grant permissions, set them before the script argument. For example:
deno run --allow-read=. main.js
❌ Denied net access to "0.0.0.0:8000".
error: Uncaught (in promise) NotCapable: Requires net access to "0.0.0.0:8000", run again with the --allow-net flag
this.#httpServer = serve?.({
^
at listen (ext:deno_net/01_net.js:504:35)
at Object.serve (ext:deno_http/00_serve.ts:555:16)
at Object.start (https://jsr.io/@oak/oak/17.1.3/http_server_native.ts:84:28)
at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:1105:16)
at startAlgorithm (ext:deno_web/06_streams.js:3661:14)
at setUpReadableStreamDefaultController (ext:deno_web/06_streams.js:3625:23)
at setUpReadableStreamDefaultControllerFromUnderlyingSource (ext:deno_web/06_streams.js:3691:3)
at new ReadableStream (ext:deno_web/06_streams.js:5160:7)
at Server.listen (https://jsr.io/@oak/oak/17.1.3/http_server_native.ts:82:20)
at Application.listen (https://jsr.io/@oak/oak/17.1.3/application.ts:840:35)

別の例としては、先程の実装ではimportでdata.jsonを読み込んでいたため、--allow-readの権限許可は必要としていませんでした。

以下のように読み込み部分を変更してみましょう。

- import data from "./data.json" with { type: "json" };

+ let data: Array<{ name: string; description: string }> = [];

+ // ファイルシステムからJSONデータを読み込む関数
+ async function loadData() {
+ const rawData = await Deno.readTextFile("./api/data.json");
+ data = JSON.parse(rawData);
+ }

+ // サーバー起動時にデータをロード
+ await loadData();

再度同様にサーバー起動をしてみようとしましたが、以下のエラーにより弾かれてしまいました。

❯  deno run --allow-env --allow-net api/main.ts
❌ Denied read access to "/home/penryu/dev/projects/deno-vue/vite-project/api/data.json".
error: Uncaught (in promise) NotCapable: Requires read access to "./api/data.json", run again with the --allow-read flag
const rawData = await Deno.readTextFile("./api/data.json");
^
at Object.readTextFile (ext:deno_fs/30_fs.js:779:24)
at loadData (file:///home/penryu/dev/projects/deno-vue/vite-project/api/main.ts:8:32)
at file:///home/penryu/dev/projects/deno-vue/vite-project/api/main.ts:13:7

このように、デフォルトで安全性が確保されているのは嬉しいポイントですね。

3. フロントエンドの構築

Vue Routerモジュールを以下のように追加します。

deno add npm:vue-router

チュートリアルでは、このあといくつかのファイルを実装することになります。対象としては、以下の7ファイルです。

├── src
│   ├── App.vue
│   ├── components
│   │   ├── Dinosaurs.vue
│   │   ├── Dinosaur.vue
│   │   └── HomePage.vue
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   ├── type.d.ts

実際の内容の紹介です。 main.tsとindex.tsでは以下のようにルーターを実装しています。

main.ts
import { createApp } from "vue";
import router from "./router/index.ts";

import "./style.css";
import App from "./App.vue";

createApp(App)
.use(router)
.mount("#app");
router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import HomePage from "../components/HomePage.vue";
import Dinosaur from "../components/Dinosaur.vue";

export default createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/",
name: "Home",
component: HomePage,
},
{
path: "/:dinosaur",
name: "Dinosaur",
component: Dinosaur,
props: true,
},
],
});

App.vueの内容について、以下のようにします。

<template>
<RouterView />
</template>

あとはコンポーネントたちを以下のように実装します。

HomePage.vue
<script setup lang="ts">
import Dinosaurs from './Dinosaurs.vue';
</script>
<template>
<h1>Welcome to the Dinosaur App! 🦕</h1>
<p>Click on a dinosaur to learn more about them</p>
<Suspense>
<template #default>
<Dinosaurs />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Dinosaur.vue
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
props: { dinosaur: String },
data(): ComponentData {
return {
dinosaurDetails: null
};
},
async mounted() {
const res = await fetch(`http://localhost:8000/dinosaurs/${this.dinosaur}`);
this.dinosaurDetails = await res.json();
}
});
</script>

<template>
<h1>{{ dinosaurDetails?.name }}</h1>
<p>{{ dinosaurDetails?.description }}</p>
<RouterLink to="/">🠠 Back to all dinosaurs</RouterLink>
</template>
Dinosaurs.vue
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
async setup() {
const res = await fetch("http://localhost:8000/dinosaurs")
const dinosaurs = await res.json() as Dinosaur[];
return { dinosaurs };
}
});
</script>

<template>
<div v-for="dinosaur in dinosaurs" :key="dinosaur.name">
<RouterLink :to="{ name: 'Dinosaur', params: { dinosaur: `${dinosaur.name.toLowerCase()}` } }" >
{{ dinosaur.name }}
</RouterLink>
</div>
</template>

ここまででフロントエンド側の実装も完了です。以下のコマンドにて、フロントエンド側のサーバーが起動できます。

deno run -A npm:vite
image.png

これでチュートリアルは完了です!


今回のチュートリアルでは、Vueの構築についてはNode.jsおよびnpmの下位互換性を活用する形で行われていました。

npmを用いない形で利用できる候補として、以下のリポジトリでは「vno」としてライブラリ開発がされていましたが、3年前ほどでコミットが途絶えているようです。。

https://github.com/open-source-labs/vno

まとめ

この記事では、Deno v2の簡単な紹介と、Vue.jsアプリの構築のチュートリアルを触ってみました。

TypeScriptのネイティブサポートや、セキュリティの概念など優れた特徴があり、Node.jsおよびnpmとの下位互換性についてもサポートされるものが増えてきています。

そのため、TypeScriptを使用したアプリケーション開発には優れた体験ができそうだと感じました。また、フロントエンドについても本体機能・ライブラリの様々な開発が進んでいるようです!

チュートリアル以上のアプリ構築についても、試してみたいと感じた2024年の冬でした。

参考文献