はじめに
Tauriが1.0版リリースされた、RustのGUI層として学習する価値があると思うので、学習ノートも残す。
作るアプリ
使用する技術
Tauri, Vite, Vuejs3, Tailwind Css
Typescript, Rust, WebAssembly
こんくらいかな、サーバー想定しないから、nuxtjsなどは使わない
まずは環境準備しよう
Rustとツールチェーンをインストール
// Mac
xcode-select --install
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
- LinuxとWindowsはリンク先を参照
https://tauri.app/v1/guides/getting-started/prerequisites/
以降の説明も、Mac環境を想定する。
初めてのTauriアプリ
# yarnを使う場合
yarn create tauri-app
# npmを使う場合
npm create tauri-app
# npxを使う場合
npx create-tauri-app
本記事では、初期化パラメータはviteとvue-tsを選択する。
詳細は画像を参照
初期化されたプロジェクトのソース構造
tauri-app
├─ node_modules
├─ public
├─ src # vueソース
├─ src-tauri # Tauriソース
│ ├─ icons # アプリケーションのアイコン
│ ├─ src # Rustソース。タスクトレーをはじめ、アプリの各種動作を定義
│ ├─ target # コンパイル成果物
│ ├─ build.rs # Tauri ビルドソース
│ ├─ Cargo.lock # Rustの依存管理Cargoのロックファイル
│ ├─ Cargo.toml # Rustの依存管理Cargoの設定ファイル
│ └─ tauri.conf.json # Tauriアプリケーションの設定ファイル。画面サイズや要求権限など
├─ index.html # アプリケーションのメイン画面
├─ package.json
├─ tsconfig.json
├─ vite.config.ts
├─ yarn.lock
└─ ...
既存のプロジェクトにTauriを追加するには
# yarnを使う場合
yarn add -D @tauri-apps/cli
yarn tauri init
# npmを使う場合
npm install --save-dev @tauri-apps/cli
npm tauri init
開発モードのアプリを起動
# viteのみ起動、従来のJS/TSプロジェクトと類似し、devサーバーが立ち上げ、ブラウザで動作確認
yarn dev
# vite + tauriを起動、初回はライブラリをフルビルドので時間がかかるが、次回以降は差分ビルドとなるため速くなる。
# また、スタンドアロンのアプリケーションが立ち上げ、動作確認ができる。
yarn tauri dev
WebAssemblyを組み込み
Tauriには、tauri::commandを使ってJS/TS側からRust側の処理を呼び出す仕組みがある。
それとは別に、WebAssemblyを組み込み方法もある。
WebAssemblyの組み込みはTauriがなくとも、一般なJS/TSプロジェクトでも組み込めるが手順は少し違う。
Rust側
rsw-rsをインストール
cargo install rsw
JS/TS側
vite-plugin-rswをインストール
yarn add -D vite-plugin-rsw
vite.config.jsを変更
import { defineConfig } from 'vite'
import ViteRsw from 'vite-plugin-rsw'; // 追加
export default defineConfig({
plugins: [
ViteRsw(), // 追加
],
})
rsw用のコマンドも追加
{
"scripts": {
"dev": "vite",
"build": "rsw build && tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"rsw": "rsw" // 追加
}
}
rswの設定ファイルを初期化する
yarn rsw init
すると、rsw.tomlファイルが生成されるはず。
Webassemblyのプロジェクトを生成
yarn rsw new rsw-hello
rsw-helloフォルダが作成されたら、先ほど生成されたrsw.tomlを更新する。
# link type: npm | yarn | pnpm, default is `npm`
cli = "npm"
[[crates]] // ここからをrsw.tomlの最後に追加
name = "rsw-hello"
link = true
以上で、WebAssemblyを組み込み設定が完了する。
実際にアプリを作ってみる
上記で生成されたテンプレートに、Google MeetsのようなLiveChatアプリを作ってみる。
サンプルのソースコード
必要なライブラリを追加
yarn add peerjs
画面ファイルを追加
会議作成・会議参加画面
// src/views/Top.vue
<script setup lang="ts">
import { createMeeting, joinMeeting, state } from '../store'
</script>
<template>
<div class="meet">
<button @click="createMeeting">
Start a Meeting
</button>
<div class="note">
The Host who started a meeting can't leave (including reload the page), or the meeting ID will be invalidated
</div>
<p class="dividebar"></p>
<p></p>
<input type="text" v-model="state.meetingId">
<p></p>
<button @click="joinMeeting">
Join a Meeting
</button>
</div>
</template>
<style scoped>
.meet {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
input {
width: 600px;
height: 40px;
border-radius: 10px;
}
button {
width: 200px;
height: 50px;
font-size: 20px;
border: solid 1px black;
}
.dividebar {
border: solid 0.2rem #5e104a;
width: 700px;
border-radius: 50px;
}
</style>
会議画面
// src/views/Meet.vue
<script setup lang="ts">
import { state, localStream, remoteStream } from '../store'
</script>
<template>
Meeting ID: <strong>{{ state.meetingId }}</strong>
<p></p>
<div class="camera-box">
<video :srcObject="localStream" autoplay muted></video>
<video v-show="remoteStream" :srcObject="remoteStream" autoplay></video>
</div>
</template>
<style scoped>
.camera-box {
display: flex;
flex-direction: row;
align-items: center;
}
video {
width: 100%;
min-width: 30vw;
border: 1px solid #000;
}
</style>
App画面
// src/App.vue
<script setup lang="ts">
import Top from './views/Top.vue'
import Meet from './views/Meet.vue'
import { meetingId } from './store'
</script>
<template>
<!-- <peer></peer> -->
<div v-show="!meetingId">
<top></top>
</div>
<div v-show="meetingId">
<meet></meet>
</div>
</template>
<style scoped>
</style>
モデルファイルを追加、Peer2Peerの接続管理
// src/store.ts
import { reactive, ref } from 'vue'
import type { Ref } from 'vue'
import { Peer } from "peerjs";
export const state = reactive({
meetingId: ''
})
export const uid = crypto.randomUUID()
export const meetingId = ref('')
/// <reference types="webrtc" />
const navigator = window.navigator;
const getUserMedia = navigator.getUserMedia ?? navigator.webkitGetUserMedia ?? navigator.mozGetUserMedia;
export const localStream: Ref<MediaStream|undefined> = ref(undefined)
export const remoteStream: Ref<MediaStream|undefined> = ref(undefined)
let peer = new Peer(uid)
const connectMeeting = (meetingId: string) => {
const call = peer.call(meetingId, localStream.value!)
call.on("stream", function(stream: MediaStream) {
remoteStream.value = stream
})
const conn = peer.connect(meetingId);
conn.on('open', function(){
conn.send({ uid: uid });
});
}
peer.on('call', function(call) {
call.answer(localStream.value)
call.on('stream', function(stream) {
remoteStream.value = stream
});
});
peer.on('connection', function(conn) {
conn.on('data', function(data){
console.log("ondata", data);
});
});
export const createMeeting = () => {
state.meetingId = uid
joinMeeting()
}
export const joinMeeting = async () => {
if (!state.meetingId) {
return;
}
if (meetingId.value !== state.meetingId) {
meetingId.value = state.meetingId
}
localStream.value = await initStream()
if (uid !== state.meetingId) {
connectMeeting(state.meetingId)
}
}
const initStream = async (): Promise<MediaStream> => {
return new Promise((resolve, reject) => {
getUserMedia({ video: true, audio: true }, resolve, reject)
})
}
最後にビルドする
yarn tauri build
ビルドが終わると、実行ファイルが作成され、そのまま実行することができる。