mcpField
R
running-nodejs-sidecar-in-tauri

running-nodejs-sidecar-in-tauri

github.com/dchuk开发36145 次安装40 周前更新
代码仓库

关于 running-nodejs-sidecar-in-tauri

running-nodejs-sidecar-in-tauri 是一项将 Node.js 应用打包为 Tauri 桌面应用 sidecar 的技能指南,面向希望在应用中运行 Node.js 工具的开发者。它说明了打包、跨平台目标配置,以及通过 Tauri Shell 插件进行进程通信的方式。

主要功能

  1. 01将 Node.js 应用打包为 sidecar
  2. 02配置 Tauri 外部二进制文件
  3. 03通过 Shell 插件调用 sidecar
  4. 04处理标准输出和流式输出
  5. 05使用 HTTP 与长驻进程通信

适用场景

  1. 01在 Tauri 桌面应用中运行现有 Node.js 工具
  2. 02无需用户单独安装 Node.js 即可分发应用
  3. 03通过 sidecar 使用缺少 Rust 等效实现的 npm 包

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill running-nodejs-sidecar-in-tauri

安装后在 Claude Code 中输入/running-nodejs-sidecar-in-tauri确认已加载。

详细介绍

在 Tauri 中将 Node.js 作为 Sidecar 运行

将 Node.js 应用打包并作为 Tauri 桌面应用中的 Sidecar 进程运行,借助 Node.js 生态系统,无需用户安装 Node.js。

为什么使用 Node.js Sidecar

  • 将现有 Node.js 工具和库与 Tauri 应用一起打包
  • 终端用户无需依赖外部 Node.js 运行时
  • 使用没有 Rust 替代方案的 npm 包
  • 将 Node.js 逻辑与 Tauri 主进程隔离
  • 跨平台支持(Windows、macOS、Linux)

前提条件

  1. 已有 Tauri v2 应用
  2. 已安装并配置 Shell 插件
  3. 开发机器上已安装 Node.js 和 npm
  4. Rust 工具链(推荐 1.84.0+)
安装 Shell 插件
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml

在 src-tauri/src/lib.rs 中注册:

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

项目结构

my-tauri-app/
├── package.json
├── src-tauri/
│   ├── binaries/
│   │   └── my-sidecar-<target-triple>[.exe]
│   ├── capabilities/default.json
│   ├── tauri.conf.json
│   └── src/lib.rs
├── sidecar/
│   ├── package.json
│   ├── index.js
│   └── rename.js
└── src/

分步设置

1. 创建 Sidecar 目录
mkdir sidecar && cd sidecar
npm init -y
npm add @yao-pkg/pkg --save-dev
2. 编写 Sidecar 逻辑

创建 sidecar/index.js:

const command = process.argv[2];
const args = process.argv.slice(3);

switch (command) {
  case 'hello':
    console.log(`Hello ${args[0] || 'World'}!`);
    break;
  case 'add':
    const [a, b] = args.map(Number);
    if (isNaN(a) || isNaN(b)) {
      console.error('Error: Both arguments must be numbers');
      process.exit(1);
    }
    console.log(JSON.stringify({ result: a + b }));
    break;
  default:
    console.error(`Unknown command: ${command}`);
    process.exit(1);
}
3. 创建重命名脚本

创建 sidecar/rename.js:

import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';

const ext = process.platform === 'win32' ? '.exe' : '';

let targetTriple;
try {
  targetTriple = execSync('rustc --print host-tuple').toString().trim();
} catch {
  const rustInfo = execSync('rustc -vV').toString();
  const match = rustInfo.match(/host: (.+)/);
  targetTriple = match ? match[1] : null;
  if (!targetTriple) {
    console.error('Could not determine Rust target triple');
    process.exit(1);
  }
}

const destDir = path.join('..', 'src-tauri', 'binaries');
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });

fs.renameSync(`my-sidecar${ext}`, path.join(destDir, `my-sidecar-${targetTriple}${ext}`));
4. 配置构建脚本

更新 sidecar/package.json:

{
  "name": "my-sidecar",
  "type": "module",
  "scripts": {
    "build": "pkg index.js --output my-sidecar --targets node18",
    "postbuild": "node rename.js"
  },
  "devDependencies": {
    "@yao-pkg/pkg": "^5.0.0"
  }
}
5. 配置 Tauri

添加到 src-tauri/tauri.conf.json:

{
  "bundle": {
    "externalBin": ["binaries/my-sidecar"]
  }
}
6. 配置权限

更新 src-tauri/capabilities/default.json:

{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{
        "args": true,
        "name": "binaries/my-sidecar",
        "sidecar": true
      }]
    }
  ]
}

限制参数以确保安全:

{
  "identifier": "shell:allow-execute",
  "allow": [
    {
      "args": ["hello", { "validator": "\\w+" }],
      "name": "binaries/my-sidecar",
      "sidecar": true
    }
  ]
}

通信模式

前端到 Sidecar(TypeScript)
import { Command } from '@tauri-apps/plugin-shell';

async function sayHello(name: string): Promise<string> {
  const command = Command.sidecar('binaries/my-sidecar', ['hello', name]);
  const output = await command.execute();
  if (output.code !== 0) throw new Error(output.stderr);
  return output.stdout.trim();
}

async function addNumbers(a: number, b: number): Promise<number> {
  const command = Command.sidecar('binaries/my-sidecar', ['add', String(a), String(b)]);
  const output = await command.execute();
  if (output.code !== 0) throw new Error(output.stderr);
  return JSON.parse(output.stdout).result;
}
后端到 Sidecar(Rust)
use tauri_plugin_shell::ShellExt;

#[tauri::command]
async fn call_sidecar(
    app: tauri::AppHandle,
    command: String,
    args: Vec<String>,
) -> Result<String, String> {
    let mut sidecar = app.shell().sidecar("my-sidecar").map_err(|e| e.to_string())?;
    sidecar = sidecar.arg(&command);
    for arg in args {
        sidecar = sidecar.arg(&arg);
    }
    let output = sidecar.output().await.map_err(|e| e.to_string())?;
    if output.status.success() {
        String::from_utf8(output.stdout).map_err(|e| e.to_string())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).to_string())
    }
}

注册命令:

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![call_sidecar])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
流式输出
import { Command } from '@tauri-apps/plugin-shell';

async function runWithStreaming(args: string[]): Promise<void> {
  const command = Command.sidecar('binaries/my-sidecar', args);
  command.on('close', (data) => console.log(`Finished: ${data.code}`));
  command.on('error', (error) => console.error(`Error: ${error}`));
  command.stdout.on('data', (line) => console.log(`stdout: ${line}`));
  command.stderr.on('data', (line) => console.error(`stderr: ${line}`));
  await command.spawn();
}

长时间运行的 HTTP Sidecar

对于持久运行的进程,请使用 HTTP:

sidecar/index.js:

import http from 'http';

const PORT = process.env.SIDECAR_PORT || 3333;

const server = http.createServer((req, res) => {
  let body = '';
  req.on('data', (chunk) => (body += chunk));
  req.on('end', () => {
    res.setHeader('Content-Type', 'application/json');
    try {
      const data = body ? JSON.parse(body) : {};
      if (req.url === '/hello') {
        res.end(JSON.stringify({ message: `Hello ${data.name || 'World'}!` }));
      } else if (req.url === '/health') {
        res.end(JSON.stringify({ status: 'ok' }));
      } else {
        res.statusCode = 404;
        res.end(JSON.stringify({ error: 'Not found' }));
      }
    } catch (err) {
      res.statusCode = 400;
      res.end(JSON.stringify({ error: err.message }));
    }
  });
});

server.listen(PORT, '127.0.0.1', () => console.log(`Listening on ${PORT}`));
process.on('SIGTERM', () => server.close(() => process.exit(0)));

前端通信:

import { Command } from '@tauri-apps/plugin-shell';
import { fetch } from '@tauri-apps/plugin-http';

let sidecarProcess: any = null;
const PORT = 3333;

async function startSidecar(): Promise<void> {
  if (sidecarProcess) return;
  const command = Command.sidecar('binaries/my-sidecar', [], {
    env: { SIDECAR_PORT: String(PORT) },
  });
  sidecarProcess = await command.spawn();
  for (let i = 0; i < 10; i++) {
    try {
      const res = await fetch(`http://127.0.0.1:${PORT}/health`);
      if (res.ok) return;
    } catch {
      await new Promise((r) => setTimeout(r, 100));
    }
  }
  throw new Error('Sidecar failed to start');
}

async function callSidecar(endpoint: string, data?: object): Promise<any> {
  const res = await fetch(`http://127.0.0.1:${PORT}${endpoint}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: data ? JSON.stringify(data) : undefined,
  });
  return res.json();
}

async function stopSidecar(): Promise<void> {
  if (sidecarProcess) {
    await sidecarProcess.kill();
    sidecarProcess = null;
  }
}

生产环境构建

更新根目录中的 package.json:

{
  "scripts": {
    "build:sidecar": "cd sidecar && npm run build",
    "dev": "npm run build:sidecar && tauri dev",
    "build": "npm run build:sidecar && tauri build"
  }
}

跨平台目标:

平台pkg 目标Rust 三元组
Windows x64node18-win-x64x86_64-pc-windows-msvc
macOS x64node18-macos-x64x86_64-apple-darwin
macOS ARMnode18-macos-arm64aarch64-apple-darwin
Linux x64node18-linux-x64x86_64-unknown-linux-gnu

安全

  1. 使用验证器,而非 "args": true
  2. 仅将 HTTP 服务绑定到 127.0.0.1
  3. 在 Tauri 和 Sidecar 中都验证输入
  4. 确保应用关闭时 Sidecar 也会终止

故障排查

找不到二进制文件: 检查目标三元组是否匹配:

ls -la src-tauri/binaries/
rustc --print host-tuple

权限被拒绝(Unix):

chmod +x src-tauri/binaries/my-sidecar-*

无提示崩溃: 检查 stderr:

const output = await command.execute();
if (output.code !== 0) console.error(output.stderr);

评论

支持 Markdown