Headless Univer
Headless Univer es el runtime de contenido de Univer sin interfaz de edición interactiva. Comparte modelos de contenido, comandos, cálculos y API Facade con el editor web. Puede procesar contenido en Node.js o ejecutar tareas sin interfaz en un navegador. Las capacidades disponibles dependen de los plugins de producto y función registrados.
En Node.js puede crear o cargar documentos, editar párrafos, texto, estructura y estilos, y guardar instantáneas Univer. Tablas, listas, columnas, fórmulas y formas necesitan sus plugins de contenido. Editar contenido no genera capturas de páginas.
El ejemplo usa el modo plugin. @univerjs/core y los plugins de contenido del producto pertenecen a Headless; @univerjs/ui, los paquetes de producto *-ui y los presets básicos del navegador quedan fuera de esta configuración. @univerjs/engine-render se mantiene como dependencia compartida sin montar Canvas ni editor. Consulte los límites de paquetes Headless.
Las instantáneas locales no necesitan colaboración. El ejemplo colaborativo requiere además cliente, servidor y autenticación. La conversión Office y las capturas necesitan Server SDK y un runtime de renderizado, respectivamente. Importar /facade no registra los plugins ni convierte las API UI en API Headless.
Instalación
pnpm add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsnpm install @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsyarn add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsbun add @univerjs/core @univerjs/engine-render @univerjs/engine-formula @univerjs/docsUso
import { LocaleType, Univer } from '@univerjs/core'import { FUniver } from '@univerjs/core/facade'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import '@univerjs/docs/facade'const univer = new Univer({ locale: LocaleType.ES_ES, locales: { [LocaleType.ES_ES]: {}, },})univer.registerPlugin(UniverRenderEnginePlugin)univer.registerPlugin(UniverFormulaEnginePlugin)univer.registerPlugin(UniverDocsPlugin)const univerAPI = FUniver.newAPI(univer)univerAPI.createDocument({})Ejemplo de colaboración en Node.js
Node.js puede actuar como cliente de colaboración: cargar un documento existente del servidor, modificar su contenido mediante la API de Facade y sincronizar los cambios con otros clientes que tengan abierto el mismo documento. Antes de ejecutarlo, prepara un servicio de colaboración, una licencia de cliente y un ID de documento accesible.
Archivos del ejemplo
Organiza los archivos con esta estructura:
src/docs/node/├── main.ts└── univer.tsuniver.ts: crea Univer y registra los plugins de funciones de documentos, las extensiones de Facade y el cliente de colaboración de Node.js.main.ts: carga un documento colaborativo y muestra las API de párrafos, texto, encabezados y pies de página, bloques destacados, bloques de código, citas, listas, formas, tablas y columnas.
Sustituye your-license, la URL del servidor y la Cookie en univer.ts, así como el ID del documento en main.ts.
import { UniverCollaborationPlugin } from '@univerjs-pro/collaboration'import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import { NodeCollaborationSocketService, UniverCollaborationClientNodePlugin,} from '@univerjs-pro/collaboration-client-node'import { UniverDocsCalloutPlugin } from '@univerjs-pro/docs-callout'import { UniverDocsCodePlugin } from '@univerjs-pro/docs-code'import { UniverDocsColumnPlugin } from '@univerjs-pro/docs-column'import { UniverDocsLatexPlugin } from '@univerjs-pro/docs-latex'import { UniverDocsListPlugin } from '@univerjs-pro/docs-list'import { UniverDocsQuotePlugin } from '@univerjs-pro/docs-quote'import { UniverDocsReferencePlugin } from '@univerjs-pro/docs-reference'import { UniverDocsShapePlugin } from '@univerjs-pro/docs-shape'import { UniverDocsTablePlugin } from '@univerjs-pro/docs-table'import { UniverLicensePlugin } from '@univerjs-pro/license'import { IAuthzIoService, IImageIoService, IMentionIOService, IUndoRedoService, LocaleType, Univer,} from '@univerjs/core'import { UniverDocsPlugin } from '@univerjs/docs'import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing'import { UniverDocsHyperLinkPlugin } from '@univerjs/docs-hyper-link'import { UniverDrawingPlugin } from '@univerjs/drawing'import { UniverRenderEnginePlugin } from '@univerjs/engine-render'import { UniverNetworkPlugin } from '@univerjs/network'import { UniverThreadCommentPlugin } from '@univerjs/thread-comment'import '@univerjs/docs/facade'import '@univerjs/docs-drawing/facade'import '@univerjs-pro/docs-shape/facade'import '@univerjs-pro/docs-callout/facade'import '@univerjs-pro/docs-reference/facade'import '@univerjs-pro/docs-code/facade'import '@univerjs-pro/docs-list/facade'import '@univerjs-pro/docs-quote/facade'import '@univerjs-pro/docs-table/facade'import '@univerjs-pro/docs-column/facade'import '@univerjs-pro/docs-latex/facade'import '@univerjs-pro/collaboration-client/facade'export function createUniverOnNode(): Univer { const univer = new Univer({ locale: LocaleType.ES_ES, locales: {}, override: [ [IAuthzIoService, null], [IUndoRedoService, null], [IMentionIOService, null], ], }) univer.registerPlugin(UniverLicensePlugin, { license: 'your-license', }) univer.registerPlugin(UniverRenderEnginePlugin) univer.registerPlugin(UniverThreadCommentPlugin) univer.registerPlugin(UniverDrawingPlugin, { override: [[IImageIoService, null]], }) univer.registerPlugin(UniverDocsPlugin) univer.registerPlugin(UniverDocsDrawingPlugin) univer.registerPlugin(UniverDocsHyperLinkPlugin) univer.registerPlugin(UniverDocsReferencePlugin) univer.registerPlugin(UniverDocsCalloutPlugin) univer.registerPlugin(UniverDocsCodePlugin) univer.registerPlugin(UniverDocsListPlugin) univer.registerPlugin(UniverDocsQuotePlugin) univer.registerPlugin(UniverDocsShapePlugin) univer.registerPlugin(UniverDocsTablePlugin) univer.registerPlugin(UniverDocsColumnPlugin) univer.registerPlugin(UniverDocsLatexPlugin) registerCollaborationPlugins(univer) return univer}function registerCollaborationPlugins(univer: Univer): void { const serverUrl = 'https://your-univer-server.example.com' univer.registerPlugin(UniverNetworkPlugin) univer.registerPlugin(UniverCollaborationPlugin) univer.registerPlugin(UniverCollaborationClientPlugin, { socketService: NodeCollaborationSocketService, enableOfflineEditing: false, enableSingleActiveInstanceLock: false, snapshotServerUrl: `${serverUrl}/universer-api/snapshot`, collabSubmitChangesetUrl: `${serverUrl}/universer-api/comb`, collabWebSocketUrl: `${serverUrl}/universer-api/comb/connect`, wsSessionTicketUrl: `${serverUrl}/universer-api/user/session-ticket`, uploadFileServerUrl: `${serverUrl}/universer-api/stream/file/upload`, signUrlServerUrl: `${serverUrl}/universer-api/file/{fileID}/sign-url`, downloadEndpointUrl: serverUrl, authzUrl: `${serverUrl}/universer-api/authz`, sendChangesetTimeout: 200, retryConnectingInterval: 1000, customHeaders: { // Ejemplo: si tu servidor Univer requiere autenticación mediante cookies, puedes definir aquí la cabecera 'cookie'. cookie: 'your-cookie', // ... más cabeceras }, }) univer.registerPlugin(UniverCollaborationClientNodePlugin)}import type { FConnectorShape } from '@univerjs-pro/engine-shape/facade'import { FUniver } from '@univerjs/core/facade'import process from 'node:process'import { createUniverOnNode } from './univer'async function run(): Promise<void> { const univer = createUniverOnNode() const univerAPI = FUniver.newAPI(univer) const document = await univerAPI.getCollaboration().loadDocAsync('your-unit-id') if (!document) { throw new Error( 'No se pudo cargar el documento colaborativo. Comprueba el ID del documento y los permisos de acceso.', ) } testDocsFacadeAPI(univerAPI) testDocsHeaderFooterFacadeAPI(univerAPI) testDocsCalloutFacadeAPI(univerAPI) testDocsCodeFacadeAPI(univerAPI) testDocsQuoteFacadeAPI(univerAPI) testDocsListFacadeAPI(univerAPI) testDocsShapeFacadeAPI(univerAPI) testDocsTableFacadeAPI(univerAPI) testDocsColumnFacadeAPI(univerAPI) await univerAPI.getCollaboration().flush(document.getId()) console.warn('Los cambios del documento se han sincronizado con el servidor.')}function testDocsShapeFacadeAPI(univerAPI: FUniver) { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.getParagraphs()[0] if (!paragraph) return const anchor = { paragraphId: paragraph.getId(), segmentId: paragraph.getSegmentId(), } const shape = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.RoundRect, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 80, verticalOffset: 80 }, }, transform: { width: 320, height: 160 }, shapeData: { fill: { fillType: univerAPI.Enum.ShapeFillEnum.SolidFill, color: '#DBEAFE', }, stroke: { lineStrokeType: univerAPI.Enum.ShapeLineTypeEnum.SolidLine, color: '#2563EB', width: 2, }, }, }) if (!shape) return shape .setRotation(4) .setStrokeOpacity(0.9) .getText() .setText('Ejemplo de forma del documento con la API de Facade') .setHorizontalAlign(univerAPI.Enum.HorizontalAlign.CENTER) .setVerticalAlign(univerAPI.Enum.VerticalAlign.MIDDLE) const target = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.Ellipse, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 500, verticalOffset: 100 }, }, transform: { width: 160, height: 120 }, shapeData: { fill: { fillType: univerAPI.Enum.ShapeFillEnum.SolidFill, color: '#DCFCE7' }, stroke: { lineStrokeType: univerAPI.Enum.ShapeLineTypeEnum.SolidLine, color: '#16A34A', width: 2 }, }, }) if (!target) return const connector = fDocument.insertShape({ shapeType: univerAPI.Enum.ShapeTypeEnum.BentConnector3, placement: { wrappingStyle: univerAPI.Enum.TextWrappingStyle.WRAP_SQUARE, anchor, position: { horizontalOffset: 360, verticalOffset: 130 }, }, transform: { width: 180, height: 80 }, }) if (!connector || !connector.isConnectorShape()) return const sourceSites = shape.getConnectionSites() const targetSites = target.getConnectionSites() ;(connector as FConnectorShape) .setStrokeColor('#475569') .setStrokeWidth(2) .setEndArrow(univerAPI.Enum.ShapeArrowTypeEnum.Arrow, univerAPI.Enum.ShapeArrowSizeEnum.Medium) .bindStart(shape.getId(), sourceSites[0]?.index ?? 0) .bindEnd(target.getId(), targetSites[2]?.index ?? targetSites[0]?.index ?? 0) console.warn('ID de la forma del documento:', shape.getId()) console.warn('Número de formas del documento:', fDocument.getShapes().length)}function testDocsFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const intro = fDocument.appendParagraph( `Saludos desde el cliente de colaboración de Node.js. Hora actual: ${new Date().toString()}.`, ) intro.appendText(' [inserción en línea]') const beforeIntro = fDocument.insertParagraph( intro.getInfo().paragraphIndex, 'Texto insertado antes de la introducción mediante un objeto de párrafo.', ) const foundIntro = fDocument.findParagraphByText('cliente de colaboración de Node.js') const introMatches = fDocument.findParagraphs({ text: 'cliente de colaboración de Node.js' }) const introRange = intro.getRange() const bodyBeforeDelete = fDocument.getBody() const deleteStartOffset = bodyBeforeDelete.dataStream.indexOf(' [inserción en línea]') if (deleteStartOffset >= 0) { fDocument.deleteRange({ startOffset: deleteStartOffset, endOffset: deleteStartOffset + ' [inserción en línea]'.length, }) } intro.setStyle({ textStyle: { fs: 12, }, }) console.warn(`ID del documento actual: ${fDocument.getId()}`) console.warn(`Nombre del documento actual: ${fDocument.getName()}`) console.warn(`¿El documento actual es moderno?: ${fDocument.isModern()}`) console.warn(`ID del párrafo de introducción: ${intro.getId()}`) console.warn(`Índice hijo del párrafo de introducción tras moverlo: ${intro.getInfo().paragraphIndex}`) console.warn('Rango del párrafo de introducción:', introRange) console.warn('ID del párrafo de introducción encontrado:', foundIntro?.getId()) console.warn('Número de párrafos de introducción coincidentes:', introMatches.length) console.warn('Número de párrafos del cuerpo:', fDocument.getParagraphs().length) console.warn(`ID del párrafo insertado: ${beforeIntro.getId()}`) console.warn(`Longitud del cuerpo de la instantánea del documento: ${fDocument.save().body?.dataStream.length ?? 0}`)}function testDocsHeaderFooterFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return if (fDocument.isModern()) { console.warn( 'Se omite el ejemplo de encabezado y pie de página: los documentos modernos no admiten encabezados ni pies de página.', ) return } const headerSegmentId = fDocument.ensurePageHeader() const footerSegmentId = fDocument.ensurePageFooter() const headerParagraph = fDocument.appendParagraph( 'Ejemplo de encabezado de Docs en Node.js con la API de Facade.', headerSegmentId, ) headerParagraph.appendText(` Documento: ${fDocument.getName() || fDocument.getId()}`) const footerParagraph = fDocument.appendParagraph( 'Ejemplo de pie de página de Docs en Node.js con la API de Facade.', footerSegmentId, ) fDocument.insertText(0, 'Encabezado de página:', headerSegmentId) footerParagraph.setStyle({ textStyle: { fs: 10, }, }) console.warn('ID del segmento de encabezado del documento:', headerSegmentId) console.warn( 'Párrafos del encabezado del documento:', fDocument.getParagraphs(headerSegmentId).map((paragraph) => paragraph.getText()), ) console.warn( 'Longitud del cuerpo del encabezado del documento:', fDocument.getBody(headerSegmentId).dataStream.length, ) console.warn('ID del segmento de pie de página del documento:', footerSegmentId) console.warn( 'ID del párrafo de pie de página del documento:', fDocument.findParagraphByText('pie de página de Docs en Node.js con la API de Facade', footerSegmentId)?.getId(), )}function testDocsCalloutFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Ejemplo de bloque destacado de Docs en Node.js con la API de Facade.') const callout = fDocument.insertCallout(paragraph, { blockId: createSampleId('callout'), config: { borderRadius: 8, icon: '!', }, }) if (!callout) return callout.updateConfig({ borderRadius: 8 }) console.warn('ID del bloque destacado insertado:', callout.getId()) console.warn('Información del bloque destacado del documento:', callout.describe())}function testDocsCodeFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph( 'const message = "Saludos desde la API de Facade de bloques de código de Docs en Node.js";', ) const code = fDocument.insertCode(paragraph, { blockId: createSampleId('code'), config: { language: 'typescript', showLineNumbers: true, tabSize: 4, wrap: true, }, }) if (!code) return code.updateConfig({ config: { language: 'typescript', showLineNumbers: true, }, }) console.warn('ID del bloque de código insertado:', code.getId()) console.warn('Información del bloque de código del documento:', code.describe())}function testDocsQuoteFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Ejemplo de bloque de cita de Docs en Node.js con la API de Facade.') const quote = fDocument.insertQuote(paragraph, { blockId: createSampleId('quote'), }) if (!quote) return console.warn('ID del bloque de cita insertado:', quote.getId()) console.warn('Información del bloque de cita del documento:', quote.describe())}function testDocsListFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const paragraph = fDocument.appendParagraph('Ejemplo de lista de Docs en Node.js con la API de Facade.') const success = fDocument.insertList(paragraph, { listType: univerAPI.Enum.PresetListType.BULLET_LIST, }) if (!success) return const item = fDocument.findListItemByText('lista de Docs en Node.js con la API de Facade') if (!item) return item.setGlyphSymbol('-', { mode: univerAPI.Enum.DocsListSelectionMode.Item }) item.setGlyphType(univerAPI.Enum.ListGlyphType.BULLET, { mode: univerAPI.Enum.DocsListSelectionMode.Item }) console.warn('Elemento de lista del documento:', item.describe()) console.warn('Elementos de lista del documento:', fDocument.describeListItems())}function testDocsTableFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return const table = fDocument.insertTableFromData( [ ['Función', 'Estado', 'Responsable'], ['Facade API', 'Listo', 'Cliente de Docs en Node.js'], ['Colaboración', 'Cargado', 'Cliente de Node.js'], ], { headerRowCount: 1, tableId: createSampleId('table'), }, ) if (!table) return table.setCellText(1, 1, 'Actualizado') table.getCell(1, 2)?.setBackgroundColor('#E8F1FF') table.setColumnType(1, { type: univerAPI.Enum.DocsTableColumnType.Text }) table.setTableBorder({ color: '#3367D6', preset: univerAPI.Enum.DocsTableBorderPreset.All, width: 1, }) console.warn('Información de la tabla del documento:', table.describe()) console.warn('ID de la tabla encontrada por texto:', fDocument.findTableByText('Facade API')?.getId())}function testDocsColumnFacadeAPI(univerAPI: FUniver): void { const fDocument = univerAPI.getActiveDocument() if (!fDocument) return if (!fDocument.isModern()) { console.warn('Se omite el ejemplo de columnas: los grupos de columnas requieren un documento moderno.') return } const groupId = createSampleId('column-group') const group = fDocument.insertColumnGroup(3, { columnGroupId: groupId, columnIds: [createSampleId('column-left'), createSampleId('column-middle'), createSampleId('column-right')], gap: 18, widthRatios: [1.1, 1, 0.9], }) if (!group) return const [leftColumn, middleColumn, rightColumn] = group.getColumns() const leftOffset = leftColumn?.getInsertOffset() if (leftOffset != null) { fDocument.insertText(leftOffset, 'Columna izquierda del ejemplo de Facade.') } const middleOffset = middleColumn?.getInsertOffset() if (middleOffset != null) { fDocument.insertText(middleOffset, 'Columna central del ejemplo de Facade.') } const rightOffset = rightColumn?.getInsertOffset() if (rightOffset != null) { fDocument.insertTableFromData( [ ['Indicador', 'Actual', 'Siguiente paso'], ['Funciones cubiertas', 'Columnas', 'Encabezado/Pie de página'], ], { headerRowCount: 1, offset: rightOffset, tableId: createSampleId('column-table'), }, ) } group.setWidthRatios([1.2, 1, 0.8]) const insertedColumn = middleColumn ? group.addColumn( middleColumn.getId(), univerAPI.Enum.DocsColumnPositionEnum.RIGHT, createSampleId('column-inserted'), ) : null const insertedOffset = insertedColumn?.getInsertOffset() if (insertedOffset != null) { fDocument.insertText(insertedOffset, 'Columna insertada mediante la API de Facade.') } const foundByText = fDocument.findColumnGroupByText('Columna central') const foundById = fDocument.findColumnGroups({ columnGroupId: group.getId() }) const firstColumn = group.getColumn(0) const groupRange = group.getRange() const rangeLookup = groupRange ? fDocument.getColumnGroupAt(groupRange.startOffset) : null console.warn('Información del grupo de columnas del documento:', group.describe()) console.warn('Número de grupos de columnas encontrados por ID:', foundById.length) console.warn('ID del grupo de columnas encontrado por texto:', foundByText?.getId()) console.warn('ID del grupo de columnas encontrado por rango:', rangeLookup?.getId()) console.warn('Texto de la primera columna del documento:', firstColumn?.getText())}function createSampleId(name: string): string { return `docs-node-${name}-${Date.now()}`}run().catch((error: unknown) => { console.error(error) process.exitCode = 1})Notas de ejecución
loadDocAsync() carga un documento existente del servidor; no necesitas llamar a createDocument(). El servicio de colaboración identifica al usuario y comprueba sus permisos mediante la información de autenticación, como la Cookie incluida en customHeaders. El servidor devuelve el memberID de la conexión.
Este ejemplo desactiva la caché sin conexión del navegador y el bloqueo de instancia entre pestañas, por lo que no es necesario llamar a setCurrentUser() para aislar la caché por cuenta. Si más adelante usas funciones como comentarios, que dependen del usuario actual local, puedes establecer el usuario de la aplicación mediante UserManagerService; esto no sustituye la autenticación del servidor.
El ejemplo de encabezado y pie de página solo se aplica a documentos paginados; el de columnas, a documentos modernos. El ejemplo comprueba el modo del documento con isModern() y omite las operaciones no compatibles.
El ejemplo inserta contenido en un documento existente, por lo que se recomienda usar un documento de prueba. El ejemplo mantiene la conexión colaborativa después de ejecutarse. Abre el mismo documento en un navegador para ver los cambios. flush() espera la confirmación del servidor para los cambios pendientes. Si lo conviertes en un script de una sola ejecución, llama a univer.dispose() cuando hayan terminado todas las operaciones asíncronas y la sincronización.
¿Cómo es esta guía?