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. 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. 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
  3. 4.
  4. 6.
  5. 7.
  6. 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
  7. 10.
  8. 16.
  9. 17.
  10. 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> ); } }
  11. 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!
  12. 22.
  13. 23.

    class SideBar extends Component { shouldComponentUpdate(nextProps) { // somehow compare

    nextProps and this.props, // and return true if update is needed } }
  14. 24.

    state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false rekursiver Vergleich notwendig! mutating state
  15. 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 !!
  16. 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
  17. 30.
  18. 32.
  19. 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 );
  20. 35.
  21. 39.

    Registries und manifest files werden angefragt für Komponenten- Instanzen und

    Anwendungsteile einziger Punkt im Lebenszyklus, wo Registries verändert werden können.
  22. 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
  23. 42.
  24. 43.
  25. 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
  26. 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
  27. 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
  28. 50.
  29. 52.
  30. 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
  31. 54.

    //... const wrapWithKeyboardListener = OriginalApp => { return class CustomKeyboardListener

    extends Component { //... render() { return ( <React.Fragment> {this.renderPalette()} <OriginalApp {...this.props}/> </React.Fragment> ); } //... } } export default wrapWithKeyboardListener; WrapWithKeyboardListener
  32. 55.
  33. 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> ); } }
  34. 59.

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

    100 •end 100 Implementiert in @neos-project/positional-array-sorter
  35. 60.
  36. 65.

    react Neos UI App Color Picker Plugin core JS bundle

    dynamisch gelinkte Architektur react shim ... plugin b.
  37. 66.

    window.NeosApiExposureMap = { '@vendor': { React, ReactDOM, PropTypes // ...

    }, '@NeosProjectPackages': { NeosUiEditors, ReactUiComponents // ... } }; öffentliche APIs exportieren
  38. 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
  39. 68.
  40. 69.