Skip to main content
Glama

Backbone.js Documentation MCP Server

by elegroag
Backbone-cap-11.md18.2 kB
# Capítulo 11 · Migración de un proyecto Backbone (jQuery + Underscore) a TypeScript Este capítulo guía una migración ordenada y gradual de un proyecto Backbone modular que usa jQuery y Underscore a TypeScript (TS), manteniendo ES Modules (ESM), Vite y los patrones del proyecto (plantillas `?raw` + `_.template`, clases base de vistas, alias `@`). La estrategia está pensada para que puedas migrar por módulos (productos, clientes, etc.) sin detener el desarrollo, con tipado incremental y mínimo riesgo. --- ## Objetivos - **Incremental**: migrar por etapas, módulo a módulo. - **Compatibilidad**: mantener ESM + Vite, Electron y organización actual (`src/ui/...`). - **Productividad**: tipados útiles para `Backbone.Model`, `Backbone.Collection`, `Backbone.View`, `Backbone.Router`. - **Integración**: jQuery tipado (`JQuery`) y `_.template` tipado, importación de HBS vía `?raw`. - **Mínimo dolor**: sin reescrituras masivas; convivir .js y .ts durante la transición. ## Prerrequisitos - Proyecto ya en ESM con Vite (ver capítulos 2–10). - Node 18+. - Conocer clases base del proyecto (`src/ui/common/ModelView.js`, `CollectionView.js`, `Layout.js`, etc.). ## Dependencias de desarrollo Instala TypeScript y definiciones: ```bash pnpm add -D typescript @types/backbone @types/jquery @types/underscore ``` Si usas lodash en paralelo, instala `@types/lodash` según corresponda. > Nota: `@types/backbone` ya referencia tipos de `jquery` y `underscore`, pero mantenerlos explícitos ayuda a los IDEs y a evitar resoluciones ambiguas. ## Configuración TypeScript (tsconfig) Puedes ampliar tu `tsconfig.json` existente o crear uno específico para la UI (ej. `tsconfig.ui.json`). Recomendado (ajusta paths a tu repo): ```jsonc { "extends": "./tsconfig.json", // si ya tienes base TS "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", // óptimo con Vite (TS 5+) "verbatimModuleSyntax": true, // recomendado con ESM "jsx": "preserve", "strict": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "useDefineForClassFields": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "skipLibCheck": true, // agiliza migración "allowJs": true, // permite convivir .js y .ts "checkJs": false, // ponlo en true si quieres avisos en .js "lib": ["ES2022", "DOM"], "types": ["vite/client"], "baseUrl": ".", "paths": { "@/*": ["src/ui/*"] // "#electron/*": ["src/electron/*"] // opcional, sólo si usas Electron } }, "include": ["src/ui/**/*.ts", "src/ui/**/*.d.ts"], "exclude": ["node_modules", "dist"] } ``` ### Inicio de history y carga perezosa En el arranque de la UI, inicia el router con `pushState` cuando el servidor soporte rutas limpias: ```ts // src/ui/main.ts import Backbone from 'backbone'; import { ProductsRouter } from '@/modules/products/ProductsRouter'; new ProductsRouter(); Backbone.history.start({ pushState: true }); ``` Para dividir el bundle, carga vistas/módulos bajo demanda dentro de handlers: ```ts // dentro de un handler de router async list() { const { ProductsListView } = await import('./view/ProductsListView'); const view = new ProductsListView({ el: '#app' }); view.render(); } ``` Nota: `moduleResolution: "Bundler"` y `verbatimModuleSyntax` requieren TypeScript 5+. Asegúrate que Vite ya resuelve `@` en `vite.config.ts` (capítulos previos). Si no, añade el alias también ahí para que TS e IDE queden alineados. ### Alineación TS y Vite (ESNext) Para evitar discrepancias entre el tipo de salida y la resolución de módulos: - Mantén alias `@` en `tsconfig.ui.json` (`compilerOptions.paths`) y en `vite.config.ts` (`resolve.alias`). - Es válido usar `target: "ES2022"` en TS (análisis de tipos) y `build.target = 'esnext'` en Vite (salida del bundle moderno). - Usa el plugin `vite-tsconfig-paths` para alinear rutas de TS con Vite automáticamente. Ejemplo `vite.config.ts`: ```ts // vite.config.ts import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ resolve: { alias: { '@': '/src/ui' } }, build: { target: 'esnext', sourcemap: true }, plugins: [tsconfigPaths()], }); ``` ## Declaraciones de módulos (plantillas y assets) Para importar plantillas como texto con `?raw` y que TS no se queje, crea/edita un archivo de tipos global, por ejemplo `types/app.d.ts` (crea la carpeta `types/` si no existe): ```ts // types/app.d.ts declare module '*.tpl?raw' { const src: string; export default src; } declare module '*.hbs?raw' { const src: string; export default src; } declare module '*.html?raw' { const src: string; export default src; } ``` Si importas estilos o imágenes desde TS, considera añadir declaraciones según sea necesario. ### Plantillas, CSP y precompilación `_.template()` compila usando `new Function` cuando lo haces en runtime, lo cual puede requerir `'unsafe-eval'` en tu CSP. Recomendación: - Desarrollo: compilar en runtime (más simple). - Producción: precompilar plantillas para evitar `'unsafe-eval'`. Opciones de precompilación: - Mantener Underscore y precompilar con un script propio (transformar `*.tpl` a funciones en build). - Migrar plantillas a Handlebars precompilado si el proyecto ya lo usa. Integra estas decisiones con la CSP del Cap. 9 (Helmet), permitiendo `'unsafe-eval'` solo en desarrollo si fuera necesario. ## Integración de Backbone con jQuery en TS - Importa `jquery` y asígnalo a Backbone para asegurar el uso consistente: ```ts import $ from 'jquery'; import Backbone from 'backbone'; Backbone.$ = $; // importante para eventos y selecciones ``` - Usa tipos `JQuery` al interactuar con el DOM: ```ts const $input: JQuery = this.$('input[name="q"]'); ``` ## Integración de Underscore con TS - Importa funciones tipadas de `underscore` (o el default): ```ts import _, { template } from 'underscore'; ``` - Compila plantillas con tipos de contexto: ```ts import formTplSrc from './view/hbs/productos_form_view.tpl?raw'; type ProductoFormCtx = { titulo: string; producto: ProductoAttrs }; const renderForm = template(formTplSrc) as (ctx: ProductoFormCtx) => string; ``` > En TS, `template()` retorna una función de tipo `(data?: object) => string`. Puedes refinarla con el `as` del contexto esperado. ### Helper para plantillas con tipos Para evitar repetir `as (ctx: ...) => string` en cada vista, centraliza un helper: ```ts // src/ui/common/templates.ts import { template } from 'underscore'; export function compileTpl<C>(src: string) { return template(src) as (ctx: C) => string; } ``` Uso: ```ts import { compileTpl } from '@/common/templates'; import formTplSrc from './hbs/productos_form_view.tpl?raw'; const renderForm = compileTpl<{ producto: ProductoAttrs }>(formTplSrc); ``` ## Patrones de tipado para Backbone ### Model ```ts // src/ui/modules/products/models/ProductoModel.ts import Backbone from 'backbone'; export type ProductoAttrs = { id?: number; nombre: string; sku: string; precio: number; activo: boolean; }; export class ProductoModel extends Backbone.Model<ProductoAttrs> { // claves por defecto defaults(): Partial<ProductoAttrs> { return { activo: true }; } // idAttribute si no es 'id' get idAttribute(): string { return 'id'; } // validación básica (puedes integrar tu ValidationUIHandler si aplica) validate(attrs: ProductoAttrs) { if (!attrs.nombre) return 'nombre requerido'; if (attrs.precio < 0) return 'precio inválido'; return undefined; } } ``` ### Collection ```ts // src/ui/modules/products/collections/ProductosCollection.ts import Backbone from 'backbone'; import { ProductoModel, type ProductoAttrs } from '../models/ProductoModel'; export class ProductosCollection extends Backbone.Collection<ProductoModel> { model = ProductoModel; url = '/api/productos'; // ejemplo de método tipado activos(): ProductoModel[] { return this.filter((m) => m.get('activo')); } } ``` ### View ```ts // src/ui/modules/products/view/ProductsFormView.ts import Backbone from 'backbone'; import $ from 'jquery'; import { template } from 'underscore'; import formTplSrc from './hbs/productos_form_view.tpl?raw'; import { ProductoModel, type ProductoAttrs } from '../models/ProductoModel'; const renderForm = template(formTplSrc) as (ctx: { producto: ProductoAttrs; }) => string; export class ProductsFormView extends Backbone.View<ProductoModel> { // opcional: refinar el tipo de this.$el declare $el: JQuery; constructor(options: { model: ProductoModel; el: Element | string }) { super(options); // Asegura el modelo desde el constructor y vincula render this.listenTo(this.model, 'change', this.render); } events() { return { 'submit form': 'onSubmit', 'click [data-action="cancel"]': 'onCancel', } as Backbone.EventsHash; } render() { const html = renderForm({ producto: this.model.toJSON() }); this.$el.html(html); return this; } onSubmit(e: JQuery.SubmitEvent) { e.preventDefault(); const $form = $(e.currentTarget); const attrs: Partial<ProductoAttrs> = { nombre: String($form.find('[name="nombre"]').val() || ''), sku: String($form.find('[name="sku"]').val() || ''), precio: Number($form.find('[name="precio"]').val() || 0), activo: Boolean($form.find('[name="activo"]').prop('checked')), }; this.model.set(attrs); if (!this.model.validationError) { this.trigger('save:producto', this.model); } } onCancel() { this.trigger('cancel'); } } ``` ### Router ```ts // src/ui/modules/products/ProductsRouter.ts import Backbone from 'backbone'; import { ProductsFormView } from './view/ProductsFormView'; import { ProductoModel } from './models/ProductoModel'; export class ProductsRouter extends Backbone.Router { routes: Backbone.RouterRoutes = { 'products/new': 'create', 'products/:id': 'detail', }; create() { const view = new ProductsFormView({ model: new ProductoModel(), el: '#app', }); view.render(); } detail(id: string) { const view = new ProductsFormView({ model: new ProductoModel({ id: Number(id) }), el: '#app', }); view.render(); } } ``` ## Uso de clases base del proyecto Si tu UI ya usa clases base en `src/ui/common/` (por ejemplo `ModelView`, `CollectionView`, `Layout`), migra esas bases primero a `.ts` para facilitar el tipado en cascada. Ejemplo de `ModelView<TModel extends Backbone.Model<any>>` con genéricos y métodos protegidos para render, bind, etc. Así evitas repetir tipos en todas las vistas. > Recuerda: las bases manejan validación (p. ej., `ValidationUIHandler`) y convenciones de render. Tiparlas primero reduce fricción en todo el árbol. ## Estrategia de migración por etapas 1. **Infra TS** - Añade dependencias y `tsconfig.ui.json` (o extiende el existente). - Crea declaraciones `*.hbs?raw` y revisa alias en Vite/TS. 2. **Bases comunes** (`src/ui/common/`) - Migra a `.ts` las bases (`ModelView`, `CollectionView`, `Layout`, `ValidationUIHandler`). - Exporta tipos auxiliares (p. ej. `RenderContext<T>`). 3. **Modelos y Colecciones** por módulo - Cambia `*.js` a `*.ts` en `modules/*/models` y `modules/*/collections`. - Define `type Attrs` por entidad y úsalo en `Model<Attrs>`. 4. **Vistas clave** - Migra `View`s con menos dependencias primero (formularios simples, listados sin subvistas). - Tipa `events()` como `Backbone.EventsHash` y argumentos de handlers con tipos de jQuery. 5. **Routers** - Migra routers y declara `routes: Backbone.RouterRoutes`. 6. **Servicios e IPC** - Centraliza llamadas en `IpcClient.invoke<T>()` y define interfaces de respuesta/DTO compartidos entre Electron y UI (en `types/` o `src/shared/`). 7. **Modo estricto y limpieza** - Sube `strictness` progresivamente (`noImplicitAny`, `exactOptionalPropertyTypes` si aplica). - Recomendadas adicionales: - `noUncheckedIndexedAccess: true` - `useUnknownInCatchVariables: true` - `noPropertyAccessFromIndexSignature: true` - Elimina `// @ts-ignore` y añade tipos faltantes. 8.**Build/QA** - Ajusta scripts para compilar/verificar TS. Con Vite, la transpilación corre en dev; para type-check usa `tsc -p tsconfig.ui.json --noEmit` en CI. ## Pruebas (Vitest + DOM + MSW) Configura entorno DOM para pruebas de vistas: ```ts // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'happy-dom', setupFiles: ['src/ui/test/setup.ts'], globals: true }, }); ``` Ejemplo mínimo de prueba de vista: ```ts // src/ui/modules/products/view/ProductsFormView.spec.ts import { describe, it, expect } from 'vitest'; import { ProductsFormView } from './ProductsFormView'; import { ProductoModel } from '../models/ProductoModel'; describe('ProductsFormView', () => { it('renderiza y emite save al enviar', () => { const view = new ProductsFormView({ el: document.createElement('div'), model: new ProductoModel() }); view.render(); expect(view.$('form').length).toBe(1); }); }); ``` Mock de API con MSW en tests de integración (opcional) para colecciones/servicios. ## Calidad: ESLint/Prettier y checker en dev Añade scripts y, si quieres, integra `vite-plugin-checker` para errores TS/ESLint en caliente. ```ts // vite.config.ts import Checker from 'vite-plugin-checker'; // ... plugins: [tsconfigPaths(), Checker({ typescript: true })]; ``` ## Ejemplo de importación de plantilla con clases base ```ts // src/ui/modules/products/view/ProductsDetailView.ts import { ModelView } from '@/common/ModelView'; // suponiendo que ya migraste esta base a TS import { template } from 'underscore'; import tplSrc from './hbs/products_detail.tpl?raw'; import { ProductoModel } from '../models/ProductoModel'; const renderTpl = template(tplSrc) as (ctx: { model: ReturnType<ProductoModel['toJSON']>; }) => string; export class ProductsDetailView extends ModelView<ProductoModel> { render() { this.$el.html(renderTpl({ model: this.model!.toJSON() })); return this; } } ``` ## Augmentations y tipos globales del proyecto - **Backbone.$**: fija una sola vez en tu bootstrap de UI: ```ts import $ from 'jquery'; import Backbone from 'backbone'; Backbone.$ = $; ``` Ubícalo en el bootstrap de la UI (por ejemplo, `src/ui/main.ts` o en tu `startApp()`). - Para compatibilidad con plugins legacy que esperan globals: ```ts // opcional y controlado: sólo si algún plugin lo requiere (window as any).$ = $; (window as any).jQuery = $; ``` - **Electron/IPC**: tipa `window.electron` o, preferentemente, usa `IpcClient` con genéricos: ```ts // services/IpcClient.ts export async function invoke<T>( channel: string, payload?: unknown ): Promise<T> { /* ... */ } ``` - **Módulos compartidos**: define DTOs en `types/` o `src/shared/` y reutilízalos en Electron y UI. ### Fronteras Electron vs Web (tsconfigs separados) Mantén tipos de Node/Electron fuera de la UI web usando tsconfigs distintos: ```jsonc // tsconfig.ui.json { "compilerOptions": { "lib": ["ES2022", "DOM"], "types": ["vite/client"], "allowJs": true } } // tsconfig.electron.json { "compilerOptions": { "lib": ["ES2022", "DOM"], "types": ["electron", "node"] } } ``` Configura builds independientes cuando empaquetes Electron. ### Validación de datos (runtime) en IPC/API Refuerza contratos usando Zod: ```ts // services/IpcClient.ts import { z } from 'zod'; export async function invokeParsed<T>(channel: string, payload: unknown, schema: z.ZodType<T>): Promise<T> { const result = await window.electron.invoke(channel, payload); return schema.parse(result); } // usage const ProductoDto = z.object({ id: z.number(), nombre: z.string(), sku: z.string(), precio: z.number(), activo: z.boolean() }); const dto = await invokeParsed('productos:get', { id: 1 }, ProductoDto); ``` ## Gotchas frecuentes - **this y eventos**: en métodos de vista usa funciones de instancia, Backbone las vincula. Evita perder `this` en callbacks externos; usa arrow functions o `_.bindAll` si es necesario. - **Atributos del modelo**: usa `get('campo')` y `set({ campo })` con tipos; evita acceso directo a `this.attributes`. - **Plantillas**: garantiza que el contexto pasado a `template()` coincide con tu tipo `as (...) => string`. - **jQuery tipos**: eventos: `JQuery.ClickEvent`, `JQuery.SubmitEvent`, etc. - **Imports ESM**: no mezclar commonjs `require`. Todo `import`. - **Paths/alias**: alinear `vite.config.ts` y `tsconfig` en `paths`/`resolve.alias`. ## Checklist de cierre por módulo - **Model/Collection**: definen `type Attrs` y compilan sin `any`s implícitos. - **View**: `events()` tipado y handlers con tipos de evento jQuery. Render sin `any`. - **Router**: `routes` tipado y navegación funcional. - **Plantillas**: importadas con `?raw` y compiladas; contexto tipado. - **Servicios**: llamadas tipadas vía `IpcClient.invoke<T>()`. - **Build**: `tsc --noEmit` sin errores y Vite dev/build correctos. ## Scripts útiles En `package.json` añade (si no los tienes): ```jsonc { "scripts": { "typecheck:ui": "tsc -p tsconfig.ui.json --noEmit", "dev": "vite", "build": "vite build", "preview": "vite preview", "test": "vitest run", "coverage": "vitest run --coverage", "lint": "eslint \"src/ui/**/*.{ts,js}\"", "format": "prettier -w ." } } ``` Ejecuta `pnpm typecheck:ui` en CI para validar tipos sin emitir archivos. > Tip: añade `rollup-plugin-visualizer` para inspeccionar tamaño de bundles tras build. ## Conclusión Migrar Backbone a TypeScript en este stack es directo si se sigue por capas: bases comunes, modelos/colecciones, vistas y routers. Mantén `?raw` + `_.template`, define tipos de atributos por entidad y tipa tus servicios IPC. Así obtienes autocompletado, seguridad de tipos y menos errores en tiempo de ejecución, sin reescrituras masivas.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/elegroag/backbone-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server