NosotrosServiciosProyectosR&DBlogHerramientasEmpezarContacto

Desarrollar para SharePoint Server Subscription Edition: las prácticas que aguantan

Subscription Edition tiene soporte hasta 2035 como mínimo y el techo de SharePoint Framework no se mueve desde 2023. Esa combinación define el trabajo: una pista larga sobre una base estable. Cómo construir bien encima de ella, con código que corre en la versión que su granja acepta de verdad.

pH7x Systems® · · 23 min de lectura

Subscription Edition tiene soporte hasta el 31 de diciembre de 2035 como mínimo, bajo la Modern Lifecycle Policy, sin fin de soporte previsto. Es una pista más larga que la de la mayoría del software que usted escribirá este año.

Su techo de SharePoint Framework, en cambio, no se mueve desde 2023.

Esos dos hechos juntos son el trabajo entero. Se construye algo que tiene que seguir funcionando durante una década, sobre una versión de framework que probablemente no cambiará por debajo. Es una posición poco habitual, y premia un cuidado muy concreto.

Conocer el techo antes de escribir nada

SharePoint on-premises solo ejecuta las versiones de SPFx que corresponden a sus dependencias del lado del servidor. La tabla de compatibilidad da a Subscription Edition v1.0 a v1.5, y la actualización de características 23H1 añadió soporte para SPFx 1.5.1, descrito entonces como "un paso en nuestro viaje a largo plazo para mejorar y ampliar las capacidades de SharePoint Framework en SharePoint Server Subscription Edition".

La actualización siguiente, la 23H2, fue un paso más allá y añadió soporte para React 16 y Office UI Fabric React 7 en las soluciones de SPFx. Después de eso, nada: leyendo de la 24H1 a la 26H1, ninguna menciona siquiera SPFx. La 26H1 trae Modern People Card, Document Intelligence y una opción de configuración del People Picker. Nada sobre el framework.

También encontrará artículos de la comunidad que reclaman un techo bastante más alto. No hemos podido confirmar ninguno de ellos contra una fuente de primera mano, así que no vamos a repetir un número de versión que no podemos sostener.

De ahí sale una instrucción práctica, no un número: compruébelo en la granja, no en el artículo. Publique un paquete trivial, construido con la versión que piensa usar, en su propio catálogo de aplicaciones, y vea si carga. Quince minutos de eso valen más que cualquier tabla, incluida la de arriba, porque el techo es una propiedad de su build.

Sea cual sea el resultado, cuente con que se quede quieto. Diseñe como si la versión que tiene fuese la versión que se queda.

El entorno que construye de verdad

La primera trampa es la cadena de herramientas, y pilla a casi todo el mundo una vez.

El generador Yeoman actual no le va a ayudar. A partir de la versión 1.13.0 solo produce proyectos para SharePoint Online. Instale el más reciente y el destino on-premises sencillamente no aparece. Hace falta un generador lo bastante antiguo como para seguir preguntando:

bash
npm install gulp-cli@2.3.0 --global
npm install yo@2.0.6 --global
npm install @microsoft/generator-sharepoint@1.10.0 --global

Después se elige el destino on-premises en el flujo del generador.

Node es antiguo. SPFx 1.4.1 y 1.5.1 apuntan por defecto a Node 6 y 8. La documentación indica que se pueden hacer funcionar en Node v12.18.1, v14.17.1 y v16.15.0 con dos cambios: graceful-fs en la v4 o posterior, y sustituir node-sass por sass. Esa es la diferencia entre una máquina de build que alguien todavía puede levantar en 2026 y otra que no.

Anótelo todo en el repositorio. La versión del generador, la de Node, la de gulp-cli. Dentro de tres años quien recoja esto no lo adivinará, y no habrá ningún artículo que se lo diga. Un .nvmrc y una sección corta de CONTRIBUTING no cuestan nada ahora y ahorran una semana después.

Planifique el caso desconectado. Generar el proyecto necesita acceso a npm. Si las máquinas de desarrollo no llegan al registro, hace falta uno local, y la propia guía de Microsoft es directa sobre el coste: más software y una cantidad significativa de trabajo de mantenimiento. Presupuéstelo con honestidad en vez de descubrirlo en la tercera semana.

Los comandos de build son los de gulp, que es el único sitio donde los artículos más antiguos siguen teniendo razón:

bash
gulp serve
gulp bundle --ship
gulp package-solution --ship

Hablar con SharePoint

SPHttpClient es el camino. Se ocupa del request digest y de la autenticación, y se comporta igual en todas las versiones de SPFx, lo que lo convierte en lo más duradero de la caja.

Póngalo en un servicio. No en un componente.

typescript
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';

export interface IDocument {
  Id: number;
  Title: string;
  Modified: string;
}

// Built apart from the call that uses it, so it can be exercised without a
// client, a farm or a browser. It is also the line most likely to be wrong.
export function itemsUrl(webUrl: string, listTitle: string, top: number): string {
  // A list called "HR & Payroll" or "Q1 'draft' items" breaks this. The test
  // list is always called Documents, which is why it reaches production.
  var list = encodeURIComponent(listTitle.replace(/'/g, "''"));

  return webUrl + "/_api/web/lists/getbytitle('" + list + "')/items" +
    '?$select=Id,Title,Modified' +
    '&$orderby=Modified desc' +
    '&$top=' + top;
}

// One place decides what an HTTP status means. Components never see numbers.
// Not called describe, because mocha puts a global function of that name in
// every test file, and the collision confuses long before it fails.
export function describeStatus(status: number): string {
  if (status === 401) { return 'notAuthenticated'; }
  if (status === 403) { return 'accessDenied'; }
  if (status === 404) { return 'notFound'; }
  if (status === 429 || status === 503) { return 'throttled'; }
  return 'unknown';
}

export class DocumentService {
  constructor(
    private readonly client: SPHttpClient,
    private readonly webUrl: string
  ) {}

  public getRecent(listTitle: string, top: number): Promise<IDocument[]> {
    return this.client
      .get(itemsUrl(this.webUrl, listTitle, top), SPHttpClient.configurations.v1)
      .then(function (response: SPHttpClientResponse): Promise<any> {
        if (!response.ok) {
          return Promise.reject(describeStatus(response.status));
        }
        return response.json();
      })
      .then(function (json: { value: IDocument[] }): IDocument[] {
        return json.value;
      });
  }
}

Hay cuatro detalles ahí dentro que importan más de lo que parecen.

El URL lo construye una función propia. No es orden: es la única parte del servicio que se puede probar sin una granja, y la sección siguiente se apoya en ella.

La comilla simple se duplica antes de codificar el título, y el orden importa. La sintaxis REST de SharePoint pone el título de la lista entre comillas simples, así que una lista llamada Q1 'draft' items cierra la cadena antes de tiempo; OData escapa una comilla duplicándola. encodeURIComponent deja la comilla en paz a propósito, de modo que duplicar después sería demasiado tarde.

$select es explícito. Pedir todo y usar tres campos es ancho de banda que se paga en cada render, y en una granja que sirve a unos miles de personas eso no es gratis.

La cadena de promesas no es nostalgia. TypeScript 2.4 compilando a ES5 no da async ni await sin un runtime de generadores. La cadena es la versión que compila siempre.

Si quiere una API más amable, PnPjs funciona, pero tiene que fijar una versión que soporte su SPFx y tratar ese pin como permanente. Un npm update que le empuje hacia delante en silencio produce una solución que compila en su máquina y falla en el servidor, que es la peor clase de defecto para diagnosticar.

Componentes, y qué React le toca en realidad

La documentación se contradice aquí, y vale más resolverlo que rodearlo.

Tres fuentes de primera mano apuntan en el mismo sentido. La tabla de compatibilidad de plataformas de SPFx da a Subscription Edition v1.0 a v1.5. La nota de la 23H1 añade soporte para SPFx 1.5.1. La nota de la 23H2 añade soporte para React 16 y Office UI Fabric React 7, "permitiendo a los desarrolladores usar estas versiones más recientes de los componentes en sus soluciones de SharePoint Framework".

Una página apunta en sentido contrario. El artículo de desarrollo de SPFx para on-premises sigue afirmando que Subscription Edition "tiene exactamente las mismas dependencias y requisitos para SharePoint Framework que SharePoint Server 2019", y le manda a la v1.4.1. Esa página no se ha puesto al día: contradice la tabla de compatibilidad publicada en el mismo sitio, y describe el producto tal como era antes de la 23H1. Dos fuentes actuales contra una desactualizada no es un empate, así que la respuesta son las notas del producto.

De ahí sale algo concreto. Los hooks llegaron en React 16.8. En una granja en la 23H2 o posterior, React 16 está soportado, así que los hooks están a su disposición siempre que instale React 16.8 o superior y lo fije. La nota nombra la versión mayor y no la menor, que es exactamente la razón para fijarlo y para dejar constancia de ese pin.

Por debajo de la 23H2, y en SharePoint 2019 y 2016, está en React 15 y todos los componentes son clases.

El componente de clase merece la pena de todos modos, porque es el que corre en todas partes: en la 1.4.1 y en la 1.5.1, por encima y por debajo de la 23H2. Si escribe algo que tiene que sobrevivir a un calendario de actualizaciones que no controla, este es el suelo, y cuesta tres cosas hechas a mano.

typescript
import * as React from 'react';
import { DocumentService, IDocument } from '../services/DocumentService';

export interface IRecentDocumentsProps {
  service: DocumentService;
  listTitle: string;
  top: number;
}

export interface IRecentDocumentsState {
  status: string;
  items: IDocument[];
}

export default class RecentDocuments extends React.Component<
  IRecentDocumentsProps,
  IRecentDocumentsState
> {
  private _mounted: boolean = false;
  private _request: number = 0;

  constructor(props: IRecentDocumentsProps) {
    super(props);
    this.state = { status: 'loading', items: [] };
  }

  public componentDidMount(): void {
    this._mounted = true;
    this._load();
  }

  public componentWillUnmount(): void {
    // There is no cleanup function to return here. Without this flag, a slow
    // list on a busy farm updates a component that is already gone every time
    // somebody navigates away mid-load.
    this._mounted = false;
  }

  public componentDidUpdate(previous: IRecentDocumentsProps): void {
    // Compare first. Reloading unconditionally is an infinite loop: load, set
    // state, re-render, load again. It is silent until somebody opens the
    // network tab.
    if (previous.listTitle !== this.props.listTitle || previous.top !== this.props.top) {
      this._load();
    }
  }

  private _load(): void {
    var self = this;
    var request = ++this._request;

    this.setState({ status: 'loading', items: [] });

    this.props.service
      .getRecent(this.props.listTitle, this.props.top)
      .then(function (items: IDocument[]): void {
        // A slower answer to an older request must never overwrite a newer one.
        if (!self._mounted || request !== self._request) { return; }
        self.setState({ status: items.length ? 'ready' : 'empty', items: items });
      })
      .catch(function (failure: string): void {
        if (!self._mounted || request !== self._request) { return; }
        self.setState({
          status: failure === 'accessDenied' ? 'accessDenied' : 'error',
          items: []
        });
      });
  }

  public render(): React.ReactElement<IRecentDocumentsProps> {
    if (this.state.status === 'loading') {
      return <p role="status">Loading recent documents.</p>;
    }
    if (this.state.status === 'accessDenied') {
      return (
        <p role="alert">
          You do not have access to this library. Ask the site owner for read access.
        </p>
      );
    }
    if (this.state.status === 'error') {
      return <p role="alert">The library could not be reached. Try again in a moment.</p>;
    }
    if (this.state.status === 'empty') {
      return <p>No documents have been added yet.</p>;
    }

    return (
      <ul>
        {this.state.items.map(function (item: IDocument): JSX.Element {
          return <li key={item.Id}>{item.Title}</li>;
        })}
      </ul>
    );
  }
}

La bandera _mounted sustituye a la función de limpieza que devolvería un hook moderno.

El contador _request sustituye a todo aquello a lo que se recurriría para manejar la concurrencia. Dos cambios seguidos en el panel de propiedades lanzan dos peticiones, y vuelven en el orden que decida la granja. Sin el contador, la respuesta más antigua puede llegar la última y ganar.

La comparación de componentDidUpdate es el error clásico de los componentes de clase. Sin ella, ha construido un bucle infinito contra su propia granja.

Temas

No hay servicio ThemeProvider en esta versión ni Fluent UI v9. Lo que hay son tokens de tema en SCSS, y bastan:

scss
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.recentDocuments {
  color: "[theme: bodyText, default: #333333]";
  background-color: "[theme: white, default: #ffffff]";
  padding: 12px;
}

.recentDocuments a {
  color: "[theme: themePrimary, default: #0078d4]";
}

.recentDocuments a:hover {
  color: "[theme: themeDarkAlt, default: #106ebe]";
}

.recentDocuments .meta {
  color: "[theme: neutralSecondary, default: #666666]";
}

La regla es la misma que se aplica en cualquier plataforma y en cualquier versión: la solución toma sus colores del sitio. Escriba un valor hexadecimal directamente y habrá construido lo que se rompe el día en que alguien aplique otro tema, y en una granja que sirve a varios departamentos con varios temas ese día está cerca.

El valor default: es lo que aparece donde no hay tema aplicado. Elíjalo de forma que la web part siga siendo legible en vez de invisible.

Los permisos son experiencia de uso

Un fallo no es una sola cosa, y on-premises esto pesa más de lo que se espera, porque los permisos en una granja con estructuras heredadas y segmentación de audiencia son genuinamente complicados.

Cinco situaciones, y la persona puede actuar sobre cuatro de ellas:

  • Sesión no iniciada. Actualizar y volver a entrar.
  • Acceso denegado. Pedírselo al propietario del sitio. No es un error que haya causado el lector, y merece una instrucción en vez de una disculpa.
  • No encontrado. La lista se renombró o se borró. Diga cuál estaba buscando.
  • Throttling o indisponibilidad. La granja está ocupada. Ofrezca una forma de volver a intentarlo.
  • No hay nada. La consulta funcionó y la respuesta está vacía. Eso no es ningún fallo.

Asígnelos una vez, en el servicio, como hace el código de arriba. Después diseñe cada uno. Una web part que dice "Se ha producido un error" en los cinco casos es una web part que genera un ticket de soporte en cada uno de ellos.

Y Microsoft Graph

Esta pregunta vuelve siempre, y la respuesta habitual, que sus datos están en sus servidores y por tanto Graph es irrelevante, es demasiado fácil. Hay muchas granjas al lado de un tenant de Microsoft 365, y querer datos de perfil de ahí es perfectamente razonable.

La respuesta concreta sirve más. MSGraphClient y AadHttpClient se introdujeron ambos en SPFx 1.4.1, así que las clases existen en su versión. Dos cosas se interponen.

Las notas de la 1.4.1 los describen como developer preview, "disponibles para uso en vista previa dentro de SharePoint Online", y explícitamente "no pensados para uso en producción todavía".

Más decisivo aún, el modelo de permisos del que dependen es infraestructura de SharePoint Online de principio a fin. Se declaran webApiPermissionRequests en package-solution.json; publicar en el catálogo de aplicaciones crea solicitudes de permiso; un administrador las aprueba en la página de acceso a API del centro de administración de SharePoint; la concesión queda guardada en la aplicación SharePoint Online Client Extensibility que Microsoft aprovisiona en todos los Entra ID. Una granja propia no tiene nada de eso, así que no hay a quién pedir ni a quién conceder.

El rodeo obvio también está cerrado: la documentación afirma que usar la Microsoft Authentication Library directamente con SPFx no está soportado a partir de la v1.4.1.

Para ser preciso sobre lo que está establecido y lo que no: está documentado que los clientes eran vista previa y estaban circunscritos a SharePoint Online, que su mecanismo de permisos es un servicio de SharePoint Online, y que MSAL no está soportada. Lo que no está documentado en ningún sitio que hayamos encontrado es una vía on-premises soportada. La ausencia de documentación no prueba que nada funcione, pero una solución en producción por una vía no documentada, en una granja que habrá que parchear los próximos nueve años, es una decisión que se toma con los ojos abiertos.

El patrón que sí aguanta es un servicio propio. Una API publicada donde usted la controla, protegida como decida, que habla con Graph del lado del servidor y a la que su web part llama con SPHttpClient o con un HttpClient normal. La gestión de tokens deja de ser un problema del navegador, y la solución se queda dentro de lo soportado.

Pruebas: el runner ya viene en la caja

Se suele dar por hecho que aquí no hay runner de pruebas. El artículo de CI/CD de la propia Microsoft es el origen de esa idea, y vale la pena leer la frase con atención: "SharePoint Framework no proporciona un framework de pruebas por defecto (desde la 1.8.0)".

Desde la 1.8.0. Por debajo de eso, sí lo proporciona, y la versión que acepta Subscription Edition está por debajo de eso.

En la 1.5.1, el paquete de build @microsoft/sp-build-web depende de @microsoft/gulp-core-build-karma, que trae Karma, Mocha, Chai, Sinon, sinon-chai, karma-coverage, el instrumentador Istanbul y PhantomJS. El generador pone @types/chai y @types/mocha en el proyecto que le escribe. Hay una tarea gulp test esperando, documentada como "ejecuta pruebas unitarias, si las hay".

Lo que falta no es la maquinaria. Es un fichero de configuración, y alguna prueba. Nadie se lo dice, así que gulp test parece averiado y todo el mundo concluye que ahí no hay nada.

Tres cosas que nada le avisa

La tarea no hace nada mientras no exista un fichero de configuración. Busca ./karma.config.js en la raíz del proyecto. Si el fichero no está, escribe un aviso y vuelve, y el build sigue en verde. Escríbalo una vez:

bash
gulp test --initkarma

Eso copia la configuración por defecto a su proyecto, y a partir de ahí gulp test se ejecuta de verdad.

El runner lee el compilado, no sus fuentes. El patrón por defecto es /.+\.test\.js?$/ aplicado a la carpeta lib. La tarea genera temp/tests.js con un require.context de webpack sobre lib, y Karma carga ese único fichero. Así que una prueba escrita en src/webparts/recentDocuments/services/DocumentService.test.ts se encuentra como lib/webparts/recentDocuments/services/DocumentService.test.js una vez que TypeScript ha corrido. Dos consecuencias: llame al fichero .test.ts y nada más (un .spec.ts se ignora en silencio, sin error y sin aviso), y recuerde que una carpeta lib vieja ejecuta pruebas viejas. gulp clean cuando un resultado le sorprenda.

Las pruebas que fallan no hacen fallar el build. failBuildOnErrors es false por defecto, así que una prueba en rojo escribe un aviso y el build continúa. El modo de producción es la excepción: con --ship, el fallo lo detiene. Vale la pena cambiar ese valor por defecto el primer día, porque un aviso en medio de un muro de salida de build es una batería de pruebas que nadie lee. En gulpfile.js, antes de build.initialize(gulp):

javascript
build.karma.setConfig({ failBuildOnErrors: true });

La prueba

Karma carga los frameworks mocha y sinon-chai, así que describe e it son globales y Chai está ahí para importar. Nada necesita un renderizador, porque nada de lo que merece la pena probar aquí renderiza.

typescript
// src/webparts/recentDocuments/services/DocumentService.test.ts
import { expect } from 'chai';
import { itemsUrl, describeStatus } from './DocumentService';

describe('itemsUrl', () => {
  // Everybody develops against a list called Documents. Production has
  // "HR & Payroll" and "Q1 'draft' items", and that is where this breaks.
  it('doubles a single quote, which OData would read as the end of the title', () => {
    expect(itemsUrl('https://sp/sites/hr', "Q1 'draft' items", 5))
      .to.contain("getbytitle('Q1%20''draft''%20items')");
  });

  it('encodes an ampersand, which would otherwise start a new parameter', () => {
    expect(itemsUrl('https://sp/sites/hr', 'HR & Payroll', 5))
      .to.contain("getbytitle('HR%20%26%20Payroll')");
  });

  it('asks only for the three fields it renders', () => {
    expect(itemsUrl('https://sp', 'Documents', 5))
      .to.contain('$select=Id,Title,Modified');
  });
});

describe('describeStatus', () => {
  it('separates the failures a person can act on', () => {
    expect(describeStatus(401)).to.equal('notAuthenticated');
    expect(describeStatus(403)).to.equal('accessDenied');
    expect(describeStatus(404)).to.equal('notFound');
  });

  it('reads a busy farm as throttled rather than broken', () => {
    expect(describeStatus(429)).to.equal('throttled');
    expect(describeStatus(503)).to.equal('throttled');
  });

  it('does not pretend to recognise a status it has never seen', () => {
    expect(describeStatus(500)).to.equal('unknown');
  });
});
bash
gulp test                     # once, with coverage
gulp test --match itemsUrl    # passed through to mocha as a grep
gulp test --debug             # leaves the browser open, skips instrumentation

PhantomJS, que es la parte que de verdad le va a frenar

La configuración por defecto lista exactamente un navegador, y es PhantomJS, cuyo desarrollo se suspendió en 2018.

phantomjs-prebuilt descarga un binario en la instalación, a partir de una tabla con cuatro entradas: linux x64, linux ia32, darwin y win32. Cualquier otra cosa recibe "no hay binario disponible para su plataforma o arquitectura" y la instalación falla. Fíjese en lo que significa ahí darwin, porque es lo que parece funcionar y no funciona: hay una sola compilación para macOS y es x86_64, así que en Apple Silicon la descarga funciona y se obtiene un binario Intel que necesita Rosetta. En Linux arm64 no hay entrada ninguna.

El servidor de descarga es una release de GitHub, y es sustituible:

bash
PHANTOMJS_CDNURL=https://your-mirror/phantomjs npm install

Es el mismo problema de las máquinas desconectadas que apareció antes en este artículo, y tiene la misma forma: una cosa más que replicar, presupuestada con honestidad en vez de descubierta la mañana del build.

Si PhantomJS no es viable, y después de 2018 normalmente no lo es, cambie el navegador, no el framework. Mocha, Chai y todas las pruebas de arriba se quedan exactamente como están; solo cambia el launcher en karma.config.js, a karma-chrome-launcher con ChromeHeadless. Tenga presente que Karma aquí está en la 0.13, así que qué versión de launcher coopera es algo que se descubre probando, no leyendo. Es una tarde acotada y no toca ni una línea del código de prueba.

La otra vía, si prefiere dejar el navegador completamente fuera, es Jest, al que el artículo de CI/CD de Microsoft apunta directamente y hacia donde SPFx fue de todos modos a partir de la 1.8.0. Para un proyecto en React 15 esa documentación nombra el preset: @voitanos/jest-preset-spfx-react15. Corre en Node, así que PhantomJS deja de ser un problema suyo, al coste de una segunda cadena de herramientas al lado de gulp test.

Qué probar, y qué no

Se gana su sitio:

  • La construcción del URL. Una comilla simple, un ampersand, un espacio, un acento. La prueba de mayor valor de toda la solución, y son los tres casos de arriba.
  • La asignación de estados. Un 403 da un estado distinto de un 500, y un 404 distinto de los dos.
  • La lectura de la respuesta. Un campo que falta, una fecha que no convierte, una lista vacía.

No se gana su sitio: afirmar que React renderizó un <ul>.

Y una disciplina que importa más que todo esto: rompa cada guarda a propósito y compruebe que una prueba se pone en rojo. Borre la bandera _mounted, o la comparación de componentDidUpdate, y ejecute la batería. Si todo sigue pasando, ha aprendido algo real: o esa guarda no está probada, o el escenario no la ejercita, o la prueba está midiendo la propiedad equivocada. Una batería verde que sigue verde después de sabotear el código no dice que el código esté bien. No dice nada.

Accesibilidad

React 15 renderiza el mismo HTML que React 19. Nada de esto depende de su versión.

  • Una lista de resultados es un <ul> de <li>, no una pila de <div>.
  • El enlace lleva el título, para que quien navega por enlaces oiga nombres de documentos en vez de "leer más" repetido.
  • Los cambios de estado van a una live region, que es lo que hacen role="status" y role="alert" en el componente de arriba.
  • Todos los controles se alcanzan con el teclado y el foco se ve.
  • Nada se transmite solo por el color. Un estado necesita una palabra.

Compruébelo en el árbol de accesibilidad del navegador, y no a ojo. Lo que lee un lector de pantalla no siempre es lo que sugiere el marcado.

Menos dependencias, y por qué pesa más aquí

Cada biblioteca que se añade queda atada a un TypeScript antiguo y a un React antiguo, se quiera o no, y ahí se queda atada. En un tenant se actualiza en silencio; en una granja, una ventana de actualización es una petición de cambio con un fin de semana de mantenimiento pegado.

Antes de añadir nada, pregúntese si SharePoint ya lo hace. Un gráfico suele ser mejor como un número con una barra proporcional. Un PDF suele ser mejor como la impresión del propio navegador con una hoja de estilos de impresión, que además da texto seleccionable.

La medición que zanja la discusión es el tamaño del paquete en un build limpio, antes y después.

Mantenga aburrido el panel de propiedades

Dos preguntas deciden si un ajuste pertenece ahí.

¿Quien añade la web part lo configura una vez, o el lector lo cambia mientras la usa? Si es lo segundo, pertenece a la interfaz.

¿Cambia el comportamiento? Si una propiedad se acepta y se ignora, no es un hueco reservado para una versión futura. Es un defecto que alguien va a reportar, después de haber confiado en ella.

Cuatro propiedades que hacen todas algo valen más que doce que casi ninguna hace, en cualquier versión de cualquier cosa.

Publicación

El catálogo de aplicaciones de su propia granja, y el workbench en /_layouts/workbench.aspx en un sitio que usted controle. Ese workbench no es el alojado que se está retirando en SharePoint Online; el suyo vive mientras viva la granja.

Dos cosas que vale la pena meter en la rutina desde el principio. Versione el .sppkg deliberadamente en package-solution.json en vez de dejarlo en 1.0.0.0 para siempre, porque un catálogo de aplicaciones que muestra la misma versión para tres builds distintos es un problema de soporte esperando a pasar. Y mantenga un registro de qué build está publicada dónde, ya que no hay ningún servicio que se lo diga.

El resumen

Una pista larga sobre una base quieta no es mal sitio donde estar. Premia decisiones que son baratas ahora y caras después: poner las llamadas en un servicio, asignar los errores una vez, tomar los colores del sitio, proteger las actualizaciones de estado, probar las partes puras, añadir lo menos posible.

Una web part escrita así en la 1.5.1 seguirá funcionando cuando se cierre la ventana de soporte de la plataforma, en 2035. Una escrita de la otra manera no sobrevive a su primer cambio de tema, y en ambos casos será usted quien la mantenga.

Antes de empezar

  • Confirme el nivel de actualización de características de la granja y verifique el techo de SPFx publicando un paquete trivial, en vez de fiarse de una tabla
  • Compruebe si la granja está en la 23H2 o posterior, porque eso es lo que decide si tiene React 16 y hooks, y fije la versión de React en cualquier caso
  • Fije y anote en el repositorio las versiones del generador, de Node, de gulp-cli y de Yeoman
  • Planifique la cuestión del acceso a npm antes del primer día, si las máquinas están desconectadas
  • Ponga todas las llamadas en un servicio, y asigne los estados HTTP en un solo sitio
  • Escape las comillas simples y codifique los títulos de lista en todos los URL de REST
  • Proteja las actualizaciones de estado con una bandera de montaje y un contador de peticiones
  • Compare las props antes de recargar en componentDidUpdate
  • Use tokens de tema en SCSS, nunca un valor hexadecimal
  • Diseñe un estado para cada fallo, incluido el de respuesta vacía
  • Ejecute gulp test --initkarma una vez, llame .test.ts a los ficheros de prueba, y ponga failBuildOnErrors: true
  • Resuelva la cuestión de PhantomJS antes de escribir pruebas, cambiando el launcher o pasando a Jest
  • Pruebe la construcción del URL, la asignación de estados y la lectura de la respuesta, y rompa cada guarda para demostrar que la prueba se pone en rojo
  • Fije PnPjs en una versión que soporte su SPFx, y trate ese pin como permanente
  • Versione el paquete deliberadamente y anote qué está publicado dónde

Seguir leyendo

Migraciones

SharePoint 2016, 2019, Subscription Edition y Online: qué cambia realmente

2016 y 2019 terminan ambos el 14 de julio de 2026. Subscription Edition no tiene fecha de fin y no se puede comprar de una vez. Online tiene funciones que los demás nunca tendrán, y Subscription Edition tiene dos que suelen aparecer listadas como imposibles. Una comparación práctica, con el licenciamiento que decide.

·20 min de lectura
IA y agentes

Un manifiesto de agente de Copilot es una frontera de seguridad. Aquí está el verificador.

Todas las propiedades de ámbito de un manifiesto de agente de Microsoft 365 Copilot son opcionales y, en seis casos, omitir una da el ámbito más amplio en lugar del más estrecho. Un validador de JSON Schema aprueba esos manifiestos, porque ninguno tiene nada inválido: falta algo que era opcional. Así que escribimos la comprobación que sí los detecta. Encuentra seis problemas en un manifiesto que pasa todas las demás pruebas.

·6 min de lectura
Desarrollo y automatización

SPFx antes de la versión 1.0: tres web parts nuestras en los samples oficiales de Microsoft

La developer preview de SharePoint Framework salió en agosto de 2016. La versión 1.0 llegó en febrero de 2017. Nuestra primera contribución al repositorio oficial de samples de Microsoft 365 es de octubre de 2016, cinco meses antes de que hubiera una 1.0 sobre la que construir. Tres de nuestras web parts están hoy en ese repositorio, y esto es lo que hace cada una.

·5 min de lectura