Plone-Tagung Dresden 2020 - Patterns und Best Practices für die Entwicklung erweiterbarer und leistungsstarker React SPAs am Beispiel der Neos CMS-Benutzeroberfläche

Plone-Tagung Dresden 2020 - Patterns und Best Practices für die Entwicklung erweiterbarer und leistungsstarker React SPAs am Beispiel der Neos CMS-Benutzeroberfläche

Große React-Anwendungen (Single Page Applications) zu schreiben, stellt uns vor viele Herausforderungen – insbesondere dann, wenn die Anwendungen nicht nur stabil und performant im Browser laufen, sondern auch erweiterbar sein sollen.

Die Benutzeroberfläche von Neos ist eine solche React Single Page Application, welche an vielen Stellen erweiterbar ist. Im ersten Teil des Vortrages wird es um Lessons Learned zum Bau einer stabilen und performanten großen React-Anwendung gehen, beispielsweise um Redux und Reselect.

Im zweiten Teil des Vortrages wird das Registry-Pattern eingeführt, welches den Kern der React-Applikations-Erweiterbarkeit in der Neos-Oberfläche implementiert. Anhand von praktischen Beispielen wird gezeigt, wie mit diesem Pattern geplante und ungeplante Erweiterbarkeit in React-Anwendungen umgesetzt werden kann.

30c0b6f50f67163bee8500aa4115d126?s=128

Sebastian Kurfürst

March 10, 2020
Tweet

Transcript

  1. Plone-Tagung 10.03.2020 | Dresden Patterns und Best Practices für die

    Entwicklung erweiterbarer und leistungsstarker React SPAs am Beispiel der Neos CMS-Benutzeroberfläche
  2. Sebastian Kurfürst Mitgründer, Technical Officer sandstorm Neos Team Member @skurfuerst

  3. Generalist Webentwickler seit >15Y Neos CMS Core Developer (früher TYPO3)

    Skalierbare Architektur PHP, Java, Groovy, Kotlin, Go JavaScript/TypeScript, Ember, React DevOps, Ansible, Docker, Kubernetes SQL, Elasticsearch, Redis *me
  4. None
  5. Open Source auf neos.io

  6. None
  7. None
  8. Hauptfunktionen in-place editing* beliebiges Ausgabe-Markup *for real

  9. Baumstrukturierter Inhalt main (ContentCollection) support (Page) SignUp (Form) KiteSupport (Text)

    sidebar (ContentCollection) FindSerialNumbers (Image) WinAKite (Text) komplett anpassbare Node Types sinnvolle Default-Typen möglich
  10. None
  11. multiple languages

  12. publishing and review workflow

  13. X D Developer Experience

  14. erweiterbar auf allen Ebenen

  15. basiert auf Flow PHP Framework Domain-Driven Design Framework Dependency Injection

    / Aspect-Oriented Programming ... Backend
  16. in React / Redux geschrieben Frontend ~ 1000 files ~45

    000 LOC (JavaScript / TypeScript)
  17. React

  18. SideBar render() props App render() class App extends Component {

    render() { return <SideBar isHidden={/*todo*/} /> } } class SideBar extends Component { render() { return ( <div className={this.props.isHidden ? 'sidebar--hidden' : 'sidebar--visible'}> TODO: Render sidebar here... </div> ); } }
  19. class SideBar extends Component { render() { return ( <div

    className={this.props.isHidden ? 'sidebar--hidden' : 'sidebar--visible'}> TODO: Render sidebar here... </div> ); } } initiales Rendering und update nutzen selben Code!
  20. y = f(x) RenderedApplication = React(ApplicationState) *pure: - ohne Seiteneffekte

    - deterministisch - Pure* Function
  21. Performance

  22. None
  23. class SideBar extends Component { shouldComponentUpdate(nextProps) { // somehow compare

    nextProps and this.props, // and return true if update is needed } }
  24. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false rekursiver Vergleich notwendig! mutating state
  25. state data UI sidebar collapsed = true ... ... ...

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false Referenzvergleich ausreichend! immutable state sidebar UI state !! hohe Performance !!
  26. class SideBar extends Component { shouldComponentUpdate(nextProps) { return this.props.sidebar ===

    nextProps.sidebar; } }
  27. Immutable State nutzen ermöglicht performantes Erkennen von Änderungen

  28. PureComponent nutzen class SideBar extends PureComponent { // - component

    is like a pure function, // depending only on its input props. // - Automatically shallowly compares // props by reference (like we have // just seen) } ... oder React.memo
  29. Welche Komponenten rendern sich erneut?

  30. None
  31. Warum rendern Komponenten erneut? import debugReasonForRendering from '@neos-project/debug-reason-for-rendering'; @debugReasonForRendering export

    default class Inspector extends PureComponent { // ...
  32. None
  33. reselect nutzen Cached Computed Properties für Redux CurrentlyEditedUser CurrentlyEditedUserId Users

  34. *reselect 1 const contextForNodeLinking = createSelector( 2 [ 3 $get('currentlyEditedUserId'),

    4 $get('users') 5 ], 6 function( 7 currentlyEditedUserId, 8 users 9 ){ 10 return users[currentlyEditedUserId]; 11 } 12 );
  35. None
  36. Erweiterbarkeit

  37. wir benötigen einen zentralen Ort, wo sich Leute in unser

    System hängen können
  38. Wir benötigen einen klaren Zeitpunkt, wann Leute sich in unser

    System hängen können.
  39. Registries und manifest files werden angefragt für Komponenten- Instanzen und

    Anwendungsteile einziger Punkt im Lebenszyklus, wo Registries verändert werden können.
  40. time Bootstrap-Sequenz manifest loader initialisieren erweiterungs-scripte laden und deren Manifeste

    laden manifeste ausführen registries wie benötigt verändern registries einfrieren Anwendung weiter starten
  41. Beispiel 1: Color Picker

  42. None
  43. None
  44. { "scripts": { "build": "neos-react-scripts build", "watch": "neos-react-scripts watch" },

    "devDependencies": { "@neos-project/neos-ui-extensibility": "~1.0.8" }, "neos": { "buildTargetDirectory": "../../Public/ColorPickerEditor" }, "dependencies": { "react-color": "^2.11.1" } } package.json
  45. import manifest from '@neos-project/neos-ui-extensibility'; import ColorPickerEditor from './ColorPickerEditor'; manifest('Neos.Neos.Ui.ExtensibilityExamples:ColorPickerEditor', {},

    globalRegistry => { const inspectorRegistry = globalRegistry.get('inspector'); const editorsRegistry = inspectorRegistry.get('editors'); editorsRegistry.set('Neos.Neos.Ui.ExtensibilityExamples/ColorPickerEditor', { component: ColorPickerEditor }); }); manifest.js
  46. export default class ColorPickerEditor extends Component { handleChangeColor = newColor

    => { this.props.commit(newColor.hex); }; render() { return (<div> <SketchPicker color={this.props.value} onChange={this.handleChangeColor}/> </div>); } } ColorPickerEditor.js
  47. yarn build

  48. Neos: Neos: Ui: resources: javascript: 'Neos.Neos.Ui.ExtensibilityExamples:ColorPickerEditor': resource: resource://Neos.Neos.Ui.ExtensibilityExamples /Public/ColorPickerEditor/Plugin.js Settings.yaml

  49. 'Vendor.Site:MyNodeType': properties: color: ui: label: 'Color picker' inspector: editor: 'Neos.Neos.Ui.ExtensibilityExamples/ColorPickerEditor'

    NodeTypes.yaml
  50. None
  51. Beispiel 2: Command Palette

  52. None
  53. import manifest from '@neos-project/neos-ui-extensibility'; import wrapWithKeyboardListener from './WrapWithKeyboardListener'; manifest('Neos.Neos.Ui.ExtensibilityExamples:WrapWithKeyboardListener', {},

    globalRegistry => { const containerRegistry = globalRegistry.get('containers'); const ApplicationContainer = containerRegistry.get('App'); const enhancedApplicationContainer = wrapWithKeyboardListener(ApplicationContainer); containerRegistry.set('App', enhancedApplicationContainer); }); manifest.js
  54. //... const wrapWithKeyboardListener = OriginalApp => { return class CustomKeyboardListener

    extends Component { //... render() { return ( <React.Fragment> {this.renderPalette()} <OriginalApp {...this.props}/> </React.Fragment> ); } //... } } export default wrapWithKeyboardListener; WrapWithKeyboardListener
  55. None
  56. Was passiert intern?

  57. @neos(globalRegistry => ({ containerRegistry: globalRegistry.get('containers') })) export default class LeftSideBar

    extends PureComponent { render() { const {containerRegistry} = this.props; const LeftSideBarTop = containerRegistry.getChildren('LeftSideBar/Top'); return ( <SideBar> <div className={style.leftSideBar__top}> {LeftSideBarTop.map((Item, key) => <Item key={key}/>)} </div> </SideBar> ); } }
  58. Positionierung?

  59. •handles •after handles •10 •20 •before handles •end •start •start

    100 •end 100 Implementiert in @neos-project/positional-array-sorter
  60. None
  61. Dynamische Erweiterbarkeit

  62. bisher haben wir gezeigt, wie Registries verwendet werden, um erweiterbare

    Funktionalität zu implementieren
  63. aber wir müssen JavaScript noch erneut compilieren!

  64. react Neos UI App Color Picker Plugin statisch compilierte Architektur

    single JS bundle
  65. react Neos UI App Color Picker Plugin core JS bundle

    dynamisch gelinkte Architektur react shim ... plugin b.
  66. window.NeosApiExposureMap = { '@vendor': { React, ReactDOM, PropTypes // ...

    }, '@NeosProjectPackages': { NeosUiEditors, ReactUiComponents // ... } }; öffentliche APIs exportieren
  67. webpack config return { // ... resolve: { // override

    config! alias: { 'react': '@neos-project/neos-ui-extensibility/src/shims/vendor/react/index', 'react-dom': '@neos-project/neos-ui-extensibility/src/shims/vendor/react-dom/index', 'prop-types': '@neos-project/neos-ui-extensibility/src/shims/vendor/prop-types/index', '@neos-project/react-ui-components': '@neos-project/neos-ui-extensibility/src/shims/neosProjectPackages/react-ui-components/index', '@neos-project/neos-ui-editors': '@neos-project/neos-ui-extensibility/src/shims/neosProjectPackages/neos-ui-editors/index', } } }; module.exports = window.NeosApiExposureMap['vendor'].React; shim file
  68. None
  69. None
  70. neoscon.io 19.+20. Juni 2020 Alter Schlachthof Dresden 15% Rabattcode: Plone

  71. www.neos.io react www.sandstorm.de