3 天,入门 TAURI 并开发一个跨平台 ChatGPT 客户端

 

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 后端基于 Node.js 平台,可以享受丰富的 Node.js 生态,更容易上手开发后端服务。
  • 支持的平台:因为渲染引擎的选择不同,Electron 只能支持 Windows/macOS/Linux,而 TAURI 不仅支持这些平台,还能支持 iOS/iPadOS/Android。

心动不如行动!现在就用 TAURI 开发一款跨平台的 ChatGPT 客户端!💪

它有如下功能:

  • 持久化本地保存对话记录
  • 多页面支持
  • 使用个人 API Key
  • 配置 API Host 代理、Chat Model、对话风格
  • 让 AI 理解上下文,并且可配置上下文消息数
  • 指定 AI 人格,让 TA 成为编程大师、郭德纲、猫娘然后与你交流
  • ……

当前项目已开源!文末给出该项目的 Github 代码仓库地址!

 

 

我从开始阅读 TAURI 官方文档,到开发完成这款 App,只用了 3 天时间。有了我的踩坑,你甚至可以 1 天内开发完成这款应用!

开始!🚀

创建项目

创建项目前,需要确保本地已安装 Node.js、Rust,然后使用你的 Node.js 包管理工具(如 pnpm )执行:

pnpm create tauri-app

在终端中,可以命名项目名称,选择包管理工具、JavaScript/TypeScript、前端框架。我这里选择的是 pnpm + TS + Vite + React。

项目目录结构:

root
├── public
├── src
├── src-tauri
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── ...

基本的目录结构和一个标准的 Web 项目目录结构几乎一致,但是这里多了一个 src-tauri 目录,这是一个 Rust 项目的目录:

src-tauri
├── Cargo.toml
├── tauri.conf.json
├── src
├── icons
└── ...

其中 tauri.conf.json 需要特别关注,因为它是整个 TAURI App 的配置文件; src-tauri/src 中可以写一些 Rust 代码, src-tauri/icons 是 App 的图标文件夹,存放了不同操作系统会用到的不同分辨率/格式的 App 图标资源,可以用 CLI tauri icon base-icon.png 自动生成 💡。

启动

安装依赖、启动项目:

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 版本(新特性巨多🥵)。

入口文件 main.tsx 没什么改动:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>
)

在 App.tsx 中配置两个页面:

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} />)
}

在 Layout.tsx 中使用 <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 = location.pathname !== '/'return (<div className={style.header}><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 './index.module.css'export default function More() {const navigate = useNavigate()const [state, setState] = useSetState({key: '',// ...})// 页面加载时,读取本地配置文件,并设置 stateuseMount(async () => {const config = await getUserConfig()setState({key: config.openAi.key,// ...})})async function save() {await setUserConfig(state)// 调用系统原生的 dialogawait dialog.message('✅ 配置已保存')navigate('/')}return (<div className={style.more}><form><div className={style.formItem}><label>OpenAI Key</label><input value={state.key} onChange={(e) => setState({ key: e.target.value })} /></div>{/* 其他表单输入项... */}<button onClick={save} type="button">保存</button></form></div>)
}

读写用户配置工具函数

getUserConfig() 和 setUserConfig() 的具体实现:

import { UserConfig } from '@/types'
import { readTextFile, writeTextFile } from './file'const CONFIG_FILE_NAME = 'config.json'// 保存一个 JS 变量,以便前端获取配置时,不用每次都读文件
let userConfig: UserConfig | null = nullexport async function getUserConfig() {if (userConfig) return userConfigconst config = await readTextFile(CONFIG_FILE_NAME)try {userConfig = JSON.parse(config)} catch (error) {userConfig = DEFAULT_USER_CONFIG}return userConfig!
}export async function setUserConfig(config: UserConfig) {await writeTextFile({path: CONFIG_FILE_NAME,contents: JSON.stringify(config),})userConfig = config
}export const DEFAULT_USER_CONFIG: UserConfig = {openAi: {key: '',apiHost: '<https://api.openai.com>',chatModel: 'gpt-3.5-turbo',},temperature: 1,maxContextMessageCount: 5,systemPersonality: '',
}

🌟 封装读写本地文件函数

TAURI 提供的 fs 对象已经很简洁易用,这里还封装一下主要有两个原因:

  1. 保证读写文件都在一个基础目录下进行,例如 $APP_DATA 目录。TAURI 出于安全考虑,要求对可读写文件的基础目录先行配置,具体为配置文件中的 tauri.allowList.fs.scope 配置项,只在一个基础目录下操作文件,减少了配置,也方便调试维护这些本地文件。
  2. 出于安全、不同平台兼容性考虑,使用 TAURI 操作文件,是无法使用 /etc/... 这种绝对路径的,相对路径 ../ 也无法使用,只能使用 fs.BaseDirectory 提供的一些枚举值代表的路径(足够丰富),如 fs.BaseDirectory.AppData 代表的是本机 $APP_DATA 目录,对 macOS 平台而言,具体为 /Users/<UserName>/Library/Application Support/<AppName> 目录,在执行写文件之前,要准备好这个文件夹!否则会写入文件失败!
import { fs } from '@tauri-apps/api'const DEFAULT_DIR = fs.BaseDirectory.AppData/*** 写文件时,应确保文件夹的存在,文件夹不存在,则无法写入* 使用 fs.createDir() 创建文件夹,如果文件夹已经存在,不会重复创建*/
async function prepareWrite() {await fs.createDir('dir', { dir: DEFAULT_DIR, recursive: true })
}export async function writeTextFile(file: Record<'path' | 'contents', string>) {await prepareWrite()await fs.writeTextFile(file, { dir: DEFAULT_DIR })
}export async function readTextFile(filePath: string) {return await fs.readTextFile(filePath, { dir: DEFAULT_DIR })
}

对话页面

页面功能说明

用户输入问题后,请求接口,聊天记录区域以打字机的效果实时渲染 AI 的回答。

页面代码

<UserInput> 固定在页面底部,上方 <MessageList> 展示对话记录,

 

import MessageList from '@/components/MessageList'
import UserInput from '@/components/UserInput'
import style from './index.module.css'export default function Chat() {return (<div className={style.chat}><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 './index.module.css'const storage = {lastUserInput: '',curUserInput: '',
}export default function UserInput() {const [state, setState] = useSetState({ input: '' })const inputRef = useRef<HTMLTextAreaElement>(null)function handleUserInput(input = '') {setState({ input })storage.curUserInput = input}function send() {const content = state.input.replace(/(^\s*)|(\s*$)/g, '')if (!content) {return}storage.lastUserInput = contenthandleUserInput()eventBus.emit(eventBus.name.SEND_QUESTION, content)}useKeyPress('enter',(e) => {e.preventDefault()send()})// 实现用户键盘轻点 ⬆️,输入框内容为上次输入的问题useKeyPress('uparrow',(e) => {const input = storage.lastUserInputif (!input) returnsetState((state) => {if (state.input) return state// 组件渲染完成后,将光标移至输入框末尾setTimeout(() => {inputRef.current!.selectionStart = input.length}, 0)return { input }})})useMount(() => setState({ input: storage.curUserInput }))return (<div className={style.userInput}><textareaplaceholder="ask anything ..."value={state.input}ref={inputRef}onChange={(e) => handleUserInput(e.target.value)}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 './index.module.css'export default function MessageList() {const [list, setList] = useState<IMessage[]>(getMessages())eventBus.useListen(eventBus.Name.CHANGE_MESSAGES, setList)if (!list.length) {return (<div className={style.messageList}><span className={style.logo} /><span className="mt-4 text-2xl font-bold text-white">欢迎使用 Chat Ta</span></div>)}return (<div className={style.messageList}>{list.map((msg) => (<MessageCard key={msg.id} {...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()headers.append('Content-Type', 'application/json')headers.append('Authorization', 'Bearer ' + config.openAi.key)const body = {model: config.openAi.chatModel,messages: params.messages,stream: true,temperature: config.temperature,}const abortController = new AbortController()// 订阅 STOP_AI_RESPOND 事件,取消请求const off = eventBus.once(eventBus.Name.STOP_AI_RESPOND, () => abortController.abort())try {const rsp = await fetch(config.openAi.apiHost + '/v1/chat/completions', {method: 'POST',headers,body: JSON.stringify(body),signal: abortController.signal,})if (rsp.status !== 200) {return { error: (await rsp.json()).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()).bodybody: ReadableStream<Uint8Array>// 渲染函数,这个函数会被多次执行,content 是从起始到当前 stream 解析出的长字符串renderer(content: string): void
}async function resolveStream(params: ResolveStreamParams) {const { body, renderer } = paramsconst reader = body.getReader()const decoder = new TextDecoder('UTF-8')// text/event-stream 的响应可能会持续一段时间,我们允许用户手动取消// 如果用户手动取消了,便不再读取流,可以让响应立即结束const unlisten = eventBus.once(eventBus.Name.STOP_AI_RESPOND, () => {reader.cancel()reader.releaseLock()})let content = ''async function readChunk() {let value: Uint8Array | undefinedtry {value = (await reader.read()).value} catch (error) {return}const decodedStr = decoder.decode(value)const strObjects = decodedStr.replaceAll('data: ', '').split('\n').filter(Boolean)for (const strObj of strObjects) {if (strObj.includes('[DONE]')) returnconst obj = JSON.parse(strObj)const newContent = obj?.choices?.[0]?.delta?.contentif (!newContent) continuecontent += newContentrenderer(content)}// 这里一定要用 await,以保证拼接字符的正确顺序await readChunk()}await readChunk()unlisten()
}

调用 API、解析 stream 这些准备工作做好了,让我们在 sendQuestion() 中组合一下,并实现在 React 应用中以打字机的效果渲染 GPT 的回复:

export async function sendQuestion(question: string) {// 发布:AI 开始响应eventBus.emit(eventBus.Name.CHANGE_AI_RESPOND_STATE, true)const rst = await openAiChat({ messages: await makeMessages(question) })if (rst.error) {const messages = getMessages()messages.at(-1)!.content = rst.error.messagemessages.at(-1)!.isError = truesetMessages([...messages])// 发布:AI 结束响应eventBus.emit(eventBus.Name.CHANGE_AI_RESPOND_STATE, false)return}function getRenderer() {const INTERVAL_TIME = 50let cachedContent = ''let curContent = ''let timer = 0// 是否手动结束渲染let isStopRender = false// 订阅:手动中止 AI 响应,结束打字机渲染const unlisten = eventBus.once(eventBus.Name.STOP_AI_RESPOND, () => {isStopRender = true})// 这个函数会在 resolveStream() 中多次调用return function renderer(content: string) {cachedContent = contentif (timer) returntimer = window.setInterval(() => {// 是否已经将当前 cachedContent 内容全部渲染完成let isRenderedAllCachedContent = curContent.length === cachedContent.length// 手动停止或者已经渲染完成全部内容if (isStopRender || (isRenderedAllCachedContent && isResolveFinished)) {window.clearInterval(timer)eventBus.emit(eventBus.Name.CHANGE_AI_RESPOND_STATE, false)unlisten()}const char = cachedContent[curContent.length]if (char === undefined) returncurContent += char// 更新 messages,让 react 执行渲染const messages = getMessages()messages.at(-1)!.content = curContentsetMessages([...messages])}, INTERVAL_TIME)}}// 标识 stream 是否已经解析完成let isResolveFinished = falseawait resolveStream({body: rst.response.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 中常见的事件-发布订阅的设计模式重写这一部分。具体来说:

  1. messages 放到一个模块里,并且该模块导出 setMessages()getMessages()
  2. 每次 setMessages(),都发布事件 emit(CHANGE_MESSAGES, messages)
  3. 每一个要用到 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 预先提供了一些事件,如应用更新、文件拖入窗口、关闭窗口等,可以通过枚举 event.TauriEvent 获取这些事件名称。当然除了 TAURI 提供的这些事件,自定义事件也是允许的,只需在前端、后端使用相同的事件名称字符串即可。

窗口事件

需要注意的是,涉及到窗口的事件时,需要通过窗口实例如 appWindow 对象来监听。

import { appWindow } from '@tauri-apps/api/window'appWindow.listen(event.TauriEvent.WINDOW_CLOSE_REQUESTED, () => {// 窗口关闭前需要执行的任务...await appWindow.close()
})

前端调用 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 => {window.header.innerHTML = response})

步骤:

  1. 写自定义的 Rust 函数并用宏声明
  2. 在 Rust main() 函数中通过 generate_handler 函数注册指令
  3. 在前端通过 invoke() 函数调用指令

构建项目

执行 tauri build 即可构建应用。

构建时会读取 tauri.conf.json 中的内容,根据该配置决定打包产物需要包含哪些特性,因此只为用到的特性设为 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.conf.json 文件中可以快速设置。

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 客户端

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/6676.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

chatgpt|安装及示例|聊天|嵌入|微调|适度|图像|音频|异步|API 错误代码-OpenAI Python库简介

文章目录 OpenAI Python库安装可选依赖项 用法参数微软 Azure 端点微软 Azure 活动目录身份验证命令行界面 示例代码聊天嵌入微调适度 图像生成 (DALLE)音频转录(Whisper)异步API要求信用TIPS:OpenAI API 错误代码API 错误状态码 概述详细解释 OpenAI Python库 项目git地址 O…

Python和ChatGPT在一起了

&#x1f449; Python练手必备&#x1f449; Python毕设实战项目&#x1f449; Python爬虫实战必备&#x1f449; 30款Python小游戏附源码&#x1f449; Python清理微信单向好友神器

chatgpt赋能python:Python支持跨平台软件开发

Python支持跨平台软件开发 作为一种高级编程语言&#xff0c;Python 以其丰富的库和跨平台支持而备受开发人员欢迎。Python 通过将应用程序的可移植性最大化&#xff0c;使得开发人员可以轻松地在不同的操作系统平台上构建和部署软件。 跨平台支持 Python 支持各种不同的操作…

ChatGPT今日正式开放API服务中小企业

开放隐私计算 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。 183篇原创内容 公…

chatGPT python API

import requests import os # 设置代理服务器地址和端口号 proxies { "https": "http://127.0.0.1:9910", "http": "http://127.0.0.1:9910" } # 设置API密钥和API请求 API_KEY "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"…

[语音信号处理] 说话人转换 voice conversion .CycleGANVC .StarGANVC .

语音信号处理的总体结构 从这个总体结构可以看出&#xff1a;无论是语音识别&#xff0c;还是语音编码与合成&#xff0c;输入的语音信号首先要进行预处理。 预处理 对信号进行适当放大和增益控制&#xff0c;并进行反混叠滤波来消除工频信号的干扰。 数字化 将模拟信号转…

【Google语音转文字】Speech to Text 超级好用的语音转文本API

前面有一篇博客说到了讯飞输入法&#xff0c;支持语音输入&#xff0c;也支持电脑内部音源输入&#xff0c;详细参考&#xff1a;【实时语音转文本】PC端实时语音转文本(麦克风外音&系统内部音源) 但是它只是作为一个工具来使用&#xff0c;如果我们想自己做一些好玩的东西…

Stable Diffusion +ChatGPT+文本转语音+剪映制作视频

目录 chatgpt免费体验入口网址 模型下载 huggingface.co civitai.com 使用Deliberate模型案例 StableDeffusion做的图片&#xff0c;chatGPT出的文案&#xff0c;微软文本转语音配的音&#xff0c;使用剪映做的视频 chatgpt免费体验入口网址 http://chat.xutongbao.top …

语音怎么转换成文字?分享两种语音转文字的方法

怎么把语音文件中的内容转换成文字呢&#xff1f;大家在日常的学习和办公过程中&#xff0c;在遇到一些重要内容或者是讲话&#xff0c;是不是会有将它录成语音的经历&#xff1f;因为这样会方便我们及时记录一些重要的内容&#xff0c;但当我们整理语音时&#xff0c;还要一句…

OpenAI-ChatGPT最新官方接口《语音智能转文本》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(六)(附源码)

Speech to text 语音智能转文本 Introduction 导言Quickstart 快速开始Transcriptions 转录python代码cURL代码 Translations 翻译python代码cURL代码 Supported languages 支持的语言Longer inputs 长文件输入Prompting 提示其它资料下载 Speech to text 语音转文本 Learn how…

学术界的期刊编辑如何识别通过 ChatGPT 编写出来的论文?

最近 ChatGPT 风靡全球&#xff0c;国外也有不少大学生使用 ChatGPT 来撰写论文&#xff0c;这给学术出版界的期刊编辑识别以 ChatGPT 为代表的人工智能写作出来的文章带来了很大的挑战。 咱们国内有一句古话&#xff1a;道高一尺&#xff0c;魔高一丈。 学术出版界的期刊编辑…

ChatGPT原理解释

写了一本介绍ChatGPT原理的课程 结构如下 01、介绍ChatGPT及其原理 1.1 ChatGPT的概述 1.2 什么是自然语言处理&#xff08;NLP&#xff09; 1.3 深度学习与NLP的关系 1.4 GPT模型的介绍 02、GPT原理探讨 2.1 GPT模型的输入与输出 2.2 GPT模型的结构 2.3 GPT模型的预训练方法…

什么?ChatGPT这么火,你没搞过实战?只用来口嗨侃大山了?

导语&#xff1a;AI技术在各个领域的应用越来越广泛&#xff0c;ChatGPT作为一款强大的自然语言处理模型&#xff0c;能帮助企业和个人提高工作效率和客户满意度。本文将详细介绍如何将ChatGPT集成到微信公众号上&#xff0c;帮助您实现自动化的客户服务和信息发布&#xff0c;…

ChatGPT这么火,我们能怎么办?

今天打开百度&#xff0c;看到这样一条热搜高居榜二&#xff1a;B站UP主发起停更潮&#xff0c;然后点进去了解一看&#xff0c;大体是因为最近AI创作太火&#xff0c;对高质量原创形成了巨大冲击&#xff01; 记得之前看过一位UP主的分享&#xff0c;说B站UP主的年收入大体约等…

GPT-3.5(ChatGPT)训练和部署成本估算

因为ChatGPT&#xff08;GPT-3.5&#xff09;未正式公布参数量&#xff0c;暂时按照1750亿参数计算。 后续其他模型公布参数量后&#xff0c;可按参数量线性比例估算相关数值。 以下数值仅为理论估算&#xff0c;可能和实际数值相差很大&#xff0c;敬请谅解。 一、GPT-3.5磁…

使用ChatGPT写一篇文章

一、MyBatis中一对一关联查询概述 MyBatis是一个Java持久层框架&#xff0c;它提供了简单而强大的数据访问方式。在实际的开发中&#xff0c;我们经常需要查询两个表之间的关联数据&#xff0c;这就需要用到MyBatis的一对一关联查询。本文将介绍如何在MyBatis中实现一对一关联…

使用tailwindcss实现文章的快速排版

Tailwind CSS 是一个功能类优先的 CSS 框架&#xff0c;它集成了诸如 flex, pt-4, text-center 和 rotate-90 这样的类&#xff0c;它们能直接在脚本标记语言中组合起来&#xff0c;构建出任何设计。tailwindcss生产环境非常小&#xff0c;并且一切皆是响应式的。 首先来看一个…

微信公众号文章如何排版?

记得小编在刚开始接触制作公众号文章的时候&#xff0c;最头疼的就是文章的排版了。因为刚开始不懂也不知道该怎么设置字号&#xff0c;字间距&#xff0c;段落格式。也是狠心好好研究了很久才慢慢摸索出来一些经验&#xff0c;这边给大家简单的分享一下文章排班的基本设置吧 首…

ChatGPT专业应用:基于原有文章改写新文章

正文共 611 字&#xff0c;阅读大约需要 2 分钟 市场运营/SEO投放必备技巧&#xff0c;您将在2分钟后获得以下超能力&#xff1a; 自动改写SEO文章 Beezy评级 &#xff1a;B级 *经过简单的寻找&#xff0c; 大部分人能立刻掌握。主要节省时间。 推荐人 | Alice 编辑者 | Lind…

【ChatGPT】写一篇文章《ChatGPT 已成为下一代的新操作系统》,不少于3000字,使用markdown格式。

目录 ChatGPT 已成为下一代的新操作系统 一、ChatGPT 的发展背景 1.1 人工智能的崛起