Patterns and Best Practices for developing extensible and high performance SPAs

Patterns and Best Practices for developing extensible and high performance SPAs

Patterns and Best Practices for developing extensible and high performance SPAs using the Neos CMS user interface as an example - JS Meetup Dresden

30c0b6f50f67163bee8500aa4115d126?s=128

Sebastian Kurfürst

July 12, 2018
Tweet

Transcript

  1. 1.

    JS Meetup 12.07.2018 | Dresden Patterns and Best Practices for

    developing extensible and high performance SPAs using the Neos CMS user interface as an example
  2. 3.
  3. 5.
  4. 6.
  5. 8.

    tree-structured content main (ContentCollection) support (Page) SignUp (Form) KiteSupport (Text)

    sidebar (ContentCollection) FindSerialNumbers (Image) WinAKite (Text) completely customizable node types start with sane default types, or roll your own
  6. 9.
  7. 10.
  8. 17.
  9. 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> ); } }
  10. 19.

    class SideBar extends Component { render() { return ( <div

    className={this.props.isHidden ? 'sidebar--hidden' : 'sidebar--visible'}> TODO: Render sidebar here... </div> ); } } initial rendering and data update use same code path!
  11. 22.
  12. 23.

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

    nextProps and this.props, // and return true if update is needed } }
  13. 25.

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

    state.UI.sidebar.collapsed = false mutating state
  14. 26.

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

    state.UI.sidebar.collapsed = false collapsed = false mutating state
  15. 27.

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

    state.UI.sidebar.collapsed = false collapsed = false deep comparison needed! mutating state
  16. 29.

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

    collapsed = false immutable state
  17. 30.

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

    collapsed = false immutable state sidebar
  18. 31.

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

    collapsed = false immutable state sidebar
  19. 32.

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

    collapsed = false immutable state sidebar UI state
  20. 33.

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

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false immutable state sidebar UI state
  21. 34.

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

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false reference comparison is enough! immutable state sidebar UI state
  22. 35.

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

    sidebarState = {...sidebarState, collapsed: false}; uiState = {...uiState, sidebar: sidebarState}; state = {...state, ui: uiState}; collapsed = false reference comparison is enough! immutable state sidebar UI state !! great performance !!
  23. 38.

    embrace PureComponent 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) }
  24. 40.
  25. 42.
  26. 44.

    *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 );
  27. 45.
  28. 49.

    meet the Registries and manifest files can be queried for

    instances of components and application parts only point in lifecycle where registries can be mutated.
  29. 50.

    time application bootstrap initialize manifest loader include extension scripts and

    register their manifests execute the manifests modify the registries as needed freeze registries continue with application bootstrap
  30. 52.
  31. 53.
  32. 54.

    { "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
  33. 55.

    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
  34. 56.

    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
  35. 60.
  36. 62.
  37. 63.

    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
  38. 64.

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

    extends Component { //... render() { return ( <React.Fragment> {this.renderPalette()} <OriginalApp {...this.props}/> </React.Fragment> ); } //... } } export default wrapWithKeyboardListener; WrapWithKeyboardListener
  39. 65.
  40. 67.
  41. 69.
  42. 70.
  43. 72.

    @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> ); } }
  44. 74.

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

    100 •end 100 Implemented in @neos-project/positional-array-sorter
  45. 75.
  46. 80.

    react Neos UI App Color Picker Plugin core JS bundle

    dynamically linked architecture react shim ... plugin b.
  47. 81.

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

    }, '@NeosProjectPackages': { NeosUiEditors, ReactUiComponents // ... } }; export public APIs
  48. 82.

    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
  49. 83.