优秀的编程知识分享平台

网站首页 > 技术文章 正文

Tauri:快速、跨平台的桌面应用程序

nanyue 2025-05-22 12:26:45 技术文章 1 ℃

多年来,Electron是构建桌面应用程序的实际跨平台框架。 Visual Studio Code,MongoDB Compass和Postman都是使用此框架构建的应用程序的很好的例子。Electron绝对很棒,但它有一些明显的缺点,其他一些现代框架已经克服了这些缺点 - Tauri是其中最好的之一。

什么是Tauri?

Tauri 是一个现代框架,允许您在前端使用熟悉的 Web 技术(如 HTML、CSS 和 JavaScript)设计、开发和构建跨平台应用程序,同时利用后端强大的 Rust 编程语言。

Tauri 与框架无关。这意味着你可以将它与你选择的任何前端库一起使用——比如 Vue、React、Svelte 等。此外,在基于 Tauri 的项目中使用 Rust 是完全可选的。你可以只使用 Tauri 提供的 JavaScript API 来构建你的整个应用程序。这样,不仅可以轻松构建新应用程序,还可以将已构建的 Web 应用程序的代码库转换为本机桌面应用程序,而几乎不需要更改原始代码。

让我们看看为什么我们应该使用Tauri而不是Electron。

Tauri与Electron:快速比较

构建一个真正伟大的应用程序有三个重要元素。应用必须小巧、快速且安全。Tauri在所有三个方面都优于Electron:

  • Tauri产生更小的二进制文件。正如你从Tauri发布的基准测试结果中看到的那样,即使是一个超级简单的Hello,World!应用程序在使用Electron构建时也可能是一个巨大的大小(超过120 MB)。相比之下,同一个 Tauri 应用程序的二进制大小要小得多,小于 2 MB。在我看来,这令人印象深刻。
  • Tauri应用程序执行得更快。从上面提到的同一页面,您还可以看到Tauri应用程序的内存使用量可能接近同等Electron应用程序的一半。
  • Tauri应用程序是高度安全的。在Tauri网站上,您可以阅读Tauri默认提供的所有内置安全功能。但我想在这里提到的一个值得注意的功能是开发人员可以显式启用或禁用某些 API。这不仅使应用更安全,而且还减小了二进制大小。

构建笔记应用

在本节中,我们将构建一个简单的笔记应用,具有以下功能:

  • 添加和删除笔记
  • 重命名备忘录的标题
  • 在 Markdown 中编辑备忘录的内容
  • 在 HTML 中预览笔记的内容
  • 将笔记保存在本地存储中
  • 将笔记导入和导出到系统硬盘驱动器

您可以在 GitHub 上找到所有项目文件。

开始

要开始使用 Tauri,您首先需要安装 Rust 及其系统依赖项。它们因用户的操作系统而异,因此我不打算在这里探讨它们。请按照文档中适用于您的操作系统的说明进行操作。

准备就绪后,在所选目录中运行以下命令:

npm create tauri-app

这将指导您完成安装过程,如下所示:

$ npm create tauri-app

We hope to help you create something special with Tauri!
You will have a choice of one of the UI frameworks supported by the greater web tech community.
This tool should get you quickly started. See our docs at https://tauri.app/

If you haven't already, please take a moment to setup your system.
You may find the requirements here: https://tauri.app/v1/guides/getting-started/prerequisites  
    
Press any key to continue...
? What is your app name? my-notes
? What should the window title be? My Notes
? What UI recipe would you like to add? create-vite (vanilla, vue, react, svelte, preact, lit) (https://vitejs.dev/guide/#scaffolding-your-first-vite-project)
? Add "@tauri-apps/api" npm package? Yes
? Which vite template would you like to use? react-ts
>> Running initial command(s)
Need to install the following packages:
  create-vite@3.2.1
Ok to proceed? (y) y

>> Installing any additional needed dependencies

added 87 packages, and audited 88 packages in 19s

9 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

added 2 packages, and audited 90 packages in 7s

10 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
>> Updating "package.json"
>> Running "tauri init"

> my-notes@0.0.0 tauri
> tauri init --app-name my-notes --window-title My Notes --dist-dir ../dist --dev-path http://localhost:5173

 What is your frontend dev command? · npm run dev
 What is your frontend build command? · npm run build
>> Updating "tauri.conf.json"
>> Running final command(s)

    Your installation completed.

    $ cd my-notes
    $ npm run tauri dev

请确保您的选择与我所做的选择相匹配,这些选择主要是为具有 Vite 和 TypeScript 支持的 React 应用程序搭建脚手架,并安装 Tauri API 包。

暂时不要运行该应用。首先,我们需要安装项目所需的一些其他软件包。在终端中运行以下命令:

npm install @mantine/core @mantine/hooks @tabler/icons @emotion/react marked-react

这将安装以下软件包:

  • Mantine:一个功能齐全的 React 组件库
  • 桌面图标:超过3000个像素完美的网页设计图标
  • React的情感:用于React 中的简单样式
  • marked-react:用于使用 Markdown 渲染为 React 组件

现在我们已经准备好测试应用程序,但在此之前,让我们看看项目是如何构建的:

my-notes/
├─ node_modules/
├─ public/
├─ src/
│   ├─ assets/
│   │   └─ react.svg
│   ├─ App.css
│   ├─ App.tsx
│   ├─ index.css
│   ├─ main.tsx
│   └─ vite-env.d.ts
├─ src-tauri/
│   ├─ icons/
│   ├─ src/
│   ├─ .gitignore
│   ├─ build.rs
│   ├─ Cargo.toml
│   └─ tauri.config.json
├─ .gitignore
├─ index.html
├─ package-lock.json
├─ package.json
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts

这里最重要的是,应用程序的 React 部分存储在目录中,而 Rust 和其他特定于 Tauri 的文件存储在 .我们需要在Tauri目录中触摸的唯一文件是 ,我们可以在其中配置应用程序。打开此文件并找到密钥。将其内容替换为以下内容:
srcsrc-tauritauri.conf.jsonallowlist

"allowlist": {
  "dialog": {
    "save": true,
    "open": true,
    "ask": true
  },
  "fs": {
    "writeFile": true,
    "readFile": true,
    "scope": ["$DOCUMENT/*", "$DESKTOP/*"]
  },
  "path": {
    "all": true
  },
  "notification": {
    "all": true
  }
},

在这里,出于安全原因,正如我上面提到的,我们只启用我们将在应用中使用的 API。我们还限制对文件系统的访问,只有两个例外 — 和目录。这将允许用户将其笔记仅导出到这些目录。DocumentsDesktop

在关闭文件之前,我们需要再更改一件事。找到密钥。在该密钥下,您将找到该密钥。将其值更改为 。这在应用构建时是必需的,因为标识符必须是唯一的。
bundleidentifiercom.mynotes.dev

我想提到的最后一件事是,在最后一个键中,您可以设置所有与窗口相关的设置:windows

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

如您所见,密钥是根据您在安装过程中为其提供的值为您设置的。title

好的,让我们终于启动应用程序了。在目录中,运行以下命令:my-notes

npm run tauri dev 

您需要等待一段时间,直到 Tauri 设置完成并且所有文件都已首次编译。不用担心。在后续构建中,该过程将快得多。当Tauri准备就绪时,它将自动打开应用程序窗口。下图显示了您应该看到的内容。

注意:在开发模式下运行或构建应用程序后,将在 src-tauri 中创建一个新的目标目录,其中包含所有编译的文件。在开发模式下,它们放置在调试子目录中,在生成模式下,它们放置在发布子目录中。

好的,现在让我们根据我们的需求调整文件。首先,删除 和 文件。然后打开该文件并将其内容替换为以下内容:index.cssApp.cssmain.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider } from '@mantine/core'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <MantineProvider withGlobalStyles withNormalizeCSS>
      <App />
    </MantineProvider>
  </React.StrictMode>
)

这将设置Mantine的组件以供使用。

接下来,打开文件并将其内容替换为以下内容:App.tsx

import { useState } from 'react'
import { Button } from '@mantine/core'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <Button onClick={() => setCount((count) => count + 1)}>count is {count}</Button>
    </div>
  )
}

export default App

现在,如果您查看应用程序窗口,您应该会看到以下内容:

通过单击按钮确保应用程序正常运行。如果出现问题,您可能需要对其进行调试。(请参阅以下注释。

注意:当应用在开发模式下运行时,可以通过右键单击应用窗口并从菜单中选择“检查”来打开 DevTools。

创建基本应用功能

现在,让我们创建应用的框架。将文件的内容替换为以下内容:App.tsx

import { useState } from 'react'
import Markdown from 'marked-react'

import { ThemeIcon, Button, CloseButton, Switch, NavLink, Flex, Grid, Divider, Paper, Text, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'

import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'

function App() {
  const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [  {
    "title": "New note",
    "content": ""
  }] })

  const [active, setActive] = useState(0)
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const [checked, setChecked] = useState(false)

  const handleSelection = (title: string, content: string, index: number) => {
    setTitle(title)
    setContent(content)
    setActive(index)
  }

  const addNote = () => {
    notes.splice(0, 0, {title: "New note", content: ""})
    handleSelection("New note", "", 0)
    setNotes([...notes])
  }

  const deleteNote = async (index: number) => {
    let deleteNote = await ask("Are you sure you want to delete this note?", {
      title: "My Notes",
      type: "warning",
    })
    if (deleteNote) {
      notes.splice(index,1)
      if (active >= index) {
        setActive(active >= 1 ? active - 1 : 0)
      }
      if (notes.length >= 1) {
        setContent(notes[index-1].content)
      } else {
        setTitle("")
        setContent("")
      } 
      setNotes([...notes]) 
    }
  }

  return (
    <div>
      <Grid grow m={10}>
        <Grid.Col span="auto">
          <Flex gap="xl" justify="flex-start" align="center" wrap="wrap">
            <Flex>
              <ThemeIcon size="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
                <IconNotebook size={32} />
              </ThemeIcon>
              <Text color="green" fz="xl" fw={500} ml={5}>My Notes</Text>
            </Flex>
            <Button onClick={addNote} leftIcon={<IconFilePlus />}>Add note</Button>
            <Button.Group>
              <Button variant="light" leftIcon={<IconFileArrowLeft />}>Import</Button>
              <Button variant="light" leftIcon={<IconFileArrowRight />}>Export</Button>
            </Button.Group>
          </Flex>

          <Divider my="sm" />

          {notes.map((note, index) => (
            <Flex key={index}>
              <NavLink onClick={() => handleSelection(note.title, note.content, index)} active={index === active} label={note.title} />
              <CloseButton onClick={() => deleteNote(index)} title="Delete note" size="xl" iconSize={20} />
            </Flex>
          ))} 
        </Grid.Col>
        <Grid.Col span={2}>
          <Switch label="Toggle Editor / Markdown Preview"  checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)}/>

          <Divider my="sm" />

          {checked === false && (
            <div>
              <TextInput mb={5} />
              <Textarea minRows={10} />
            </div>
          )}
          {checked && (
            <Paper shadow="lg" p={10}>
              <Text fz="xl" fw={500} tt="capitalize">{title}</Text>

              <Divider my="sm" />

              <Markdown>{content}</Markdown>
            </Paper>
          )}
        </Grid.Col>
      </Grid>
    </div>
  )
}

export default App

这里有很多代码,所以让我们一点一点地探索它。

导入必要的包

一开始,我们导入所有必要的包,如下所示:

import { useState } from 'react'
import Markdown from 'marked-react'

import { ThemeIcon, Button, CloseButton, Switch, NavLink, Flex, Grid, Divider, Paper, Text, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'

import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'

设置应用存储和变量

在下一部分中,我们使用钩子来设置笔记的存储。useLocalStorage

我们还为当前笔记的标题和内容设置了几个变量,另外两个变量用于确定选择了哪个笔记 () 以及是否启用了 Markdown 预览 ()。activechecked

最后,我们创建一个实用程序函数来处理音符的选择。选择注释后,它将相应地更新当前注释的属性:

const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [  {
  "title": "New note",
  "content": ""
}] })

const [active, setActive] = useState(0)
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [checked, setChecked] = useState(false)

const handleSelection = (title: string, content: string, index: number) => {
  setTitle(title)
  setContent(content)
  setActive(index)
}

添加添加/删除注释功能

接下来的两个函数用于添加/删除注释。

addNote()在数组中插入新的注释对象。它用于在添加新注释后自动选择新注释。最后,它会更新笔记。我们在这里使用点差运算符的原因是,否则状态不会更新。这样,我们强制更新状态和重新渲染组件,因此注释将正确显示:noteshandleSelection()

const addNote = () => {
  notes.splice(0, 0, {title: "New note", content: ""})
  handleSelection("New note", "", 0)
  setNotes([...notes])
}

const deleteNote = async (index: number) => {
  let deleteNote = await ask("Are you sure you want to delete this note?", {
    title: "My Notes",
    type: "warning",
  })
  if (deleteNote) {
    notes.splice(index,1)
    if (active >= index) {
      setActive(active >= 1 ? active - 1 : 0)
    }
    if (notes.length >= 1) {
      setContent(notes[index-1].content)
    } else {
      setTitle("")
      setContent("")
    } 
    setNotes([...notes]) 
  }
}

deleteNote()使用该对话框确认用户是否要删除注释,并且没有意外单击“删除”按钮。如果用户确认删除 (),则执行该语句:askdeleteNote = trueif

  • 注释从阵列中删除notes
  • 变量已更新active
  • 当前笔记的标题和内容已更新
  • 数组已更新notes

创建 JSX 模板

在模板部分,我们有两列。

在第一列中,我们创建应用程序徽标和名称,以及用于添加、导入和导出笔记的按钮。我们还通过数组循环来渲染音符。在这里,我们再次使用在单击笔记标题链接时正确更新当前笔记的属性:noteshandleSelection()

<Grid.Col span="auto">
  <Flex gap="xl" justify="flex-start" align="center" wrap="wrap">
    <Flex>
      <ThemeIcon size="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
        <IconNotebook size={32} />
      </ThemeIcon>
      <Text color="green" fz="xl" fw={500} ml={5}>My Notes</Text>
    </Flex>
    <Button onClick={addNote} leftIcon={<IconFilePlus />}>Add note</Button>
    <Button.Group>
      <Button variant="light" leftIcon={<IconFileArrowLeft />}>Import</Button>
      <Button variant="light" leftIcon={<IconFileArrowRight />}>Export</Button>
    </Button.Group>
  </Flex>

  <Divider my="sm" />

  {notes.map((note, index) => (
    <Flex key={index}>
      <NavLink onClick={() => handleSelection(note.title, note.content, index)} active={index === active} label={note.title} />
      <CloseButton onClick={() => deleteNote(index)} title="Delete note" size="xl" iconSize={20} />
    </Flex>
  ))} 
</Grid.Col>

在第二列中,我们添加一个切换按钮,用于在笔记编辑和预览模式之间切换。在编辑模式下,当前笔记的标题有一个文本输入,当前笔记的内容有一个文本区域。在预览模式下,标题由 Mantine 的组件渲染,内容由 的组件渲染:Textmarked-reactMarkdown

<Grid.Col span={2}>
  <Switch label="Toggle Editor / Markdown Preview"  checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)}/>

  <Divider my="sm" />

  {checked === false && (
    <div>
      <TextInput mb={5} />
      <Textarea minRows={10} />
    </div>
  )}
  {checked && (
    <Paper shadow="lg" p={10}>
      <Text fz="xl" fw={500} tt="capitalize">{title}</Text>

      <Divider my="sm" />

      <Markdown>{content}</Markdown>
    </Paper>
  )}
</Grid.Col>

唷!那是很多代码。下图显示了我们的应用此时应查看的内容。

伟大!我们现在可以添加和删除笔记,但无法编辑它们。我们将在下一节中添加此功能。

添加笔记的标题和内容更新功能

在函数后添加以下代码:deleteNote()

const updateNoteTitle = ({ target: { value } }: { target: { value: string } }) => {
  notes.splice(active, 1, { title: value, content: content })
  setTitle(value)
  setNotes([...notes])
}

const updateNoteContent = ({target: { value } }: { target: { value: string } }) => {
  notes.splice(active, 1, { title: title, content: value })
  setContent(value)
  setNotes([...notes])
}

这两个函数分别替换当前笔记的标题和/或内容。为了使它们工作,我们需要将它们添加到模板中:

<TextInput value={title} onChange={updateNoteTitle} mb={5} />
<Textarea value={content} onChange={updateNoteContent} minRows={10} />

现在,当选择注释时,其标题和内容将分别显示在输入文本和文本区域中。当我们编辑笔记时,其标题将相应更新。

我添加了一些注释来演示应用程序的外观。带有所选笔记及其内容的应用程序如下图所示。

下图显示了我们笔记的预览。

下图显示了在笔记删除期间显示的确认对话框。

伟大!要使我们的应用程序真正酷炫,我们需要做的最后一件事是添加将用户的笔记导出和导入系统硬盘驱动器的功能。

添加导入和导出笔记的功能

在函数后添加以下代码:updateNoteContent()

const exportNotes = async () => {
  const exportedNotes = JSON.stringify(notes)
  const filePath = await save({
    filters: [{
      name: "JSON",
      extensions: ["json"]
    }]
  })
  await writeTextFile(`${filePath}`, exportedNotes)
  sendNotification(`Your notes have been successfully saved in ${filePath} file.`)
}

const importNotes = async () => {
  const selectedFile = await open({
    filters: [{
      name: "JSON",
      extensions: ["json"]
    }]
  })
  const fileContent = await readTextFile(`${selectedFile}`)
  const importedNotes = JSON.parse(fileContent)
  setNotes(importedNotes)
}

在第一个函数中,我们将笔记转换为 JSON。然后我们使用对话框保存笔记。接下来,我们使用函数将文件物理写入磁盘。最后,我们使用该功能通知用户笔记已成功保存以及保存位置。savewriteTextFile()sendNotification()

在第二个函数中,我们使用对话框从磁盘中选择一个包含注释的 JSON 文件。然后使用该函数读取文件,将其 JSON 内容转换为对象,最后使用新内容更新 notes 存储。openreadTextFile()

我们需要做的最后一件事是更改模板以使用上述功能:

<Button variant="light" onClick={importNotes} leftIcon={<IconFileArrowLeft />}>Import</Button>
<Button variant="light" onClick={exportNotes} leftIcon={<IconFileArrowRight />}>Export</Button>

下面是最终的 App.tsx 文件的外观。

在接下来的屏幕截图中,您可以看到“另存为”和“打开”对话框,并保存显示为注释的系统通知。

恭喜!您刚刚构建了一个具有 Tauri 功能的全功能笔记桌面应用程序。

构建应用

现在,如果一切正常并且您对最终结果感到满意,则可以构建应用程序并获取操作系统的安装包。为此,请运行以下命令:

npm run tauri build

结论

在本教程中,我们探讨了 Tauri 是什么,为什么与 Electron 相比,它是构建本机桌面应用程序的更好选择,最后是如何构建一个简单但功能齐全的 Tauri 应用程序。

我希望你和我一样喜欢这段短暂的旅程。要更深入地了解Tauri世界,请查看其文档并继续尝试其强大的功能。

Tags:

最近发表
标签列表