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 对象已经很简洁易用,这里还封装一下主要有两个原因:
- 保证读写文件都在一个基础目录下进行,例如
$APP_DATA目录。TAURI 出于安全考虑,要求对可读写文件的基础目录先行配置,具体为配置文件中的tauri.allowList.fs.scope配置项,只在一个基础目录下操作文件,减少了配置,也方便调试维护这些本地文件。 - 出于安全、不同平台兼容性考虑,使用 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 中常见的事件-发布订阅的设计模式重写这一部分。具体来说:
- 将
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 预先提供了一些事件,如应用更新、文件拖入窗口、关闭窗口等,可以通过枚举 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})
步骤:
- 写自定义的 Rust 函数并用宏声明
- 在 Rust
main()函数中通过generate_handler函数注册指令 - 在前端通过
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 客户端
相关文章:
3 天,入门 TAURI 并开发一个跨平台 ChatGPT 客户端
TAURI 是什么 TAURI 是一个使用 Rust 编写的程序框架,它允许我们使用 Web 技术和 Rust 语言构建跨端应用。它提供了大量特性,例如系统通知、网络请求、全局快捷键、本地文件处理等,它们都可以在前端通过 JavaScript 便捷的调用。 TAURI 应用…...
14个最佳创业企业WordPress主题
要创建免费网站?从易服客建站平台免费开始 500M免费空间,可升级为20GB电子商务网站 创建免费网站 您网站的设计使您能够展示产品的独特卖点。通过正确的主题,您将能够解释为什么客户应该选择您的品牌而不是其他品牌。 在本文中࿰…...
MySQL基础(三十)PowerDesigner的使用
1 PowerDesigner的使用 PowerDesigner是一款开发人员常用的数据库建模工具,用户利用该软件可以方便地制作 数据流程图 、概念数据模型 、 物理数据模型,它几乎包括了数据库模型设计的全过程,是Sybase公司为企业建模和设计提供的一套完整的集…...
nginx 服务器总结
一. 负载均衡的作用有哪些? 1、转发功能 按照一定的算法【权重、轮询】,将客户端请求转发到不同应用服务器上,减轻单个服务器压力,提高 系统并发量。 2、故障移除 通过心跳检测的方式,判断应用服务器当前是否可以正常…...
基于Hebb学习的深度学习方法总结
基于Hebb学习的深度学习方法总结 0 引言1 前置知识1.1 Hebb学习规则1.2 Delta学习规则 2 SoftHebb学习算法2.1 WTA(Winner Take All)2.2 SoftHebb2.3 多层Hebb网络2.4 Hebb学习的性能测评 3 参考文献 0 引言 总所周知,反向传播算法(back-propagating, B…...
思科模拟器 | 访问控制列表ACL实现网段精准隔绝
文章目录 一、ACL工作原理二、ACL分类初步介绍三、标准ACL1、标准ACL的决策过程2、标通配符掩码关键字3、标准ACL网络拓扑4、标准ACL演示5、实战讲解 四、扩展ACL1、基础语法明细2、扩展ACL示例3、扩展ACL网络拓扑4、实战讲解 五、总结与提炼 一、ACL工作原理 ACL(A…...
Python os模块详解
1. 简介 os就是“operating system”的缩写,顾名思义,os模块提供的就是各种 Python 程序与操作系统进行交互的接口。通过使用os模块,一方面可以方便地与操作系统进行交互,另一方面页也可以极大增强代码的可移植性。如果该模块中相…...
Oracle PL/SQL基础语法学习13:比较运算符
系列文章目录 Oracle PL/SQL基础语法学习12:短路求值 Oracle PL/SQL基础语法学习13:比较运算符 Oracle PL/SQL基础语法学习14:BOOLEAN表达式 文章目录 系列文章目录Oracle PL/SQL基础语法学习13:比较运算符比较运算符介绍官方文档…...
金仓数据库适配记录
金仓数据库适配记录 人大金仓数据库管理系统KingbaseES(简称:金仓数据库或KingbaseES)是北京人大金仓信息技术股份有限公司自主研制开发的具有自主知识产权的通用关系型数据库管理系统。 金仓数据库主要面向事务处理类应用,兼顾各类数据分析类应用,可用做管理信息系统、…...
ElasticSearch 学习 ==ELK== 进阶
二、ElasticSearch 学习 ELK 进阶 (1)文档局部更新 我们也说过文档是不可变的——它们不能被更改,只能被替换。 update API必须遵循相同的规则。表面看来,我们似乎是局部更新了文档的位置,内部却是像我们之前说的一样…...
【数据结构 -- C语言】 双向带头循环链表的实现
目录 1、双向带头循环链表的介绍 2、双向带头循环链表的接口 3、接口实现 3.1 开辟结点 3.2 创建返回链表的头结点 3.3 判断链表是否为空 3.4 打印 3.5 双向链表查找 3.6 双向链表在pos的前面进行插入 3.6.1 头插 3.6.2 尾插 3.6.3 更新头插、尾插写法 3.7 双向链…...
自然语言处理与其Mix-up数据增强方法报告
自然语言处理与其Mix-up数据增强方法 1绪论1.课题背景与意义1.2国内外研究现状 2 自然语言经典知识简介2.1 贝叶斯算法2.2 最大熵模型2.3神经网络模型 3 Data Augmentation for Neural Machine Translation with Mix-up3.1 数据增强3.2 对于神经机器翻译的软上下文的数据增强3.…...
Vue(组件化编程:非单文件组件、单文件组件)
一、组件化编程 1. 对比传统编写与组件化编程(下面两个解释图对比可以直观了解) 传统组件编写:不同的HTML引入不同的样式和行为文件 组件方式编写:组件单独,复用率高(前提组件拆分十分细致) 理…...
【MATLAB数据处理实用案例详解(22)】——基于BP神经网络的PID参数整定
目录 一、问题描述二、算法仿真2.1 BP_PID参数整定初始化2.2 优化PID2.3 绘制图像 三、运行结果四、完整程序 一、问题描述 基于BP神经网络的PID控制的系统结构如下图所示: 考虑仿真对象,输入为r(k)1.0,输入层为4,隐藏层为5&…...
第11章 项目人力资源管理
文章目录 项目人力资源管理 过程11.2.1 编制项目人力资源计划的工具与技术(1)层次结构图(工作、组织、资源 分解结构)(2)矩阵图(责任分配矩阵,RAM)(3…...
07-Vue技术栈之(组件之间的通信方式)
目录 1、组件的自定义事件1.1 绑定自定义事件:1.1.1 第一种方式1.1.2 第二种方式1.1.3 自定义事件只触发一次 1.2 解绑自定义事件1.3绑定原生DOM事件1.4 总结 2、全局事件总线(GlobalEventBus)2.1 应用全局事件总线 3、 消息订阅与发布&#…...
度量学习Metirc Learning和基于负例的对比学习Contrastive Learning的异同点思考
参考:对比学习(Contrastive Learning):研究进展精要 - 知乎 参考:对比学习论文综述【论文精读】_哔哩哔哩_bilibili 参考:度量学习DML之Contrastive Loss及其变种_对比损失的变种_胖胖大海的博客-CSDN博客 参考&…...
3.编写油猴脚本之-helloword
3.编写油猴脚本之-helloword Start 通过上一篇文章的学习,我们安装完毕了油猴插件。今天我们来编写一个helloword的脚步,体验一下油猴。 1. 开始 点击油猴插件>添加新脚本 默认生成的脚本 // UserScript // name New Userscript // name…...
openwrt的openclash提示【更新失败,请确认设备闪存空间足够后再试】
网上搜索了一下,问题应该是出在“无法从网络下载内核更新包”或者“无法识别内核的版本号” 解决办法:手动下载(我是只搞了DEV内核就搞定了TUN和Meta没有动) --> 上传到路由器上 --> 解压缩 --> 回到openclash界面更新配…...
torch.nn.Module
它是所有的神经网络的根父类! 你的神经网络必然要继承 可以看一下这篇文章...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
基于数字孪生的水厂可视化平台建设:架构与实践
分享大纲: 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年,数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段,基于数字孪生的水厂可视化平台的…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
