Sik.limited Logo

SvelteKit + Tauri v2: guía inicial de compilación estática, ventanas y permisos de archivos

Una guía paso a paso para diseñar la compilación estática, la estructura de ventanas, IPC y los permisos de archivos necesarios para mover una aplicación SvelteKit a una aplicación de escritorio Tauri v2.

Sik ·

Al mover un proyecto web a una aplicación de escritorio, lo primero que cambia es el método de implementación.

Poner una pantalla creada con SvelteKit en Tauri parece más sencillo de lo que parece. Creo que se completará una vez que se construya la interfaz y Tauri muestre el resultado. De hecho, abrir la primera ventana es rápido. Sin embargo, a partir de ese momento, no se trata de mover la misma aplicación SvelteKit tal como está, sino de redefinir los límites para adaptarla a un tiempo de ejecución de escritorio sin servidor.

La primera premisa a aceptar es ésta. Tauri no opera el servidor de nodo de SvelteKit. Los HTML·CSS·JavaScript integrados se proporcionan dentro de la aplicación, y solo las funciones necesarias del sistema operativo se conectan a Rust como un complemento. Por lo tanto, si espera funciones que requieran un proceso de servidor, como +page.server.ts, acción de formulario y enlaces de servidor, en un paquete de escritorio, la estructura se arruinará.

Este artículo cubre la estructura mínima necesaria para iniciar una aplicación de escritorio con Tauri v2 para cualquiera que ya haya usado SvelteKit. Conectemos la compilación estática, la configuración de la ventana, el límite entre la interfaz y Rust y los permisos para leer y escribir archivos en un solo flujo. La cuestión de cuál elegir entre Tauri y Electron se resumió por primera vez en artículo comparativo de aplicaciones de escritorio SvelteKit. Aquí nos centramos en crear un punto de partida infalible una vez que decida Tauri.


En lugar de abandonar las funciones del servidor, separe las funciones del servidor

SvelteKit es un marco que naturalmente abarca SSR y rutas de servidor. Por lo tanto, en los servicios web, es conveniente que la función load de la página obtenga datos del servidor, inicie sesión en el procesamiento como una acción de formulario y use la clave secreta como +server.ts. Sin embargo, la distribución predeterminada de Tauri no incluye el tiempo de ejecución de Node para continuar ejecutando estos servidores.

Esta diferencia no significa renunciar a la funcionalidad. Está cerca de tener sentido cambiar de roles. Los datos públicos/públicos son manejados por el HTTPS API existente, el control de archivos y ventanas en el dispositivo del usuario está controlado por el complemento Tauri o los comandos Rust, y las operaciones que requieren valores secretos aún se envían a un backend externo. En el momento en que agrega la clave API a la aplicación de escritorio y asume el rol de servidor, la estructura que parecía conveniente queda inmediatamente expuesta en la distribución.

Por eso es una buena idea aclarar estas tres cosas antes de comenzar:

  • Llamada UI·Status·API que se puede ejecutar en el navegador
  • Operaciones de archivos, ventanas, notificaciones y bandejas que deben realizarse únicamente dentro del dispositivo del usuario.
  • Tareas de autenticación, pago, clave secreta y verificación de autoridad que deben permanecer en el servidor.

Si no realiza esta clasificación, terminará dedicando tiempo a corregir los errores faltantes de window y las fallas de autenticación de API. Si se clasifica de manera opuesta, Tauri no es un reemplazo de una aplicación web, sino un shell que adjunta funciones locales delgadas y claras al front-end web.

La compilación estática no es una opción, es el formato predeterminado para el escritorio

Al escribir SvelteKit desde Tauri, la clave es @sveltejs/adapter-static. Cree una salida estática que su aplicación pueda leer y haga que frontendDist de Tauri mire ese directorio. Si tiene un SPA con URL dinámicas en mente, configure fallback: 'index.html' para que el enrutador del cliente también reciba actualizaciones y entrada directa.

npm install -D @sveltejs/adapter-static @tauri-apps/cli
npm install @tauri-apps/api
npm run tauri init

Si ya tiene un proyecto SvelteKit, el comando anterior es solo un punto de partida. Debe hacer coincidir el administrador de paquetes y la estructura de comandos de su proyecto, y tratar el src-tauri generado como si fuera un proyecto nativo separado. Las dependencias de frontend y las dependencias de Rust forman la misma aplicación, pero sus ciclos de actualización y mecanismos de falla son diferentes.

Deje que svelte.config.js produzca una salida estática como esta:

import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      fallback: 'index.html'
    })
  }
};

export default config;

Tenga en cuenta que fallback no es una configuración mágica que resolverá todos sus problemas. Este es un dispositivo que devuelve index.html para que el cliente pueda manejar rutas desconocidas en el momento de la compilación, como /projects/123 después de iniciar sesión. Si la pantalla se basa en load del servidor únicamente o en acciones de formulario, no funcionará incluso si hay un respaldo. Esos datos deben convertirse a API, invocables desde su navegador.

Apague SSR en el diseño raíz.

// src/routes/+layout.ts
export const ssr = false;
export const prerender = false;

ssr = false facilita el manejo de código que requiere window, como Tauri API, sin declaraciones condicionales. prerender = false es útil para especificar operaciones SPA basadas en respaldo. Sin embargo, para aplicaciones donde todas las rutas se confirman en el momento de la compilación, como documentos estáticos y pantallas de inicio públicas, la estrategia prerender = true también es posible. Antes de mezclar los dos, es mucho más seguro documentar primero "¿quién obtiene los datos de esta pantalla, cuándo y dónde?".

Tauri La configuración implica acordar la ruta de compilación.

Durante el desarrollo, el servidor de desarrollo Vite proporciona la interfaz de usuario; en las versiones de lanzamiento, el directorio estático proporciona la interfaz de usuario. Si estas dos rutas no coinciden, ocurre el problema clásico donde la aplicación se abre bien en modo de desarrollo, pero aparece una ventana en blanco en la aplicación empaquetada.

// src-tauri/tauri.conf.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "productName": "Desk Notes",
  "version": "0.1.0",
  "identifier": "com.example.desknote",
  "build": {
    "beforeDevCommand": "npm run dev",
    "devUrl": "http://localhost:5173",
    "beforeBuildCommand": "npm run build",
    "frontendDist": "../build"
  },
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "Desk Notes",
        "width": 1200,
        "height": 800,
        "minWidth": 900,
        "minHeight": 640,
        "resizable": true
      }
    ]
  },
  "bundle": {
    "active": true,
    "targets": "all"
  }
}

La ruta de salida predeterminada para el adaptador estático SvelteKit y frontendDist anteriores puede variar de un proyecto a otro. Lo importante no es la convención, sino el directorio creado detrás del npm run build. Una vez creado, verifique si build/index.html existe y, si no, modifique la ruta de salida del adaptador o la configuración de Tauri. Si adivinas ambos lugares al mismo tiempo, pierdes la causa.

El desarrollo y la verificación del paquete deben estar separados.

# 개발 서버 + Tauri 창
npm run tauri dev

# 정적 SvelteKit 빌드 + 네이티브 번들 생성
npm run tauri build

En el modo de desarrollo, los servidores de devUrl hacen todo por usted, por lo que es fácil pasar por alto problemas como rutas de archivos, CSP y rutas de recursos de aplicaciones. Cada vez que se agrega una función, debe adquirir el hábito de ejecutarla en los resultados empaquetados al menos una vez. En particular, las imágenes de rutas relativas, las rutas dinámicas y las redirecciones OAuth externas tienden a fallar primero en una versión.


Una ventana puede ser un límite de permiso en lugar de una pantalla

Al principio, es fácil pensar que las ventanas son sólo una cuestión de diseño. Simplemente agregue el ancho, alto, título, tamaño mínimo, etc. Sin embargo, en Tauri v2, la etiqueta de la ventana puede ser la unidad que vincula los permisos. Es importante tener en cuenta que el title que se muestra al usuario y el label utilizado en la configuración de seguridad son valores diferentes.

Por ejemplo, digamos que su editor solo guarda archivos locales en la ventana principal y las ventanas de ayuda o inicio de sesión no necesitan esa funcionalidad. Si adjunta la misma capacidad a todas las ventanas, la interfaz que se ejecuta en la ventana de ayuda también tiene permiso para escribir archivos. Aunque parezca simple, aumenta innecesariamente la superficie de ataque.

Para una ventana nueva, es mejor decidir primero “qué debería poder hacer” en lugar de “qué debería mostrar”, y luego decidir la etiqueta y las capacidades. En particular, para ventanas que muestran URL externas o para autenticación de complementos, es mejor tener permisos más limitados que la ventana predeterminada.

Incluso cuando necesita crear una ventana en código, la etiqueta se trata como un identificador fijo.

use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};

fn open_preferences(app: &tauri::AppHandle) -> tauri::Result<()> {
    if app.get_webview_window("preferences").is_none() {
        WebviewWindowBuilder::new(
            app,
            "preferences",
            WebviewUrl::App("/preferences".into())
        )
        .title("환경설정")
        .inner_size(720.0, 560.0)
        .resizable(false)
        .build()?;
    }

    Ok(())
}

Esta es la misma razón por la que no se crea una ventana colocando la entrada del usuario directamente en la etiqueta. Las configuraciones de permisos están asociadas con etiquetas en lugar de títulos, por lo que la creación de nombres de ventanas sobre la marcha dificulta el seguimiento del diseño de capacidades. También debe considerar por separado si la creación de la ventana debe estar abierta en todas las ventanas.

La interfaz y Rust están conectadas por un contrato delgado.

Cuando encuentra por primera vez Tauri a Rust, es fácil sentir que necesita mover toda su lógica a Rust. La mayoría de las aplicaciones no lo hacen. La retroalimentación instantánea del estado de la interfaz de usuario, la validación de formularios, las transiciones de pantalla y las expresiones de respuesta de la red se pueden dejar en manos de SvelteKit. En el lado Rust, solo colocamos tareas cercanas al sistema operativo y tareas que deben verificarse dentro del límite de confianza.

Por ejemplo, un comando que le indica a la recepción la carpeta de datos de la aplicación es breve y claro.

// src-tauri/src/lib.rs
#[tauri::command]
fn app_data_dir(app: tauri::AppHandle) -> Result<String, String> {
    app.path()
        .app_data_dir()
        .map(|path| path.to_string_lossy().to_string())
        .map_err(|error| error.to_string())
}

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![app_data_dir])
        .run(tauri::generate_context!())
        .expect("error while running Tauri application");
}

La página principal llama al comando solo en el entorno de ejecución de la aplicación. Si desea utilizar el mismo componente en la vista previa del navegador, es mejor envolver Tauri con un adaptador delgado.

// src/lib/platform/desktop.ts
import { browser } from '$app/environment';

export async function getAppDataDir(): Promise<string | null> {
  if (!browser || !('__TAURI_INTERNALS__' in window)) return null;

  const { invoke } = await import('@tauri-apps/api/core');
  return invoke<string>('app_data_dir');
}
<script lang="ts">
  import { onMount } from 'svelte';
  import { getAppDataDir } from '$lib/platform/desktop';

  let directory: string | null = null;

  onMount(async () => {
    directory = await getAppDataDir();
  });
</script>

{#if directory}
  <p>로컬 데이터는 <code>{directory}</code>에 저장됩니다.</p>
{:else}
  <p>브라우저 미리보기에서는 로컬 앱 경로를 표시하지 않습니다.</p>
{/if}

Lo importante aquí es evitar llamar directamente a invoke durante todo el servicio. Pasar módulos como desktop.ts le permite tener puntos de ramificación en un solo lugar para vistas previas web, pruebas e incluso futuras extensiones de Capacitor. Si está pensando en WebView móvil, elija entre límite de vista web encontrado en SvelteKit + Capacitor y Capacitor y Tauri También vale la pena leer Criteria desde la misma perspectiva.


Seleccionar un archivo y acceder a un archivo son dos cosas diferentes

Tauri Si agrega un botón "Abrir archivo" a su aplicación pero encuentra un error de permiso, la mayoría de las veces el problema comienza porque cree que la selección de archivos y el acceso a archivos son la misma función. El complemento de diálogo permite al usuario seleccionar un archivo. El complemento fs le permite leer y escribir esa ruta. Son dos API diferentes y las operaciones fs requieren dos capas de restricciones: permisos de comando y alcance de ruta.

Primero, agregue los complementos necesarios.

npm run tauri add dialog
npm run tauri add fs

Si la ventana principal solo lee y escribe documentos de datos de la aplicación, es mejor escribir la capacidad junto con comandos y rutas específicas.

// src-tauri/capabilities/main-editor.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-editor",
  "description": "Capability for the local note editor",
  "windows": ["main"],
  "permissions": [
    "dialog:allow-open",
    "fs:allow-read-text-file",
    {
      "identifier": "fs:allow-write-text-file",
      "allow": [{ "path": "$APPDATA/notes/*.md" }]
    },
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPDATA/notes/*.md" }]
    }
  ]
}

Las configuraciones anteriores son solo ejemplos, y el identificador de permiso real debe verificarse en el esquema y el documento creado por la versión del complemento instalado. La razón por la que enfatizo este punto es porque es muy tentador deshacerse del problema poniendo fs:default primero. Si le das permiso a una función que guarda un único archivo para leer y escribir en toda la casa, la operación será más rápida, pero el diseño será peor.

Separar la selección y el guardado en la parte frontal también hace que la experiencia del usuario sea más clara.

import { open } from '@tauri-apps/plugin-dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';

export async function importMarkdown(): Promise<string | null> {
  const path = await open({
    multiple: false,
    directory: false,
    filters: [{ name: 'Markdown', extensions: ['md', 'mdx'] }]
  });

  if (typeof path !== 'string') return null;
  return readTextFile(path);
}

export async function saveDraft(path: string, content: string) {
  await writeTextFile(path, content);
}

Si su aplicación necesita editar continuamente archivos aleatorios seleccionados por el usuario, debe diseñar un ciclo de vida separado para el "primer archivo externo seleccionado". Una forma práctica de reducir la complejidad es copiarlo a la carpeta de datos de la aplicación al importarlo y luego editarlo solo dentro de $APPDATA/notes. Este método reduce el alcance y reúne políticas de copia de seguridad, sincronización y eliminación en un solo lugar. Por otro lado, si el producto requiere que el usuario edite el archivo original directamente, el nuevo acceso a la ruta seleccionada y los permisos de archivo específicos de la plataforma deben tratarse como casos de prueba.

Los errores de permiso son una señal para limitar sus preguntas antes de expandir su configuración.

Cuando ve not allowed o promete un rechazo durante el desarrollo, es fácil elegir abrir permisos completos. Pero este error suele dar lugar a una buena pregunta. ¿Esta ventana realmente necesita usar API? ¿Es sólo leer o también es necesario escribir? ¿Es necesario todo el directorio o puede ser suficiente con una sola carpeta debajo de los datos de la aplicación? ¿Sería mejor verificar la ruta del archivo en el comando Rust en lugar de obtenerla directamente desde la interfaz de usuario?

Las capacidades·permiso·alcance de Tauri v2 se cubrirán con más profundidad en un artículo posterior. Tauri v2 diseño de privilegios mínimos: capacidad·permiso·alcance explica en qué unidades dividir el sistema de archivos, las URL externas y las ventanas múltiples, seguido de una lista de verificación de auditoría. Basta recordar aceptar primero los errores de permisos como un punto de revisión del diseño y no como una falla funcional.


Secuencia de verificación que no termina en modo desarrollo

El hecho de que la aplicación esté encendida es sólo el comienzo de la verificación funcional. El punto donde se encuentran la interfaz web y el shell nativo puede comportarse de manera diferente en el modo de desarrollo y en el modo de paquete. Verificar en el siguiente orden lo ayudará a delimitar la causa.

  1. Asegúrese de que npm run build cree una salida estática y que index.html exista en el frontendDist que configuró.
  2. Verifique que las ramas de vista previa de Ventana, Enrutamiento y Navegador en npm run tauri dev estén funcionando como se esperaba.
  3. Ejecute la aplicación creada con npm run tauri build y verifique la entrada de URL directa, la actualización y la ruta dinámica.
  4. Ejecute la selección, lectura y guardado del archivo una vez cada uno para determinar en qué etapa ocurre el error de permiso.
  5. Si hay una ventana separada, como Ayuda, Inicio de sesión o Configuración, verifique que la función de archivo de la ventana principal no se llame desde esa ventana.
  6. Verifique la ejecución del paquete al menos una vez para cada plataforma compatible real: macOS, Windows o Linux. Tauri usa OS WebView, por lo que el comportamiento de entrada con CSS no debe generalizarse basándose únicamente en los resultados de una plataforma.

Especialmente justo antes de la implementación, es mejor no considerar "se ejecuta en la Mac en desarrollo" e "instalado en el sistema operativo de otro usuario" como los mismos criterios de aprobación. La firma de código, las advertencias de seguridad del sistema operativo, los estados de preparación de WebView y las políticas de acceso a archivos son tipos de problemas operativos completamente diferentes.

Un buen punto de partida no es una aplicación con pocas funciones, sino una aplicación con pocos límites

Cuando crea su primera aplicación con SvelteKit + Tauri, querrá ampliar rápidamente su lista de funciones. Sin embargo, la mejor opción desde el principio es dejar claros los límites de su aplicación. El frente se ejecuta como un paquete estático, todo lo que el servidor necesita hacer permanece en el servidor, las funciones locales solo pasan a través de Tauri API explícito y los permisos de archivos solo tienen las rutas que necesitan.

Una vez que tenga esta estructura implementada, será más fácil determinar dónde colocar funciones adicionales como bandeja, actualización automática, restauración del estado de la ventana, base de datos local y extensiones móviles en el futuro. Por el contrario, si aumenta la funcionalidad mientras este límite es borroso, incluso una pequeña aplicación de escritorio pronto se convertirá en una aplicación con permisos web, Rust y del sistema operativo entrelazados.

Ya sea que el motivo para elegir Tauri sea una distribución ligera o permisos nativos más limitados, el punto de partida es el mismo. Su código y configuración deben poder describir en el mismo idioma qué instala su aplicación en el dispositivo del usuario, qué le pide que haga y cuánto acceso tiene.

Fuente

Últimas publicaciones