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. 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. Sebastian Kurfürst Mitgründer, CTO sandstorm Neos Team Member @skurfuerst

  3. None
  4. Open Source at neos.io

  5. None
  6. None
  7. main features in-place editing* any output markup possible *for real

  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
  9. None
  10. None
  11. multiple languages

  12. publishing and review workflow

  13. Great X D Developer Experience

  14. extensible on all levels

  15. based on Flow PHP Framework Domain-Driven Design Framework Dependency Injection

    / Aspect-Oriented Programming ... Backend
  16. written in React / Redux Frontend

  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> ); } } initial rendering and data update use same code path!
  20. y = f(x) RenderedApplication = React(ApplicationState) pure:
 - side-effect free


    - deterministic - 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 ... ... ...

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

    state.UI.sidebar.collapsed = false mutating state
  26. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false mutating state
  27. state data UI sidebar collapsed = true ... ... ...

    state.UI.sidebar.collapsed = false collapsed = false deep comparison needed! mutating state
  28. state data UI sidebar collapsed = true ... ... ...

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

    collapsed = false immutable state
  30. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar
  31. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar
  32. state data UI sidebar collapsed = true ... ... ...

    collapsed = false immutable state sidebar UI state
  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
  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
  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 !!
  36. class SideBar extends Component { shouldComponentUpdate(nextProps) { return this.props.sidebar ===

    nextProps.sidebar; } }
  37. embrace Immutable State as it allows performant change detection

  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) }
  39. figure out which components rerender

  40. None
  41. figure out why components rerender import debugReasonForRendering from '@neos-project/debug-reason-for-rendering'; @debugReasonForRendering

    export default class Inspector extends PureComponent { // ...
  42. None
  43. embrace reselect Cached Computed Properties for Redux CurrentlyEditedUser CurrentlyEditedUserId Users

  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 );
  45. None
  46. Extensibility

  47. we need a central place where people can hook into

    our system
  48. we need a defined point in time when people can

    hook into our system
  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.
  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
  51. Example 1: Color Picker

  52. None
  53. None
  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
  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
  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
  57. yarn build

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

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

    NodeTypes.yaml
  60. None
  61. Example 2: Command Palette

  62. None
  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
  64. //... const wrapWithKeyboardListener = OriginalApp => { return class CustomKeyboardListener

    extends Component { //... render() { return ( <React.Fragment> {this.renderPalette()} <OriginalApp {...this.props}/> </React.Fragment> ); } //... } } export default wrapWithKeyboardListener; WrapWithKeyboardListener
  65. None
  66. Example 3: Flat Navigation

  67. None
  68. Example 4: Inline Editing of Structured Properties

  69. None
  70. None
  71. Under The Covers

  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> ); } }
  73. Positioning?

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

    100 •end 100 Implemented in @neos-project/positional-array-sorter
  75. None
  76. Dynamic Extensibility

  77. so far, we've shown how functionality is built extensibly using

    registries
  78. however, we'd still need a recompile!

  79. react Neos UI App Color Picker Plugin statically compiled architecture

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

    dynamically linked architecture react shim ... plugin b.
  81. window.NeosApiExposureMap = { '@vendor': { React, ReactDOM, PropTypes // ...

    }, '@NeosProjectPackages': { NeosUiEditors, ReactUiComponents // ... } }; export public APIs
  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
  83. None
  84. Neos Conference Impressions: https://www.youtube.com/watch?v=w3QZwOIiIbo

  85. neoscon.io 10% Rabattcode: Meetup Next Neos Meetup 26.07.