1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Tauriでデスクトップアプリを作ってみる

Last updated at Posted at 2022-08-19

はじめに

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

以降の説明も、Mac環境を想定する。

初めてのTauriアプリ

# yarnを使う場合
yarn create tauri-app

# npmを使う場合
npm create tauri-app

# npxを使う場合
npx create-tauri-app

本記事では、初期化パラメータはviteとvue-tsを選択する。
詳細は画像を参照
スクリーンショット 2022-08-01 15.27.25.png

初期化されたプロジェクトのソース構造

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

ビルドが終わると、実行ファイルが作成され、そのまま実行することができる。

スクリーンショット 2022-08-18 13.23.18.png

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?