用笨办法应对充满不确定性的未来

Tauri 从零到一:使用 Tauri 开发一个 ChatGPT 工具

2023.07.13

前言

Tauri 是一个应用程序构建工具包,可以使用 Web 技术为所有主要的桌面操作系统构建软件。

本文的目的是介绍关于上架一个 Tauri 应用所必须的那部分知识,并且尝试介绍一下踩过的坑。

首先我对 Tauri 的理解是:

它是一个值得使用,但是并非万金油的一个跨平台应用开发框架,需要选择合适的应用场景,并且不可避免的会踩许多坑。

为了介绍使用 Tauri 开发的必备知识同时展现它的优势和不足。这篇文章会动手创建一个由 LangChain 和 OpenAI 驱动的的智能 App 。

以下是本次的行动概要。

本文的结构图

本文的结构图

产品设计

Tauri 的优势

以下是官网列出的优势

  • 安全
  • 体积小
  • 高性能

首先是安装包体积小,相比 Electron 安装包动辄上百兆,使用 Tauri 开发的应用安装包最小可以做到不到 10MB。

在 macOS 上运行时使用的是 跟 Safari 同源的 Webkit 内核,占用的内存也比较小。

  • 产出的包非常小

适合的场景

如果符合以下条件,那么会比较适合使用 Tauri,否则请直接左转 Electron 或者原生开发

  • 需要用比较低的成本来开发一个桌面端 App
  • 用户的电脑系统比较新,至少是近两年发布的版本。
  • 前端没有使用非常新的 API、语法,或者本身存在兼容问题的特性
  • 出问题了能够比较方便的找到对应机型或者有办法线下沟通

规划 MVP 功能

现在 ChatGPT 很流行,不如开发一个使用了 ChatGPT 的工具。做一个酷炫的 AI 工具

介绍:通过自然语言来生成一段查询表结构的 SQL,并能够直接将其复制到数据库查询工具中使用

价值

  • 历史记录等都保存到本地
  • 能够执行更复杂的命令
  • 能够读取本地文档进行对话

后续可拓展功能

  • 增加本地知识库
  • 接入命令行工具
  • 调用本地浏览器等能力

页面布局

我们计划将整个页面分为三块:用户输入、表结构设置、AI 输出。

用户输入和表结构设置都是文本输入框,AI 输出需要做格式化和代码高亮。

创建开发环境

本节首先快速的去创建一个工程,先跑起来

需要先准备好 NodeJs 环境和 Rust 环境。

Rust 安装:链接

在本地从零启动一个 Tauri 工程

创建工程

pnpm create tauri-app

选择前端技术栈

✔ Project name · tauri-quick-start
? Choose which language to use for your frontend ›
❯ TypeScript / JavaScript  (pnpm, yarn, npm)
  Rust

这里我们选择 TypeScript/JavaScript

最终结果如下:

✔ Project name · tauri-quick-start
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm)
✔ Choose your package manager · pnpm
✔ Choose your UI template · React - (https://reactjs.org/)
✔ Choose your UI flavor · TypeScript

查看一下目录结构

.
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
│   ├── tauri.svg
│   └── vite.svg
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── assets
│   ├── main.tsx
│   ├── styles.css
│   └── vite-env.d.ts
├── src-tauri
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── build.rs
│   ├── icons
│   ├── src
│   ├── target
│   └── tauri.conf.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

到这里是不是很眼熟,同样的包管理工具,同样的工程创建流程,最后生成的目录结构跟我们普通的前端工程非常相似。 只是多了一个 src-tauri 目录,内部其实是一个 rust 源码目录

然后启动:

  cd tauri-quick-start
  pnpm install
  pnpm tauri dev

会打开一个预览 APP:

第一个 tauri 工程界面

第一个 tauri 工程界面

在这个阶段我们完全可以不去深究 Rust 部分,先使用熟悉的 TypeScript/React 技术栈进行开发。

小结:我们这一次快速的启动了一个 Tauri 工程,观察了一下项目结构。下一步我们缓一缓来了解一下 Tauri 的优势,以及对我们的 AI 产品做 MVP 版本设计。

开发 MVP 版本

选择开发模式,前端优先 / Rust 优先

我们首先前端为主,Rust 为辅,把 Tauri 当作一个壳来使用。

前端界面开发

界面编写部分跟其他的 React 没有区别。不过既然是 MVP 版本,直接引入 AntD 快速写出来。

使用 LangChainJS 来对接 OpenAI

安装 LangChainJS

pnpm add langchain

在工程中引入 LangChain

import {ChatOpenAI} from "langchain/chat_models/openai";
import {HumanChatMessage, SystemChatMessage} from "langchain/schema";

export const chat = new ChatOpenAI({temperature: 0, openAIApiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'});

编写按钮事件处理函数

const handleRequest = async () => {
        const values = await form.validateFields().catch(e => e);

        const response = await chat.call([
            new SystemChatMessage(getSQLPrompt(values.tableStructure)),
            new HumanChatMessage(
                values.user
            ),
        ]);
        form.setFieldValue('assistant', response.text)
    }

这里简单写一个系统指令,让 AI 来给我们将自然语言编写成 SQL 查询语句:

const getSQLPrompt = (state: string) => `you are a sql statement generator machine, you receive a query statement, and reply standard and efficient sql statement.you should use table structure below:
------
${state}
------
`;

代码其他部分比较简单就不贴出来了

如何请求网络 API

在 Tauri 中,有两种方式可以发起网络请求:

  • 从 WebView 中发出
  • 从 Rust 中

使用 axios 即可

请求从 Rust 中发出

使用由 Rust 侧提供的 HTTP 客户端

import { getClient, ResponseType } from '@tauri-apps/api/http';
const client = await getClient();
const response = await client.get('http://localhost:3003/users', {
  timeout: 30,
  // the expected response type
  responseType: ResponseType.JSON
});

阶段小结

第一阶段编写出的 APP 截图

第一阶段编写出的 APP 截图

从数据库中把想查的表名和表结构捞出来,贴到我们开发的查询工具中

再用自然语言编写一个查询需求

结果出来了,生成了一段 SQL 语句

我们手动将 SQL 复制,粘贴到数据库中并执行查询

确实能出结果

目前我们实现了功能验证,大体的交互流程已经出来了,但是还缺少很多功能,例如图标、名称、甚至都没有打出正式包,目前还跑在开发模式。

打包发布

配置打包参数

在 package.json 中添加打包用的 script,最终结果如下:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "tauri": "tauri",
    "tauri-dev": "tauri dev",
    "tauri-build": "tauri build --target universal-apple-darwin"
  }
}

修改包名:

Error You must change the bundle identifier in `tauri.conf.json > tauri > bundle > identifier`.

设定好自己的包名后执行打包

pnpm tauri-build

最终打包文件在:

src-tauri/target/universal-apple-darwin/release/bundle

目录结构如下:

├── dmg
│   ├── bundle_dmg.sh
│   ├── icon.icns
│   ├── support
│   │   ├── eula-resources-template.xml
│   │   └── template.applescript
│   └── tauri-quick-start_0.0.0_universal.dmg
└── macos
    └── tauri-quick-start.app
        └── Contents
            ├── Info.plist
            ├── MacOS
            │   └── tauri-quick-start
            └── Resources
                └── icon.icns

8 directories, 8 files

架构类型:通用包、专用包

对于 macOS 来说有 Apple Silicon 和 Intel 两种芯片架构,可以选择打成同时支持两种芯片的通用包,也可以分开打包。

而由于在用户机器上始终只会运行一种架构的软件,打通用包的优点是方便开发者分发软件的,也让用户不需要选择,不同的架构的电脑都可以通过下载一份安装包来使用。

因此如果前期没有处理好自动构建和发布升级流程,则应该打通用包。多数情况下都应该按系统架构打专用包。

打包类型:发布包、debug 包、升级包

第一个迭代版本

回顾

现在我们拥有了一个能响应用户输入并且弹出结果的

从源码中移除 apiKey,增加配置项

在界面上增加一个输入框,并在“生成”按钮的事件处理函数中增加该输入框值的处理。跟常见的 Form 表单处理方式一样。

调整启动界面尺寸

在 tauri.config.json 添加配置

{
    "windows": [
      {
        "fullscreen": false,
        "height": 600,
        "resizable": true,
        "title": "Tauri App",
        "width": 800
      }
    ]
}

配置参数持久化

将配置写入到文件中,然后在应用启动时从该文件中读取

{
  "tauri": {
    "allowlist": {
      "fs": {
        "scope": ["$APP/databases/*"],
        "readFile": true,
        "writeFile": true,
        "createDir": true,
      }
    }
  }
}

然后写入文件

import { BaseDirectory, writeFile } from '@tauri-apps/api/fs';
await writeFile({ path: 'app.conf', contents: 'file contents' }, { dir: BaseDirectory.App });

读取文件

import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs';
// Read the text file in the `$APPCONFIG/app.conf` path
const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig });

复制生成的 SQL 到粘贴板

需要先申请权限,在 tauri.conf.json 中添加权限配置

{
  "tauri": {
    "allowlist": {
      "clipboard": {
        "all": true, // enable all Clipboard APIs
        "writeText": true,
        "readText": true
      }
    }
  }
}

向粘贴板写入内容

import { writeText, readText } from '@tauri-apps/api/clipboard';
await writeText('Tauri is awesome!');
assert(await readText(), 'Tauri is awesome!');

如何读取环境变量

之前将 openai 的 key 直接写在了代码中,这其实不是好的实践。一般会将 key 存到服务端,使用其他方式来鉴权。但是已经超出了本文的内容。

如何处理兼容问题

Tauri 需要开发者自己验证、处理不同系统上的兼容问题,因此强烈建议在应用上线前准备好以下排错方案

监控

最好配置上监控系统,自研或者开源的监控,比如 Sentry 等

真机

如果有报错的用户的辅助,那是最快的方式了,直接打一个调试包,打开控制台分析报错信息。这虽然比较 low 但是真的管用。

善用虚拟机

如果以上方法都不行,那可以根据上报的信息,自己新建对应版本系统的虚拟机,但是缺点是虚拟机通常性能存在问题,最好在大内存的机器上新建虚拟机,不然在电脑上一边要实时编译 tauri 项目还得在虚拟机内调试,开发体验会比较差。

小结

当前版本界面如下:

优化之后比较紧凑的界面

优化之后比较紧凑的界面

现在界面更加简洁紧凑,并且将 apiKey 从源码中移除,界面上增加了配置选项。原本是摆设的复制按钮现在也能够正常工作了。

这个版本才是完整的最小可用版本。

填写好相关字段,apiKey、表名以及表结构描述、想要的查询字段

填写好各个字段如查询需求后的界面

填写好各个字段如查询需求后的界面

点击生成按钮:

点击生成按钮后的界面

点击生成按钮后的界面

再复制到查询工具中查询就可以啦。目前看稳定性还不错

支持跨平台能力

在单一平台(macOS)上完成功能后,想让用 Windows 或者 Linux 的用户也能用上先进的 AI 科技的话,

就需要先测试一下在其他平台能否正常工作。

不仅仅要测试 macOS、Windows、Ubuntu 三个平台,还需要测试不同操作系统的旧版本。

这个工作量无疑是非常巨大的,每次改动都需要在各个平台验证一遍,如果光靠手动操作会把人累死的。

修改打包发布配置

修改打包命令

修改发布命令

搭建运行环境

如果是白嫖 Github Action 的话,使用 Tauri 官方的 workflow 配置能够极大的减少配置环境的工作量。

如果是自己用机器搭的话,耗费的时间成本就比较高了,想想想还是比较麻烦,这里只介绍一下思路。

想要达到 Github Action 的能力,要解决以下问题:

启动多个虚拟机,并逐个在多个虚拟机中执行打包构建命令

或者也可以配置 Github Action 的 SelfHost Runner,这样只需要启动多个虚拟机就行了。

简单的自动化测试

编写一个自动化测试脚本,直接打开 App 然后截图。对于我们的只有一个页面的简单工具型 App 来说,能够看出是否白屏、界面布局是否正常就好了。

更多的迭代

诚然现在 APP 的功能还很简陋,甚至都没有取一个正式的名字,也没有图标,但是它很好的展示了 Tauri 的开发流程

以下是一些还可以优化的功能点:

  • 支持读取本地文件功能,做一个类似 ChatFiles 的功能
  • 本地不保存 apiKey,而是通过独立的服务端与 AI 服务交互
  • 支持使用钉钉/微信扫码登录,GitLab /GitHub 登录授权
  • 常驻菜单栏、菜单栏小浮层
  • 多语种翻译功能
  • 置顶窗口
  • ……

总之还可以不断的打磨

总结

开发过程的难点

难点就是,没有难点,通过阅读文档、读 API 基本能够解决问题。原本比较复杂的 SQL 生成部分,也交给了 AI…

到目前为止我们仅仅是使用了 Tauri 封装的 API,没有碰到 Rust 代码,拿原生部分当作容器使用,因此一切都显得非常的自然。

跨平台的代价

  • 程序员需要面对不同平台的兼容问题
  • 上游问题难以解决
  • 多个平台需要测试

目前已知的不足以及需要注意的地方

  1. 兼容性。跟操作系统版本绑定,如果用户不愿意升级,而开发者又找不到办法去做兼容,基本完蛋。
  2. 排查问题。兼容问题基本靠同版本系统复现,因此第一版本就要做好监控。
  3. 自动升级功能比较简陋。一旦失效后容易降低用户信心,影响使用意愿

Tauri 的优势是否是真实存在的?

从两个方面来分析:

用户角度:

  • 安装包确实比较小:这意味着用户侧能够更快的拿到安装包,更快的更新版本。有时候快也是一种价值。
  • 性能更好:直接使用 Webkit 也比引入 Chrome 内核占用的内存更小。对日常需要开大量软件的用户来说,使用内存越小越好。
  • 安全性:有比较严密的权限控制,要使用某个权限、读取某个目录需要在配置文件中申请。但是要注意开发者也可以申请根目录访问、较大的权限。
  • 基于 Rust 提供的安全性、性能:目前没有大量编写 Rust,因此无法判断

开发者角度:

  • 开发效率:
    • 能够立即使用熟悉的技术栈,例如 React、Vue 等都可以直接上手,甚至以前的组件库等都能直接搬过来用。这里加分
    • 命令行工具比较好用,没有很多复杂的概念(当然也是因为比较新),比较好理解。
    • Rust 的编写效率。Rust 和 TypeScript 都是对类型比较严格的,偶尔会陷入到跟类型搏斗的情况,并且如果深入 Rust 侧,由于不熟悉,会导致效率变低。

总体上而言:Tauri 带来的价值更多体现在“更小、更快、更简单的”上,能够让前端工程师也能比较轻松愉快的编写桌面端应用。但是带来的潜在问题如兼容性则短期内不会暴露出来,特别是用在编写跨平台应用场景中,需要提前做好兼容性测试、以及对功能的取舍,关键是要准备好复现环境、做好监控。