Pourquoi React pour les ressources web Dynamics 365 ?
Les ressources web de Dynamics 365 permettent d'intégrer du contenu HTML, JavaScript et CSS personnalisé dans les applications model-driven. Alors que le JavaScript traditionnel fonctionne bien pour les scénarios simples, les interfaces utilisateur complexes bénéficient énormément de l'architecture basée sur les composants, de la gestion d'état et de l'écosystème de React. Ressources web dans Dynamics 365.
Avantages d'utiliser React
- Réutilisabilité des composants : Construire une fois, utiliser à travers plusieurs formulaires et apps
- Gestion d'état : Flux de données prévisible avec hooks et context
- Écosystème riche : Des milliers de composants et bibliothèques prêts à l'emploi
- Expérience développeur : Hot reload, prise en charge de TypeScript, excellent outillage
- Performance : Le DOM virtuel met à jour efficacement uniquement les éléments changés
Configuration du projet
Création du projet React
Configurez un nouveau projet React configuré pour le déploiement Dynamics 365 :
# Create React app with TypeScript npx create-react-app d365-webresource --template typescript cd d365-webresource # Install additional dependencies npm install @types/xrm axios # For UI components (optional) npm install @fluentui/react
Configuration TypeScript pour Xrm
Ajoutez les définitions de types Xrm pour l'accès à l'API Dynamics 365 :
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"types": ["xrm"]
},
"include": ["src"]
}Configuration de build pour sortie fichier unique
Configurez le build pour produire un fichier JavaScript unique adapté au déploiement en tant que ressource web :
// For Create React App, use react-app-rewired
npm install react-app-rewired
// config-overrides.js
module.exports = {
webpack: function(config, env) {
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
// Single output file
config.output.filename = 'static/js/bundle.js';
return config;
}
};
// Update package.json scripts
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}Accès au contexte Dynamics 365
Obtenir le contexte parent
Les ressources web s'exécutent dans un iframe, donc vous devez accéder au contexte Xrm parent :
// src/utils/XrmContext.ts
declare global {
interface Window {
parent: Window & { Xrm?: typeof Xrm };
}
}
export function getXrm(): typeof Xrm | undefined {
// Try parent window first (when embedded in form)
if (window.parent && window.parent.Xrm) {
return window.parent.Xrm;
}
// Fall back to current window (for standalone testing)
if ((window as any).Xrm) {
return (window as any).Xrm;
}
return undefined;
}
export function getFormContext(): Xrm.FormContext | undefined {
const xrm = getXrm();
if (xrm && xrm.Page) {
return xrm.Page as unknown as Xrm.FormContext;
}
return undefined;
}Fournisseur de contexte React pour Xrm
Créez un contexte React pour fournir l'accès Xrm dans tout votre arbre de composants :
// src/context/D365Context.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { getXrm, getFormContext } from '../utils/XrmContext';
interface D365ContextType {
xrm: typeof Xrm | undefined;
formContext: Xrm.FormContext | undefined;
recordId: string | undefined;
entityName: string | undefined;
isLoading: boolean;
}
const D365Context = createContext<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
export const D365Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
useEffect(() => {
const initContext = () => {
const xrm = getXrm();
const formContext = getFormContext();
if (xrm) {
setState({
xrm,
formContext,
recordId: formContext?.data?.entity?.getId()?.replace(/[{}]/g, ''),
entityName: formContext?.data?.entity?.getEntityName(),
isLoading: false
});
} else {
setTimeout(initContext, 100);
}
};
initContext();
}, []);
return (
<D365Context.Provider value={state}>
{children}
</D365Context.Provider>
);
};
export const useD365 = () => useContext(D365Context);Intégration web API
Hook personnalisé pour les appels web API
Créez un hook personnalisé pour effectuer des appels Web API avec une authentification appropriée :
// src/hooks/useWebApi.ts
import { useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useWebApi() {
const { xrm } = useD365();
const retrieveRecord = useCallback(async (
entityName: string,
recordId: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveRecord(entityName, recordId, options);
}, [xrm]);
const retrieveMultiple = useCallback(async (
entityName: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveMultipleRecords(entityName, options);
}, [xrm]);
const createRecord = useCallback(async (
entityName: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.createRecord(entityName, data);
}, [xrm]);
const updateRecord = useCallback(async (
entityName: string,
recordId: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.updateRecord(entityName, recordId, data);
}, [xrm]);
const deleteRecord = useCallback(async (
entityName: string,
recordId: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.deleteRecord(entityName, recordId);
}, [xrm]);
return { retrieveRecord, retrieveMultiple, createRecord, updateRecord, deleteRecord };
}Construction des composants
Exemple : composant d'enregistrements liés
Construisez un composant qui affiche les enregistrements liés avec des capacités d'édition :
// src/components/RelatedContacts.tsx
import React, { useEffect, useState } from 'react';
import { useD365 } from '../context/D365Context';
import { useWebApi } from '../hooks/useWebApi';
interface Contact {
contactid: string;
fullname: string;
emailaddress1: string;
telephone1: string;
}
export const RelatedContacts: React.FC = () => {
const { recordId, entityName, isLoading: contextLoading } = useD365();
const { retrieveMultiple } = useWebApi();
const [contacts, setContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchContacts = async () => {
if (!recordId || entityName !== 'account') {
setLoading(false);
return;
}
try {
const result = await retrieveMultiple(
'contacts',
`?$select=contactid,fullname,emailaddress1,telephone1&$filter=_parentcustomerid_value eq ${recordId}`
);
setContacts(result.entities as Contact[]);
} finally {
setLoading(false);
}
};
if (!contextLoading) fetchContacts();
}, [recordId, entityName, contextLoading, retrieveMultiple]);
if (contextLoading || loading) return <div>Loading...</div>;
return (
<div className="contacts-grid">
<h3>Related Contacts ({contacts.length})</h3>
<table>
<thead>
<tr><th>Name</th><th>Email</th><th>Phone</th></tr>
</thead>
<tbody>
{contacts.map(contact => (
<tr key={contact.contactid}>
<td>{contact.fullname}</td>
<td>{contact.emailaddress1}</td>
<td>{contact.telephone1}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};Utilisation des composants Fluent UI
Fluent UI fournit des composants qui correspondent à l'apparence de Dynamics 365 :
// Using Fluent UI for consistent D365 styling
import { Card, CardHeader, Text, Badge, makeStyles } from '@fluentui/react-components';
import { PersonRegular, MailRegular } from '@fluentui/react-icons';
const useStyles = makeStyles({
card: {
marginBottom: '8px',
cursor: 'pointer',
'&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }
}
});
export const ContactCard: React.FC<ContactCardProps> = ({ contactId, fullname, email, isPrimary }) => {
const styles = useStyles();
const { xrm } = useD365();
const openContact = () => {
xrm?.Navigation.openForm({ entityName: 'contact', entityId: contactId });
};
return (
<Card className={styles.card} onClick={openContact}>
<CardHeader
header={
<div>
<PersonRegular />
<Text weight="semibold">{fullname}</Text>
{isPrimary && <Badge>Primary</Badge>}
</div>
}
/>
</Card>
);
};Communication avec le formulaire parent
Lecture et écriture des données de formulaire
Interagissez avec les champs de formulaire depuis votre ressource web React :
// src/hooks/useFormAttribute.ts
import { useState, useEffect, useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useFormAttribute<T>(attributeName: string) {
const { formContext } = useD365();
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
setValue(attribute.getValue() as T);
attribute.addOnChange(() => setValue(attribute.getValue() as T));
}
}, [formContext, attributeName]);
const updateValue = useCallback((newValue: T) => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
attribute.setValue(newValue);
setValue(newValue);
}
}, [formContext, attributeName]);
return [value, updateValue] as const;
}
// Usage
const [accountName, setAccountName] = useFormAttribute<string>('name');
const [revenue, setRevenue] = useFormAttribute<number>('revenue');Déploiement
Build pour production
# Build the production bundle npm run build # Output structure: # build/ # index.html → Upload as HTML WebResource # static/js/bundle.js → Upload as JS WebResource # static/css/main.css → Upload as CSS WebResource
Modèle HTML de la ressource web
Créez un fichier HTML qui charge votre bundle React :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React WebResource</title>
<link href="new_/css/main.css" rel="stylesheet">
<style>
body { margin: 0; padding: 8px; font-family: 'Segoe UI', sans-serif; }
#root { min-height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script src="new_/js/bundle.js"></script>
</body>
</html>Ajout au formulaire
- Ouvrez l'éditeur de formulaire dans make.powerapps.com
- Ajoutez une nouvelle section ou onglet pour la ressource web
- Insérez le composant Ressource web HTML
- Sélectionnez votre fichier HTML uploadé
- Configurez l'étendue de lignes et colonnes selon les besoins
- Cochez « Passer le code de type d'objet d'enregistrement et l'identifiant unique comme paramètres » si nécessaire
Bonnes pratiques
Performance
- Garder la taille du bundle petite ; utiliser le code splitting si possible
- Mettre en œuvre des états de chargement pour les opérations asynchrones
- Debounce les mises à jour de champs de formulaire pour éviter les appels API excessifs
- Utiliser React.memo pour les composants coûteux
Gestion d'erreurs
- Utiliser des error boundaries pour attraper et afficher les erreurs gracieusement
- Gérer les cas où le contexte Xrm n'est pas disponible (scénarios de test)
- Logger les erreurs vers Application Insights ou un logging personnalisé
Workflow de développement
- Utiliser des données mock pendant le développement local
- Configurer des variables d'environnement pour différents environnements D365
- Automatiser le déploiement des ressources web avec des outils CLI
- Inclure les ressources web dans votre solution pour l'ALM
Conclusion
Construire des ressources web avec React apporte les pratiques modernes de développement web à la personnalisation Dynamics 365. La combinaison du modèle de composants de React, de la sécurité de type de TypeScript et de l'extensibilité de Dynamics 365 crée des expériences personnalisées puissantes qui améliorent la productivité des utilisateurs. Commencez par des composants simples et construisez progressivement des solutions plus complexes à mesure que vous vous familiarisez avec les modèles d'intégration.