2024.03.07
イベントの感想募集でGoogleフォーム使ってみたら便利だった
2024.02.26
RustRust+SvelteKitでデスクトップアプリケーションを作ってみた

こんにちは!普段はインフラエンジニアをやっているMです。この記事では、RUST+SvelteKitを使用してデスクトップアプリケーションを作成する過程を紹介します。
この記事も含めて、自分は記事の原稿作成はMarkdownで行っています(前回の記事も)。しかし、会社で使われているCMSで記事作成はリッチテキストエディタか、HTMLでの記述しかできず、Markdownをコピペしても、書式などをとりこんではくれません。
CMS側でMarkdownに対応するには、プラグインの導入が必要です。しかし、プラグインをインストールする申請を出すのが非常に面倒でした。
というわけで、自前でMarkdownからHTMLに変換するツールを作ってみることにしました。
● Tauri for SvelteKit のインストール
● SvelteKitのインストール
● SvelteKitでGUI作成
● RustでMarkdownをHTMLにパースする処理を追加する(ほぼライブラリのおかげ)
私の普段の業務はクラウド(99%AWS)の設計と設定がメインなので、フロントエンド技術はほぼ初体験(HTMLとCSSでダサくて動かない画面がかろうじてつくれたくらいのど初心者、DOMとJSはなんだかよくわかっていない)でした。
そのため、いきなりフロントエンドフレームワークのSvelteKitを勉強しはじめて、コンポーネント間のデータの受け渡しや、CSSで思った通りの画面実現する方法を探すのにほとんどの時間を取られました。
SvelteKit(およびSvelte)がNext.js(React)やNuxt(Vue)よりも新興のフレームワークということもあり、Qiitaなどにマネできる日本語記事が少ないというのも、時間がかかった一要因ですね。
↑クリックすると再生できます。
上のGifが今回作成するアプリの完成像です。
それでは、環境構築の手順を以下に示します。
0.Rustのインストール(未インストールの場合):
Rustの公式ウェブサイトからRustをダウンロードしてインストールします。詳細は公式サイトを参照してください
1.SvelteKitのインストール:
今回はpnpmを使用しました。(インストールされていない方は公式サイトを参照してインストールしてください。)
以下のコマンドを実行します。
|
1 2 |
pnpm create tauri-app --bata |
いくつかの質問に答えて、プロジェクトフォルダを作成しました。
|
1 2 3 4 5 6 7 8 |
✔ Project name · md2html ✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun) ✔ Choose your package manager · pnpm ✔ Choose your UI template · Svelte - (https://svelte.dev/) ✔ Choose your UI flavor · TypeScript ? Would you like to setup the project for mobile as well? (y/n) › n ✔ Would you like to setup the project for mobile as well? · |
プロジェクトの作成が完了すると以下のコマンドを実行する指示が表示されます。
|
1 2 3 4 5 |
Template created! To get started run: cd md2html pnpm install pnpm tauri dev |
実行すると以下のようなウィンドウが起動するでしょう。
これで環境構築は完了です。
実際にデスクトップアプリケーションの開発を始める準備が整いました。
それでは、今回実際に実装したコードを参照しながら解説していこうと思います
1.RustでMarkdownをHTMLにパースする処理を追加する:
Rustのパッケージ管理ツールであるCargoを使用して、MarkdownをHTMLにパースする処理を追加します。具体的な手順は以下の通りです。
Cargo.tomlファイルに以下の依存関係を追加します。
■ Cargo.toml
|
1 2 3 4 |
[dependencies] pulldown-cmark = "0.8.1" \ |
ソースコードに以下の処理を追加します。
■ main.rs
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use pulldown_cmark::{Parser, html}; fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![exchenge_2_html]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } #[tauri::command] fn exchenge_2_html(value: &str) -> String { let parser = Parser::new(&value); let mut html_output = String::new(); html::push_html(&mut html_output, parser); html_output } |
フロントエンドのSvelteKitからRustで実装した処理を呼び出すAPIとして登録しているのが以下のコードです。
■ main.rs
|
1 2 3 4 5 6 7 |
fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![exchenge_2_html]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } |
.invoke_handler(tauri::generate_handler![exchenge_2_html]) この部分で関数をコマンドとして登録しています。
コマンドとして登録する関数には#[tauri::command] のアノテーション(注釈)をつけます。今回のコードでは以下の通りです。
■ main.rs
|
1 2 3 4 5 6 7 8 9 |
#[tauri::command] fn exchenge_2_html(value: &str) -> String { let parser = Parser::new(&value); let mut html_output = String::new(); html::push_html(&mut html_output, parser); html_output } |
関数の引数には後述しますが、svelteのコードから関数を呼びだす際に指定する引数名と同一名の変数名を指定します。
2.SvelteKitでGUIを構築する:
SvelteKitでは/<projectfolder>/src/ に表示するページなどを構築していきます。今回は以下のようなフォルダ構造です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
src │ app.d.ts │ app.html │ ├─lib │ Botton.svelte │ Editor.svelte │ index.ts │ └─routes +layout.ts +page.svelte |
lib 配下にコンポーネント。
routes 配下に表示するページという構成になっています。
routes直下の+page.svelte がアプリ起動時に表示されます。
+page.svelte の中身はこのようになっています。
■ +page.svelte
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
<script context="module"> import Botton from "$lib/Botton.svelte"; import Editor from "$lib/Editor.svelte"; </script> <script lang="ts"> let editor :string let exchenged_txt :string </script> <div class="container"> <div class='controls'> <Botton value={editor} bind:MSG={exchenged_txt}/> </div> <div class='main-pane'> <div class="input-area"> <label for="input">Markdown input area</label> <Editor bind:value = {editor} /> </div> <div class="output-area"> <label for="output">Exchenged HTML</label> <pre>{exchenged_txt}</pre> </div> </div> </div> <style> .container{ display: flex; flex-direction: column; height: 98vh; } .controls{ flex: 1; display: flex; justify-content: center; align-items: center; } .main-pane{ flex: 9; display: flex; } .main-pane > div { height: 100%; } .input-area, .output-area{ flex: 5; width: 40vw; border: 0px; } .output-area > pre { background-color: rgb(229, 229, 229); border: 8px; border-color: black; height: 97.5%; width: 98%; margin-top: 0%; padding: 1em; } label { margin: 0%; padding-left: 10px; padding-right: 10px; } .input-area > label { background-color: rgb(244, 236, 225); } .output-area > label { background-color: rgb(229, 229, 229); } </style> |
このページでは受け渡し用の変数の定義とコンポーネントの読み込み、ページ全体のスタイリングを行っています。
エディタへの書き込みと内容の読み取り、変換コマンドの呼び出しはコンポーネントの方でやっています。
各コンポーネントのコードは以下のようになっています。
■ Bottorn.svelte
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<script context="module"> import { invoke } from '@tauri-apps/api/tauri'; import { writeText } from '@tauri-apps/api/clipboard'; </script> <script lang="ts"> export let value :string; export let MSG :string = ''; let x :string = "Copy to clipboard"; async function button_click_action(){ MSG = await invoke('exchenge_2_html', { value }) } async function button_click_action_copy() { await writeText(MSG).then(() => { x = "COPYED!"; }).catch(() => { alert("コピーに失敗しました。"); }).finally(() =>{ setTimeout(() => { x = "Copy to clipboard"; },1000); }) } </script> <button class="exchenge_button" on:click={button_click_action}> MD ⇒ HTML </button> <button class="copy_button" on:click={button_click_action_copy}> {x} </button> <style> .exchenge_button, .copy_button{ width: 120px; height: 40px; border-radius: 10px; color: rgb(38, 72, 101); margin: 1vw; } </style> |
■ Editor.svelte
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<script context="module"> //import { onMount } from "svelte"; //import { createEditor, $getRoot as getRoot } from "lexical"; //import { registerPlainText } from "@lexical/plain-text"; </script> <script lang="ts"> //let editor_dom: HTMLElement; //onMount(() => { // const editor = createEditor(); // editor.setRootElement(editor_dom); // registerPlainText(editor); //}) // //let root = getRoot(); // Lexicalわからん export let value: string = " " </script> <!-- <div id="editor" contenteditable="true" bind:this={editor_dom} /> --> <textarea contenteditable="true" bind:value={value}></textarea> <style> textarea { color: black; height: 97%; width: 99%; background-color: rgb(244, 236, 225); border: 0px; padding: 1em; } </style> |
Lixicalでエディターを作ろうとして挫折した形跡がありますね 😅
index.tsは初期に作成されるライブラリー用のファイルです。
Tauriの場合は、ロジックをRustに持たせるので、あまり用はないですが、$lib でライブラリを読み込むことができます。
上記に加えて、ボタンで使用しているTauri API(クリップボードCopy とAlert)を許可するために、src-tauri\tauri.conf.json に以下のオプションを追加しています。
■ tauri.conf.json
|
1 2 3 4 5 6 7 8 |
"tauri": { "allowlist": { "dialog":{ "message": true }, "clipboard":{ "writeText": true } |
以上で実装は完了です。
viteによってホットロードに対応しているので、GUIの変更はファイルを保存すれば自動で変更されてきたかと思います。
アプリを終了していた方は以下を実行してください。
|
1 |
pnpm tauri dev |
最終的にこのような画面のアプリケーションが完成すると思います。
お疲れ様でした!
これでMarkdownをHTMLに変換するGUIアプリの完成です。
ぶっちゃけ一番時間がかかったのはGUIの部分でした。このデザインで力尽きるまで10時間くらいはいじっていたと思います。
デザインセンスがほしい……。
それはともかく、この記事が皆様のTauriとRust、あとSvelteKitエンジニア生に役に立つことを願って、長話を終わりにしようと思います。
次回があれば、作ったアプリをWindowsのほかの環境でも動かせる実行ファイルにビルドして、インストーラーにパッケージングする記事を書くつもりです。
以上、Mでした。
【記事への感想募集中!】
記事への感想・ご意見がありましたら、ぜひフォームからご投稿ください!【テクノデジタルではエンジニア/デザイナーを積極採用中です!】
下記項目に1つでも当てはまる方は是非、詳細ページへ!Qangaroo(カンガルー)
【テクノデジタルのインフラサービス】
当社では、多数のサービスの開発実績を活かし、
アプリケーションのパフォーマンスを最大限に引き出すインフラ設計・構築を行います。
AWSなどへのクラウド移行、既存インフラの監視・運用保守も承りますので、ぜひご相談ください。
詳細は下記ページをご覧ください。
最近の記事
タグ検索
SNS共有