feat: add documentation for plugins (#1412)

* feat: add documentation for plugins

* update prettierignore

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* update contributing guide

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* add notification guide stub

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* update guide

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* revise lifecycle events

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* some more updates and revisions

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* Formatting

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>

* resolve some todos

* Update src/content/docs/2/guide/plugins/index.mdx

* Update src/content/docs/2/guide/plugins/index.mdx

* format

* The big refactor

* Fix broken links, add todo

* rework sidebar

* typos and minor fixes

* fix link

* Apply suggestions from code review

* why??

* fill "develop an iOS plugin" section

* Apply suggestions from code review

* enable i18n

* fix typo

---------

Signed-off-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
Co-authored-by: Lorenzo Lewis <lorenzo_lewis@icloud.com>
This commit is contained in:
Lucas Fernandes Nogueira
2023-09-07 13:05:12 -03:00
committed by GitHub
parent 8d8a05a146
commit 2d970c97d0
9 changed files with 736 additions and 82 deletions

View File

@@ -52,7 +52,17 @@ While Tauri 2.0 is still in the prerelease stage people follow these guidelines
### Writing Style
Any ideas? Put them here!
**Dictionary**
| Word | Description |
| -------- | ------------------------------------ |
| app | A Tauri app, prefer over application |
| web view | Where the UI is rendered |
- Use an [oxford comma](https://www.grammarly.com/blog/what-is-the-oxford-comma-and-why-do-people-care-so-much-about-it/) in paragraphs, but not in headings and titles
- Use [title case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case) for headings and titles
- Make headings as succinct as possible to help the reader quickly find the content they need
- Use [simple present tense](https://www.grammarly.com/blog/simple-present/) for verbs
### Guide
@@ -90,4 +100,4 @@ Topics that are around understanding something can be written as a blog post (we
Thanks for your interest in helping to translate the documentation! Visit the [translation status page](https://beta.tauri.app/contribute/translate-status) to see which docs are ready for translation, need updated, or need reviewed.
Read the [Translating Guide](./TRANSLATING.md) for more information.
Read the [Translating Guide](./TRANSLATING.md) for more information.

View File

@@ -14,3 +14,4 @@ pnpm-lock.yaml
# Configs
.github
!.github/**.md

View File

@@ -115,7 +115,7 @@ export default defineConfig({
],
},
{
label: 'Workflow',
label: 'Guides',
items: [
{
label: 'Develop',
@@ -137,6 +137,10 @@ export default defineConfig({
label: 'Distribute',
link: '2/guide/distribute',
},
{
label: 'Plugin Development',
link: '2/guide/plugins',
},
],
},
{

View File

@@ -0,0 +1,7 @@
---
title: Commands
---
import Stub from '@components/Stub.astro';
<Stub />

View File

@@ -0,0 +1,385 @@
---
title: Mobile Plugin Development
i18nReady: true
---
:::tip[Plugin Development]
Be sure that you're familiar with the concepts covered in the [Plugin Development guide](/2/guide/plugins) as many concepts in this guide build on top of foundations covered there.
:::
Plugins can run native mobile code written in Kotlin (or Java) and Swift. The default plugin template includes an Android library project using Kotlin and a Swift package including an example mobile command showing how to trigger its execution from Rust code.
## Initialize Plugin Project
Follow the steps in the [Plugin Development guide](/2/guide/plugins#initialize-plugin-project) to initialize a new plugin project.
If you have an existing plugin and would like to add Android or iOS capabilities to it, you can use `plugin android add` and `plugin ios add` to bootstrap the mobile library projects and guide you through the changes needed.
The default plugin template splits the plugin's implementation into two separate modules: `desktop.rs` and `mobile.rs`.
The desktop implementation uses Rust code to implement a functionality, while the mobile implementation sends a message to the native mobile code to execute a function and get a result back. If shared logic is needed across both implementations, it can be defined in `lib.rs`:
```rust
// lib.rs
use tauri::Runtime;
impl<R: Runtime> <plugin-name><R> {
pub fn do_something(&self) {
// do something that is a shared implementation between desktop and mobile
}
}
```
This implementation simplifies the process of sharing an API that can be used both by commands and Rust code.
### Develop an Android Plugin
A Tauri plugin for Android is defined as a Kotlin class that extends `app.tauri.plugin.Plugin` and is annoted with `app.tauri.annotation.TauriPlugin`. Each method annotated with `app.tauri.annotation.Command` can be called by Rust or JavaScript.
Tauri uses Kotlin by default for the Android plugin implementation, but you can switch to Java if you prefer. After generating a plugin, right click the Kotlin plugin class in Android Studio and select the "Convert Kotlin file to Java file" option from the menu. Android Studio will guide you through the project migration to Java.
### Develop an iOS Plugin
A Tauri plugin for iOS is defined as a Swift class that extends the `Plugin` class from the `Tauri` package. Each function with the `@objc` attribute and the `(_ invoke: Invoke)` parameter (for example `@objc private func download(_ invoke: Invoke) { }`) can be called by Rust or JavaScript.
The plugin is defined as a [Swift package](https://www.swift.org/package-manager/) so that you can use its package manager to manage dependencies.
## Plugin Configuration
Refer to the [Plugin Configuration section](/2/guide/plugins/#plugin-configuration) of the Plugin Development guide for more details on developing plugin configurations.
The plugin instance on mobile has a getter for the plugin configuration:
<Tabs>
<TabItem label="Android">
```java
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
val timeout = this.config.getInt("timeout", 30)
}
}
```
</TabItem>
<TabItem label="iOS">
```swift
class ExamplePlugin: Plugin {
@objc public override func load(webview: WKWebView) {
let timeout = self.config["timeout"] as? Int ?? 30
}
}
```
</TabItem>
</Tabs>
## Lifecycle Events
Plugins can hook into several lifecycle events:
- [load](#load): When the plugin is loaded into the web view
- [onNewIntent](#onnewintent): Android only, when the activity is re-launched
There are also the additional [lifecycle events for plugins](/2/guide/plugins#lifecycle-events) in the Plugin Development guide.
### load
- **When**: When the plugin is loaded into the web view
- **Why**: Execute plugin initialization code
<Tabs>
<TabItem label="Android">
```java
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
// perform plugin setup here
}
}
```
</TabItem>
<TabItem label="iOS">
```swift
class ExamplePlugin: Plugin {
@objc public override func load(webview: WKWebView) {
let timeout = self.config["timeout"] as? Int ?? 30
}
}
```
</TabItem>
</Tabs>
### onNewIntent
**Note**: This is only available on Android.
- **When**: When the activity is re-launched. See [Activity#onNewIntent](<https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)>) for more information.
- **Why**: Handle application re-launch such as when a notification is clicked or a deep link is accessed.
```java
// import android.content.Intent
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun onNewIntent(intent: Intent) {
// handle new intent event
}
}
```
## Adding Mobile Commands
There is a plugin class inside the respective mobile projects where commands can be defined that can be called by the Rust code:
import { Tabs, TabItem } from '@astrojs/starlight/components';
<Tabs>
<TabItem label="Android">
```java
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
@Command
fun openCamera(invoke: Invoke) {
val allowEdit = invoke.getBoolean("allowEdit", false)
val quality = invoke.getInt("quality", 100)
val ret = JSObject()
ret.put("path", "/path/to/photo.jpg")
invoke.resolve(ret)
}
}
```
</TabItem>
<TabItem label="iOS">
```swift
class ExamplePlugin: Plugin {
@objc public func openCamera(_ invoke: Invoke) {
let allowEdit = invoke.getBool("allowEdit", false)
let quality = invoke.getInt("quality", 100)
invoke.resolve(["path": "/path/to/photo.jpg"])
}
}
```
</TabItem>
</Tabs>
Use the [`tauri::plugin::PluginHandle`](https://docs.rs/tauri/2.0.0-alpha/tauri/plugin/struct.PluginHandle.html) to call a mobile command from Rust:
```rust
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::Runtime;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraRequest {
quality: usize,
allow_edit: bool,
}
#[derive(Deserialize)]
pub struct Photo {
path: PathBuf,
}
impl<R: Runtime> <plugin-name;pascal-case><R> {
pub fn open_camera(&self, payload: CameraRequest) -> crate::Result<Photo> {
self
.0
.run_mobile_plugin("openCamera", payload)
.map_err(Into::into)
}
}
```
## Permissions
If a plugin requires permissions from the end user, Tauri simplifies the process of checking and requesting permissions.
<Tabs>
<TabItem label="Android">
First define the list of permissions needed and an alias to identify each group in code. This is done inside the `TauriPlugin` annotation:
```java
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
]
)
class ExamplePlugin(private val activity: Activity): Plugin(activity) { }
```
</TabItem>
<TabItem label="iOS">
First override the `checkPermissions` and `requestPermissions` functions:
```swift
class ExamplePlugin: Plugin {
@objc open func checkPermissions(_ invoke: Invoke) {
invoke.resolve(["postNotification": "prompt"])
}
@objc public override func requestPermissions(_ invoke: Invoke) {
// request permissions here
// then resolve the request
invoke.resolve(["postNotification": "granted"])
}
}
```
</TabItem>
</Tabs>
Tauri automatically implements two commands for the plugin: `checkPermissions` and `requestPermissions`. Those commands can be directly called from JavaScript or Rust:
{/* TODO: PermissionState type should be exported in Tauri */}
<Tabs>
<TabItem label="JavaScript">
```javascript
import { invoke } from '@tauri-apps/api/tauri'
type PermissionState = 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'
interface Permissions {
postNotification: PermissionState
}
// check permission state
const permission = await invoke<Permissions>('plugin:<plugin-name>|checkPermissions')
if (permission.postNotification === 'prompt-with-rationale') {
// show information to the user about why permission is needed
}
// request permission
if (permission.postNotification.startsWith('prompt')) {
const state = await invoke<Permissions>('plugin:<plugin-name>|requestPermissions', { permissions: ['postNotification'] })
}
```
</TabItem>
<TabItem label="Rust">
```rust
use serde::{Serialize, Deserialize};
use tauri::Runtime;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PermissionResponse {
pub post_notification: PermissionState,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RequestPermission {
post_notification: bool,
}
impl<R: Runtime> Notification<R> {
pub fn request_post_notification_permission(&self) -> crate::Result<PermissionState> {
self.0
.run_mobile_plugin::<PermissionResponse>("requestPermissions", RequestPermission { post_notification: true })
.map(|r| r.post_notification)
.map_err(Into::into)
}
pub fn check_permissions(&self) -> crate::Result<PermissionResponse> {
self.0
.run_mobile_plugin::<PermissionResponse>("checkPermissions", ())
.map_err(Into::into)
}
}
```
</TabItem>
</Tabs>
## Plugin Events
{/* TODO: Is this section a duplicate of Lifecycle Events above? */}
Plugins can emit events at any point of time using the `trigger` function:
<Tabs>
<TabItem label="Android">
```java
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
trigger("load", JSObject())
}
override fun onNewIntent(intent: Intent) {
// handle new intent event
if (intent.action == Intent.ACTION_VIEW) {
val data = intent.data.toString()
val event = JSObject()
event.put("data", data)
trigger("newIntent", event)
}
}
@Command
fun openCamera(invoke: Invoke) {
val payload = JSObject()
payload.put("open", true)
trigger("camera", payload)
}
}
```
</TabItem>
<TabItem label="iOS">
```swift
class ExamplePlugin: Plugin {
@objc public override func load(webview: WKWebView) {
trigger("load", data: [:])
}
@objc public func openCamera(_ invoke: Invoke) {
trigger("camera", data: ["open": true])
}
}
```
</TabItem>
</Tabs>
The helper functionss can then be called from the NPM package by using the [`addPluginListener`](/2/reference/js/core/namespacetauri/#addpluginlistener) helper function:
```javascript
import { addPluginListener, PluginListener } from '@tauri-apps/api/tauri';
export async function onRequest(
handler: (url: string) => void
): Promise<PluginListener> {
return await addPluginListener(
'<plugin-name>',
'event-name',
handler
);
}
```

View File

@@ -1,71 +0,0 @@
---
title: Develop a Tauri Plugin
---
import Stub from '@components/Stub.astro';
<Stub />
## `plugin init`
```sh
cargo tauri plugin init
```
```
Initializes a Tauri plugin project
Usage: cargo tauri plugin init [OPTIONS] --name <PLUGIN_NAME>
Options:
-n, --name <PLUGIN_NAME> Name of your Tauri plugin
-v, --verbose... Enables verbose logging
--no-api Initializes a Tauri plugin without the TypeScript API
-d, --directory <DIRECTORY> Set target directory for init [default: C:\Users\Fabian-Lars]
-t, --tauri-path <TAURI_PATH> Path of the Tauri project to use (relative to the cwd)
-a, --author <AUTHOR> Author name
-h, --help Print help
-V, --version Print version
```
## `plugin android`
```sh
cargo tauri plugin android
```
```
Manage the Android project for Tauri plugins
Usage: cargo tauri plugin android [OPTIONS] <COMMAND>
Commands:
add Adds the Android project to an existing Tauri plugin
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose... Enables verbose logging
-h, --help Print help
-V, --version Print version
```
## `plugin ios`
```sh
cargo tauri plugin ios
```
```
Manage the iOS project for Tauri plugins
Usage: cargo tauri plugin ios [OPTIONS] <COMMAND>
Commands:
add Adds the iOS project to an existing Tauri plugin
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose... Enables verbose logging
-h, --help Print help
-V, --version Print version
```

View File

@@ -1,13 +1,303 @@
---
title: Plugins
title: Plugin Development
i18nReady: true
---
import Stub from '@components/Stub.astro';
{/* TODO: Add a CLI section */}
<Stub>
import CommandTabs from '@components/CommandTabs.astro';
- What is a Plugin?
- [Developing a Plugin](/2/guide/plugins/develop-plugin)
- Guide/Recipe for each plugin in https://github.com/tauri-apps/plugins-workspace/tree/v2
{/* TODO: Link to windowing system, commands for sending messages, and event system */}
</Stub>
:::tip[Plugin Development]
This guide is for developing Tauri plugins. If you're looking for a list of the currently available plugins and how to use them then visit the [Features and Recipes list](/2/guide/list).
:::
Plugins are able to hook into the Tauri lifecycle, expose Rust code that relies on the web view APIs, handle commands with Rust, Kotlin or Swift code, and much more.
Tauri offers a windowing system with web view functionality, a way to send messages between the Rust process and the web view, and an event system along with several tools to enhance the development experience. By design, the Tauri core does not contain features not needed by everyone. Instead it offers a mechanism to add external functionalities into a Tauri application called plugins.
A Tauri plugin is composed of a Cargo crate and an optional NPM package that provides API bindings for its commands and events. Additionally, a plugin project can include an Android library project and a Swift package for iOS. You can learn more about developing plugins for Android and iOS in the [Mobile Plugin Development guide](/2/guide/plugins/develop-mobile).
{/* TODO: https://github.com/tauri-apps/tauri/issues/7749 */}
## Naming Convention
{/* TODO: Add link to allowlist */}
Tauri plugins have a prefix (`tauri-plugin-` prefix for the Rust crate name and `@tauri-apps/plugin-` for the NPM package) followed by the plugin name. The plugin name is specified on the plugin configuration under [`tauri.conf.json > plugin`](/2/reference/config/#pluginconfig) and on the allowlist configuration.
By default Tauri prefixes your plugin crate with `tauri-plugin-`. This helps your plugin to be discovered by the Tauri community, but is not not a requirement. When initializing a new plugin project, you must provide its name. The generated crate name will be `tauri-plugin-{plugin-name}` and the JavaScript NPM package name will be `tauri-plugin-{plugin-name}-api` (although we recommend using an [NPM scope](https://docs.npmjs.com/about-scopes) if possible). The Tauri naming convention for NPM packages is `@scope-name/plugin-{plugin-name}`.
## Initialize Plugin Project
To bootstrap a new plugin project, run `plugin init`. If you do not need the NPM package, use the `--no-api` CLI flag.
<CommandTabs
npm="npm run tauri plugin init"
yarn="yarn tauri plugin init"
pnpm="pnpm tauri plugin init"
cargo="cargo tauri plugin init"
/>
This will initialize the plugin and the resulting code will look like this:
```
. plugin-name/
├── src/ - Rust code
│ ├── commands.rs - defines the commands the webview can use
| ├── desktop.rs - desktop implementation
│ ├── lib.rs - re-exports appropriate implementation, setup state...
│ └── mobile.rs - mobile implementation
├── android - Android library
├── ios - Swift package
├── webview-src - source code of the JavaScript API bindings
├── webview-dist - Transpiled assets from webview-src
├── Cargo.toml - Cargo crate metadata
└── package.json - NPM package metadata
```
{/* TODO: https://github.com/tauri-apps/tauri/issues/7749 */}
If you have an existing plugin and would like to add Android or iOS capabilities to it, you can use `plugin android add` and `plugin ios add` to bootstrap the mobile library projects and guide you through the changes needed.
## Mobile Plugin Development
Plugins can run native mobile code written in Kotlin (or Java) and Swift. The default plugin template includes an Android library project using Kotlin and a Swift package. It includes an example mobile command showing how to trigger its execution from Rust code.
Read more about developing plugins for mobile in the [Mobile Plugin Development guide](/2/guide/plugins/develop-mobile).
## Plugin Configuration
In the Tauri application where the plugin is used, the plugin configuration is specified on `tauri.conf.json` where `plugin-name` is the name of the plugin:
```json
{
"build": { ... },
"tauri": { ... },
"plugins": {
"plugin-name": {
"timeout": 30
}
}
}
```
The plugin's configuration is set on the `Builder` and is parsed at runtime. Here is an example of the `Config` struct being used to specify the plugin configuration:
```rust
// lib.rs
use tauri::plugin::{Builder, Runtime, TauriPlugin};
use serde::Deserialize;
// Define the plugin config
#[derive(Deserialize)]
struct Config {
timeout: usize,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
// Make the plugin config optional
// by using `Builder::<R, Option<Config>>` instead
Builder::<R, Config>::new("<plugin-name>")
.setup(|app, api| {
let timeout = api.config.timeout;
Ok(())
})
.build()
}
```
## Lifecycle Events
Plugins can hook into several lifecycle events:
- [setup](#setup): Plugin is being initialized
- [on_navigation](#on_navigation): Web view is attempting to perform navigation
- [on_webview_ready](#on_webview_ready): New window is being created
- [on_event](#on_event): Event loop events
- [on_drop](#on_drop): Plugin is being deconstructed
There are additional [lifecycle events for mobile plugins](/2/guide/plugins/develop-mobile#lifecycle-events).
### setup
- **When**: Plugin is being initialized
- **Why**: Register mobile plugins, manage state, run background tasks
```rust
use tauri::{Manager, plugin::Builder};
use std::{collections::HashMap, sync::Mutex, time::Duration};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, api| {
app.manage(DummyStore(Default::default()));
let app_ = app.clone();
std::thread::spawn(move || {
loop {
app_.emit("tick", ());
std::thread::sleep(Duration::from_secs(1));
}
});
Ok(())
})
```
### on_navigation
- **When**: Web view is attempting to perform navigation
- **Why**: Validate the navigation or track URL changes
Returning `false` cancels the navigation.
```rust
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_navigation(|window, url| {
println!("window {} is navigating to {}", window.label(), url);
// Cancels the navigation if forbidden
url.scheme() != "forbidden"
})
```
### on_webview_ready
- **When**: New window has been created
- **Why**: Execute an initialization script for every window
```rust
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_webview_ready(|window| {
window.listen("content-loaded", |event| {
println!("webview content has been loaded");
});
})
```
### on_event
- **When**: Event loop events
- **Why**: Handle core events such as window events, menu events and application exit requested
With this lifecycle hook you can be notified of any event loop [events](https://docs.rs/tauri/2.0.0-alpha/tauri/enum.RunEvent.html).
```rust
use std::{collections::HashMap, fs::write, sync::Mutex};
use tauri::{plugin::Builder, Manager, RunEvent};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, _api| {
app.manage(DummyStore(Default::default()));
Ok(())
})
.on_event(|app, event| {
match event {
RunEvent::ExitRequested { api, .. } => {
// user requested a window to be closed and there's no windows left
// we can prevent the app from exiting:
api.prevent_exit();
}
RunEvent::Exit => {
// app is going to exit, you can cleanup here
let store = app.state::<DummyStore>();
write(
app.path().app_local_data_dir().unwrap().join("store.json"),
serde_json::to_string(&*store.0.lock().unwrap()).unwrap(),
)
.unwrap();
}
_ => {}
}
})
```
### on_drop
- **When**: Plugin is being deconstructed
- **Why**: Execute code when the plugin has been destroyed
See [`Drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html) for more information.
```rust
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_drop(|app| {
// plugin has been destroyed...
})
```
## Exposing Rust APIs
The plugin APIs defined in the project's `desktop.rs` and `mobile.rs` are exported to the user as a struct with the same name as the plugin (in pascal case). When the plugin is setup, an instance of this struct is created and managed as a state so that users can retrieve it at any point in time with a `Manager` instance (such as `AppHandle`, `App`, or` Window`) through the extension trait defined in the plugin.
For example, the [`global-shortcut plugin`](/2/guide/global-shortcut) defines a `GlobalShortcut` struct that can be read by using the `global_shortcut` method of the `GlobalShortcutExt` trait:
```rust
use tauri_plugin_global_shortcut::GlobalShortcutExt;
tauri::Builder::default()
.plugin(tauri_plugin_global_shortcut::init())
.setup(|app| {
app.global_shortcut().register(...);
Ok(())
})
```
## Adding Commands
Commands are defined in the `commands.rs` file. They are regular Tauri applications commands. They can access the AppHandle and Window instances directly, access state, and take input the same way as application commands. Read the [Commands guide](/2/guide/commands) for more details on Tauri commands.
This command shows how to get access to the `AppHandle` and `Window` instance via dependency injection, and takes two input parameters (`on_progress` and `url`):
```rust
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]
async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) {
// implement command logic here
on_progress.send(100).unwrap();
}
```
To expose the command to the webview, you must hook into the `invoke_handler()` call in `lib.rs`:
```rust
// lib.rs
Builder::new("<plugin-name>")
.invoke_handler(tauri::generate_handler![commands::upload])
```
Define a binding function in `webview-src/index.ts` so that plugin users can easily call the command in JavaScript:
```js
// webview-src/index.ts
import { invoke, Channel } from '@tauri-apps/api/tauri'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> {
const onProgress = new Channel<number>()
onProgress.onmessage = onProgressHandler
await invoke('plugin:<plugin-name>|upload', { url, onProgress })
}
```
Be sure to build the TypeScript code prior to testing it.
## Managing State
A plugin can manage state in the same way a Tauri application does. Read the [State Management guide](/2/guide/state-management) for more information.

View File

@@ -0,0 +1,28 @@
---
title: State Management
---
import Stub from '@components/Stub.astro';
<Stub>
Define a state struct and let Tauri manage it:
```rust
use std::{collections::HashMap, sync::Mutex};
use tauri::{Manager, plugin::Builder};
#[derive(Default)]
struct Store(Mutex<HashMap<usize, String>>);
Builder::new("<plugin-name>")
.setup(|app, api| {
app.manage(Store::default());
// retrieve the store later
let store = app.state::<Store>();
Ok(())
})
```
</Stub>

View File

@@ -19,7 +19,7 @@ You can add the Tauri CLI to your current project using your package manager of
:::tip[Developing a Plugin]
For CLI commands related to developing plugins visit the [Develop a Tauri Plugin](/2/guide/plugins/develop-plugin) guide.
For CLI commands related to developing plugins visit the [Develop a Tauri Plugin guide](/2/guide/plugins).
:::