TAURI 是什么
TAURI 是一个使用 Rust 编写的程序框架,它允许我们使用 Web 技术和 Rust 语言构建跨端应用。它提供了大量特性,例如系统通知、网络请求、全局快捷键、本地文件处理等,它们都可以在前端通过 JavaScript 便捷的调用。
TAURI 应用的后端基于 Rust,这是一种内存安全、性能出色、跨平台的系统级程序设计语言,它保证了 TAURI 应用的高效和安全性。TAURI 应用由系统的 WebView 进行用户界面的渲染,因此开发者可以使用流行的 Web 技术快速构建用户界面,并且可以有效的控制打包产物体积。
TAURI 当前已支持 macOS、Windows、Linux 平台,在即将到来的 2.0 版本中将会支持 iOS/iPadOS 和 Android。
TAURI 对比 Electron
TAURI 和 Election 都是基于 Web 技术构建跨平台应用的程序框架,但是 Electron 比 TAURI 诞生早了将近 6 年。
???? Github Star 对比:107k ???? 63k
Electron 基本可以归属于上个时代的产物,和 React 同年 2013 年面世,彼时还处于前端高速发展的初期,Angular 和 React 刚从 jQuery 中抢过来一小部分用户,Vue 还在胎中,webpack 刚发布还不足两年……
Electron 的诞生大大降低了桌面应用开发成本、维护难度,并且有 GitHub 和 Microsoft 巨头公司背书,多年来一直拥有活跃的技术社区,再加上 VS Code、Slack、Discord 这些知名 App 的流行,让更多的人加入了蓬勃发展的社区。
庞大的社区带来了丰富的生态系统,这也是 TAURI 不及 Electron 最明显的方面。
下面是其他方面二者的对比:
- 渲染引擎:Electron 应用统一使用 Chromium,具有很好的兼容性和性能表现,但是也增加了打包产物体积,App 运行时所占内存也一直被诟病;TAURI 使用系统 WebView 作为渲染引擎,打包产物体积更小、运行所占内存更少,但是由于 WebView 的差异,TAURI App 兼容性相对薄弱。
- 后端技术:TAURI 后端基于 Rust,TAURI App 会使用更少的内存和 CPU 资源,性能更优,TAURI 提供了更好的集成方式,可以很方便的将 Rust 和其他后端语言结合使用;Electron 后端基于 平台,可以享受丰富的 生态,更容易上手开发后端服务。
- 支持的平台:因为渲染引擎的选择不同,Electron 只能支持 Windows/macOS/Linux,而 TAURI 不仅支持这些平台,还能支持 iOS/iPadOS/Android。
心动不如行动!现在就用 TAURI 开发一款跨平台的 ChatGPT 客户端!????
它有如下功能:
- 持久化本地保存对话记录
- 多页面支持
- 使用个人 API Key
- 配置 API Host 代理、Chat Model、对话风格
- 让 AI 理解上下文,并且可配置上下文消息数
- 指定 AI 人格,让 TA 成为编程大师、郭德纲、猫娘然后与你交流
- ……
当前项目已开源!文末给出该项目的 Github 代码仓库地址!
我从开始阅读 TAURI 官方文档,到开发完成这款 App,只用了 3 天时间。有了我的踩坑,你甚至可以 1 天内开发完成这款应用!
开始!????
创建项目
创建项目前,需要确保本地已安装 、Rust,然后使用你的 包管理工具(如 pnpm
)执行:
pnpm create tauri-app
在终端中,可以命名项目名称,选择包管理工具、JavaScript/TypeScript、前端框架。我这里选择的是 pnpm + TS + Vite + React。
项目目录结构:
-
root
-
├── public
-
├── src
-
├── src-tauri
-
├──
-
├── package.json
-
├──
-
├──
-
└── ...
基本的目录结构和一个标准的 Web 项目目录结构几乎一致,但是这里多了一个 src-tauri
目录,这是一个 Rust 项目的目录:
-
src-tauri
-
├──
-
├──
-
├── src
-
├── icons
-
└── ...
其中 需要特别关注,因为它是整个 TAURI App 的配置文件;
src-tauri/src
中可以写一些 Rust 代码, src-tauri/icons
是 App 的图标文件夹,存放了不同操作系统会用到的不同分辨率/格式的 App 图标资源,可以用 CLI tauri icon
自动生成 ????。
启动
安装依赖、启动项目:
-
pnpm i
-
pnpm tauri dev
执行后,会根据配置校验代码、编译前端代码、编译 Rust 代码,启动 App:
这是一个使用系统 WebView 渲染的用户界面,如果希望可以像开发传统 Web 项目一样,使用 Chrome 浏览器开发调试,只需要执行 pnpm vite
即可(假如选择的前端工具是 vite)。
注意:用浏览器开发时,系统原生能力是无法使用的,只有通过 tauri dev
启动打开的 App 才能调用系统原生能力。
多页面支持
让 TAURI App 支持多页面并非难事,常见的前端路由库都可以用在 TAURI 应用中实现多页面应用,这里我们选用 React Router 实现多页面。
pnpm add react-router-dom
当前安装的是 v6
版本(新特性巨多????)。
入口文件 没什么改动:
-
import React from 'react'
-
import ReactDOM from 'react-dom/client'
-
import App from './App'
-
-
(('root')).render(
-
<>
-
<App />
-
</>
-
)
在 中配置两个页面:
-
import Chat from '@/pages/Chat'
-
import More from '@/pages/More'
-
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
-
import Layout from './Layout'
-
-
// 页面多的话可以抽离出去组织一下
-
const router = createBrowserRouter([
-
{
-
path: '/',
-
element: <Layout />,
-
children: [
-
{ path: '/', element: <Chat /> },
-
{ path: '/more', element: <More /> }
-
],
-
},
-
])
-
-
export default function App() {
-
return (
-
<RouterProvider router={router} />
-
)
-
}
在 中使用
<Outlet />
指定页面组件渲染的位置:
-
import { Outlet } from 'react-router-dom'
-
import Header from '../Header'
-
-
export default function Layout() {
-
return (
-
<div>
-
<Header />
-
<main>
-
<Outlet />
-
</main>
-
</div>
-
)
-
}
通过页面顶部的 <Header>
导航组件看一下 React Router 其他一些用法:
-
import { Link, useLocation, useNavigate } from 'react-router-dom'
-
-
export default function Header() {
-
// 调用 navigate() 去你想去的地方 ⛱️
-
const navigate = useNavigate()
-
// 我在哪?
-
const location = useLocation()
-
const showBack = !== '/'
-
-
return (
-
<div className={}>
-
<div className={classNames(!showBack && 'invisible')} onClick={() => navigate('/')}>
-
<MaskIcon src={ICON_BACK} />
-
</div>
-
<div className={classNames(showBack && 'invisible')}>
-
{/* 相当于 HTML 中的 <a>,点击后跳转页面 */}
-
<Link to="/more" title="更多">
-
<MaskIcon src={ICON_MORE} />
-
</Link>
-
</div>
-
</div>
-
)
-
}
- 懒加载页面组件在 TAURI 应用里不是很刚需,因为打包后代码文件都在本地,加载速度足够快
- 大部分情况下,可以把只有一个 Window 的 TAURI App 视作 Web 中的单页面应用(SPA)
用户设置页面
页面功能说明
一个表单页面,点击 保存 后将用户的配置保存到本地文件中。
页面代码
-
import { getUserConfig, setUserConfig } from '@/utils/user-config'
-
import { dialog } from '@tauri-apps/api'
-
import { useMount, useSetState } from 'ahooks'
-
import { useNavigate } from 'react-router-dom'
-
import style from './'
-
-
export default function More() {
-
const navigate = useNavigate()
-
const [state, setState] = useSetState({
-
key: '',
-
// ...
-
})
-
-
// 页面加载时,读取本地配置文件,并设置 state
-
useMount(async () => {
-
const config = await getUserConfig()
-
setState({
-
key: ,
-
// ...
-
})
-
})
-
-
async function save() {
-
await setUserConfig(state)
-
// 调用系统原生的 dialog
-
await ('✅ 配置已保存')
-
navigate('/')
-
}
-
-
return (
-
<div className={}>
-
<form>
-
<div className={}>
-
<label>OpenAI Key</label>
-
<input value={} onChange={(e) => setState({ key: })} />
-
</div>
-
{/* 其他表单输入项... */}
-
<button onClick={save} type="button">
-
保存
-
</button>
-
</form>
-
</div>
-
)
-
}
读写用户配置工具函数
getUserConfig()
和 setUserConfig()
的具体实现:
-
import { UserConfig } from '@/types'
-
import { readTextFile, writeTextFile } from './file'
-
-
const CONFIG_FILE_NAME = ''
-
-
// 保存一个 JS 变量,以便前端获取配置时,不用每次都读文件
-
let userConfig: UserConfig | null = null
-
-
export async function getUserConfig() {
-
if (userConfig) return userConfig
-
-
const config = await readTextFile(CONFIG_FILE_NAME)
-
try {
-
userConfig = (config)
-
} catch (error) {
-
userConfig = DEFAULT_USER_CONFIG
-
}
-
-
return userConfig!
-
}
-
-
export async function setUserConfig(config: UserConfig) {
-
await writeTextFile({
-
path: CONFIG_FILE_NAME,
-
contents: (config),
-
})
-
userConfig = config
-
}
-
-
export const DEFAULT_USER_CONFIG: UserConfig = {
-
openAi: {
-
key: '',
-
apiHost: '<>',
-
chatModel: 'gpt-3.5-turbo',
-
},
-
temperature: 1,
-
maxContextMessageCount: 5,
-
systemPersonality: '',
-
}
???? 封装读写本地文件函数
TAURI 提供的 fs
对象已经很简洁易用,这里还封装一下主要有两个原因:
- 保证读写文件都在一个基础目录下进行,例如
$APP_DATA
目录。TAURI 出于安全考虑,要求对可读写文件的基础目录先行配置,具体为配置文件中的配置项,只在一个基础目录下操作文件,减少了配置,也方便调试维护这些本地文件。
- 出于安全、不同平台兼容性考虑,使用 TAURI 操作文件,是无法使用
/etc/...
这种绝对路径的,相对路径../
也无法使用,只能使用提供的一些枚举值代表的路径(足够丰富),如
代表的是本机
$APP_DATA
目录,对 macOS 平台而言,具体为/Users/<UserName>/Library/Application Support/<AppName>
目录,在执行写文件之前,要准备好这个文件夹!否则会写入文件失败!
-
import { fs } from '@tauri-apps/api'
-
-
const DEFAULT_DIR =
-
-
/**
-
* 写文件时,应确保文件夹的存在,文件夹不存在,则无法写入
-
* 使用 () 创建文件夹,如果文件夹已经存在,不会重复创建
-
*/
-
async function prepareWrite() {
-
await ('dir', { dir: DEFAULT_DIR, recursive: true })
-
}
-
-
export async function writeTextFile(file: Record<'path' | 'contents', string>) {
-
await prepareWrite()
-
await (file, { dir: DEFAULT_DIR })
-
}
-
-
export async function readTextFile(filePath: string) {
-
return await (filePath, { dir: DEFAULT_DIR })
-
}
对话页面
页面功能说明
用户输入问题后,请求接口,聊天记录区域以打字机的效果实时渲染 AI 的回答。
页面代码
<UserInput>
固定在页面底部,上方 <MessageList>
展示对话记录,
-
import MessageList from '@/components/MessageList'
-
import UserInput from '@/components/UserInput'
-
import style from './'
-
-
export default function Chat() {
-
return (
-
<div className={}>
-
<MessageList />
-
<UserInput />
-
</div>
-
)
-
}
UserInput
用户输入框,支持恢复待发送文本、按 ⬆️ 键恢复上次已发送文本。因为处理用户输入时的接口请求、文本渲染这些工作大部分都与当前组件无关,所以采用通过事件传递用户输入的文本,由 SEND_QUESTION
事件的订阅者来处理这些复杂的任务;后面将会增加新的聊天机器人,如 Bing AI,也通过订阅该事件进行 AI 回复。
-
import { eventBus } from '@/utils/event-bus'
-
import { useKeyPress, useMount, useSetState } from 'ahooks'
-
import { useRef } from 'react'
-
import style from './'
-
-
const storage = {
-
lastUserInput: '',
-
curUserInput: '',
-
}
-
-
export default function UserInput() {
-
const [state, setState] = useSetState({ input: '' })
-
const inputRef = useRef<HTMLTextAreaElement>(null)
-
-
function handleUserInput(input = '') {
-
setState({ input })
-
= input
-
}
-
-
function send() {
-
const content = (/(^\s*)|(\s*$)/g, '')
-
if (!content) {
-
return
-
}
-
= content
-
handleUserInput()
-
(.SEND_QUESTION, content)
-
}
-
-
useKeyPress(
-
'enter',
-
(e) => {
-
()
-
send()
-
}
-
)
-
// 实现用户键盘轻点 ⬆️,输入框内容为上次输入的问题
-
useKeyPress(
-
'uparrow',
-
(e) => {
-
const input =
-
if (!input) return
-
setState((state) => {
-
if () return state
-
// 组件渲染完成后,将光标移至输入框末尾
-
setTimeout(() => {
-
!.selectionStart =
-
}, 0)
-
return { input }
-
})
-
}
-
)
-
-
useMount(() => setState({ input: }))
-
-
return (
-
<div className={}>
-
<textarea
-
placeholder="ask anything ..."
-
value={}
-
ref={inputRef}
-
onChange={(e) => handleUserInput()}
-
spellCheck={false}
-
/>
-
</div>
-
)
-
}
MessageList
该组件展示对话记录。具体每一条消息用 <MessageCard>
渲染出来。
-
import { IMessage } from '@/types'
-
import { eventBus } from '@/utils/event-bus'
-
import { getMessages } from '@/utils/messages'
-
import { useState } from 'react'
-
import MessageCard from './MessageCard'
-
import style from './'
-
-
export default function MessageList() {
-
const [list, setList] = useState<IMessage[]>(getMessages())
-
-
(.CHANGE_MESSAGES, setList)
-
-
if (!) {
-
return (
-
<div className={}>
-
<span className={} />
-
<span className="mt-4 text-2xl font-bold text-white">欢迎使用 Chat Ta</span>
-
</div>
-
)
-
}
-
-
return (
-
<div className={}>
-
{((msg) => (
-
<MessageCard key={} {...msg} />
-
))}
-
</div>
-
)
-
}
页面搭建已完成,接下来看看如何处理最核心的请求、解析接口,渲染 AI 回复至界面。
???? 接收 API 返回的流,并渲染至界面
OpenAI Chat API 的具体调用格式可参考官方文档,这里着重介绍一下 stream
参数。
介绍这个参数前需要了解一下 ChatGPT 回复的大概过程:用户输入问题,发送请求,GPT 开始响应,但是服务器上 GPT 的回复不是一下子全部都有的,而是一个字符一个字符的生成,每生成下一个字符,GPT 都会综合利用已回复的上文,这也是 GPT 这类语言模型的重要特征。
参数 stream
默认值为 false
,具体表现为在服务器上等待全部回复的内容生成完整,然后再接口返回,因此回复内容稍微长一点都需要等很久接口才能返回。
将 stream
设为 true
,告诉服务器以流的形式传输内容,这样服务器每生成一个字符,前端都能立马拿到渲染出来,搭配上打字机的效果,用户体验 ???? !
-
import { Role } from '@/constants/enum'
-
import { eventBus } from '@/utils/event-bus'
-
import { getUserConfig } from '@/utils/user-config'
-
-
type ApiResult =
-
| { response: Response; error?: undefined }
-
| { response?: Response; error: { message: string; type?: string } }
-
-
export async function openAiChat(params: {
-
messages: { role: Role; content: string }[]
-
}): Promise<ApiResult> {
-
const config = await getUserConfig()
-
const headers = new Headers()
-
('Content-Type', 'application/json')
-
('Authorization', 'Bearer ' + )
-
const body = {
-
model: ,
-
messages: ,
-
stream: true,
-
temperature: ,
-
}
-
-
const abortController = new AbortController()
-
// 订阅 STOP_AI_RESPOND 事件,取消请求
-
const off = (.STOP_AI_RESPOND, () => ())
-
try {
-
const rsp = await fetch( + '/v1/chat/completions', {
-
method: 'POST',
-
headers,
-
body: (body),
-
signal: ,
-
})
-
if ( !== 200) {
-
return { error: (await ()).error || { message: '未知错误' } }
-
} else {
-
return { response: rsp }
-
}
-
} catch (error: any) {
-
return { error: error || { message: '程序异常' } }
-
} finally {
-
off()
-
}
-
}
该请求返回的响应 Headers 中 Content-Type: text/stream-event
。
对于流格式内容的解析相对复杂一些,一方面需要用 TextDecoder
实例对象去解码 ReadableStream
中的 Uint8Array
内容,另一方面,涉及到异步函数的多次调用,还需要处理中止流传输的操作。下面是具体代码实现:
-
interface ResolveStreamParams {
-
// 例如 (await fetch()).body
-
body: ReadableStream<Uint8Array>
-
// 渲染函数,这个函数会被多次执行,content 是从起始到当前 stream 解析出的长字符串
-
renderer(content: string): void
-
}
-
-
async function resolveStream(params: ResolveStreamParams) {
-
const { body, renderer } = params
-
const reader = ()
-
const decoder = new TextDecoder('UTF-8')
-
-
// text/event-stream 的响应可能会持续一段时间,我们允许用户手动取消
-
// 如果用户手动取消了,便不再读取流,可以让响应立即结束
-
const unlisten = (.STOP_AI_RESPOND, () => {
-
()
-
()
-
})
-
-
let content = ''
-
async function readChunk() {
-
let value: Uint8Array | undefined
-
try {
-
value = (await ()).value
-
} catch (error) {
-
return
-
}
-
const decodedStr = (value)
-
const strObjects = ('data: ', '').split('\n').filter(Boolean)
-
for (const strObj of strObjects) {
-
if (('[DONE]')) return
-
const obj = (strObj)
-
const newContent = obj?.choices?.[0]?.delta?.content
-
if (!newContent) continue
-
content += newContent
-
renderer(content)
-
}
-
// 这里一定要用 await,以保证拼接字符的正确顺序
-
await readChunk()
-
}
-
await readChunk()
-
unlisten()
-
}
调用 API、解析 stream 这些准备工作做好了,让我们在 sendQuestion()
中组合一下,并实现在 React 应用中以打字机的效果渲染 GPT 的回复:
-
export async function sendQuestion(question: string) {
-
// 发布:AI 开始响应
-
(.CHANGE_AI_RESPOND_STATE, true)
-
-
const rst = await openAiChat({ messages: await makeMessages(question) })
-
-
if () {
-
const messages = getMessages()
-
(-1)!.content =
-
(-1)!.isError = true
-
setMessages([...messages])
-
// 发布:AI 结束响应
-
(.CHANGE_AI_RESPOND_STATE, false)
-
return
-
}
-
-
function getRenderer() {
-
const INTERVAL_TIME = 50
-
let cachedContent = ''
-
let curContent = ''
-
let timer = 0
-
// 是否手动结束渲染
-
let isStopRender = false
-
// 订阅:手动中止 AI 响应,结束打字机渲染
-
const unlisten = (.STOP_AI_RESPOND, () => {
-
isStopRender = true
-
})
-
-
// 这个函数会在 resolveStream() 中多次调用
-
return function renderer(content: string) {
-
cachedContent = content
-
if (timer) return
-
timer = (() => {
-
// 是否已经将当前 cachedContent 内容全部渲染完成
-
let isRenderedAllCachedContent = ===
-
// 手动停止或者已经渲染完成全部内容
-
if (isStopRender || (isRenderedAllCachedContent && isResolveFinished)) {
-
(timer)
-
(.CHANGE_AI_RESPOND_STATE, false)
-
unlisten()
-
}
-
const char = cachedContent[]
-
if (char === undefined) return
-
curContent += char
-
// 更新 messages,让 react 执行渲染
-
const messages = getMessages()
-
(-1)!.content = curContent
-
setMessages([...messages])
-
}, INTERVAL_TIME)
-
}
-
}
-
-
// 标识 stream 是否已经解析完成
-
let isResolveFinished = false
-
await resolveStream({
-
body: !,
-
renderer: getRenderer(),
-
})
-
isResolveFinished = true
-
}
这里是用 setInterval
实现打字机的效果,每 50ms 拼接一个字符,但是我们不用去限制解析 stream 时每 50ms 解析一个 chunk,因此用变量 cachedContent
暂存一下解析结果,以便后续的渲染继续使用。
???? 渲染优化
App 中有一个 messages
存储的是完整的对话记录,这个变量对于整个应用至关重要,比如:
- 应用启动时,需要读取本地存储的 json 文件里的对话记录,将其赋值给
messages
- 应用关闭时,需要将
messages
保存为本地 json 文件 - 每次进入聊天页面,都要把完整的
messages
渲染出来 - 用户输入问题、解析 API 都需要更新
messages
- 根据
messages
是否为空决定是否展示清空记录按钮。 - …….
messages
有两个重要特点:全局性、频繁更新。
一开始我将其设置为 React 全局 Context 的一个 valve(本项目没有使用任何 React 状态库,Mobx 不想用,Redux 嫌它老,新的不想学????),用起来倒是方便,但是会导致巨量的组件 rerender,比如消息聊天记录页面用来展示每条消息的每个 <MessageCard>
都用到了它,因为打字机效果需要,每 50ms 全部 rerender 一遍 ???? 。这是完全没必要的,因为其中绝大部分的组件不需要 rerender,打字机效果生效时,只有最后一个消息卡片需要每 50ms 重新渲染。这么干严重加重了 CPU 负载,不妥不妥。 (对于 React 里这种场景大家有什么优雅的解决方案,欢迎留言讨论 ????)
于是我决定采用 JS 中常见的事件-发布订阅的设计模式重写这一部分。具体来说:
- 将
messages
放到一个模块里,并且该模块导出setMessages()
和getMessages()
- 每次
setMessages()
,都发布事件emit(CHANGE_MESSAGES, messages)
- 每一个要用到
messages
的组件,都通过 hook 订阅事件useListen(CHANGE_MESSAGES, handler)
,在handler
内更新当前组件的 state
为什么可以提升性能❓
React Context 机制决定了,value 的每一次改变,都会触发其所有的子组件 rerender,以便它们都能接收到最新值。通过事件订阅机制,可以令用到 messages
的组件订阅数据改变事件,然后根据 event handler 接收到的新的 messages
更新组件内部 state。这么做限定了变更数据会影响到的组件、减少了组件 rerender 的时机,更细粒度、更精准的掌控组件,因此可以有效提升应用性能。
TAURI 中的事件
通过 @tauri-apps/api/event
导出的 event
对象,可以很方便的在前端、后端间进行事件通信。TAURI 预先提供了一些事件,如应用更新、文件拖入窗口、关闭窗口等,可以通过枚举 获取这些事件名称。当然除了 TAURI 提供的这些事件,自定义事件也是允许的,只需在前端、后端使用相同的事件名称字符串即可。
窗口事件
需要注意的是,涉及到窗口的事件时,需要通过窗口实例如 appWindow
对象来监听。
-
import { appWindow } from '@tauri-apps/api/window'
-
-
(.WINDOW_CLOSE_REQUESTED, () => {
-
// 窗口关闭前需要执行的任务...
-
await ()
-
})
前端调用 Rust
通过 Rust 我们可以调用系统原生能力,Tauri 允许在 JavaScript 前端调用 Rust 编写的函数(称为指令)。
???? 示例:
-
// 定义一个 greet 指令
-
#[tauri::command]
-
fn greet(name: &str) -> String {
-
format!("???? Hello, {}!", name)
-
}
-
-
fn main() {
-
tauri::Builder::default()
-
.invoke_handler(tauri::generate_handler![greet]) // 注册指令
-
.run(tauri::generate_context!())
-
.expect("❌ error while running tauri application.")
-
}
前端调用:
-
import { invoke } from '@tauri-apps/api'
-
-
invoke('greet', { name: '???? 红绿灯的黄' })
-
.then(response => {
-
= response
-
})
步骤:
- 写自定义的 Rust 函数并用宏声明
- 在 Rust
main()
函数中通过generate_handler
函数注册指令 - 在前端通过
invoke()
函数调用指令
构建项目
执行 tauri build
即可构建应用。
构建时会读取 中的内容,根据该配置决定打包产物需要包含哪些特性,因此只为用到的特性设为
true
,可以有效降低安装包体积。
应用图标
不同的平台,所使用的 App 图标格式是不同的,而且在不同的场景下,平台也可能会使用不同分辨率的图标。TAURI 提供了一个很方便的命令,只需要准备一张基本的图标,然后执行命令行,即可生成所有平台需要的图标。
pnpm tauri icon <your-logo-path>
生成的图标资源存放在 src-tauri/icon
目录中。
这里可以使用 figma 创建图标,我所采用的标准为:
- 图标尺寸:256 * 256
- 白色矩形尺寸:212 * 212
- 白色矩形圆角:56
- 内容尺寸:128 * 128
- 画笔宽度:14
- 导出:512,PNG
安装包
生成安装包文件将在 src-tauri/releas/bundle
目录下。(4.6 MB❗️❗️❗️)
运行内存
(29.0 MB❗️❗️❗️)
写在最后
TAURI 为整个项目开发周期都提供了便利的 CLI 工具,方便我们快速创建、启动、调试、构建应用,甚至贴心的提供了一个命令来生成不同平台会用到的所有图标,在前端调用 Rust 也是非常方便的,总体来说开发体验 ????。
TAURI 把应用的安全性放在很关键的位置,所有系统原生能力都需要通过配置才能启用,所有可以访问的系统目录也需要配置。当然,配置也是很简单的,在项目 文件中可以快速设置。
TAURI App 使用 WebView 渲染页面,处理前端逻辑,后端使用 Rust 编译产生的二进制文件,和 Electron 相比,可以极为有效的控制打包产物大小、提升应用性能,而且将来可以适配的平台也更多。很多人介于不同平台上的 WebView 差异较多,可能不太看好 TAURI,更看好 Electron 这种借助 Chromium 提供统一 WebView 的框架。这也没错。但我想说,如今早已不是十年前那个浏览器市场战火纷飞一地鸡毛还能让 IE 大行其道的时代了,如今各种前端标准越来越规范,兼容性问题已经不再是令人措不及防应接不暇的问题;macOS 上基于 WebKit 的 WebView 已经足够好用,在 Windows11 上新的 WebView2 也是基于更现代化的 Chromium;移动设备上的 WebView 更是无需担心,因为它们的系统本就是现代化的操作系统,装载的 WebView 可能会有的疑难杂症也更少。当然,实际开发中还是需要解决一些兼容性问题,可是,我们作为一个前端开发,开发 Web 应用、小程序时,尚且需要处理一些兼容性问题,开发 TAURI App 也是同样的道理。况且其中大部分问题都可以通过前端工具进行兼容处理。
作为一个前端开发,我们可以借助 TAURI,将我们的技术能力扩展至 Rust、原生系统、Shell 这些更为底层、更有挑战性、更有可为的技术领域。
总之,TAURI 是一个很值得关注、尝试使用的框架。
最后,本项目 GitHub 仓库地址: ???? GitHub - Y80/chat-ta: 一款基于 TAURI 的跨平台 ChatGPT 客户端