Komponen web dalam proyek nyata


Halo semuanya! Nama saya Arthur, saya bekerja sebagai pengembang frontend di Exness. Belum lama ini, kami mengalihkan salah satu proyek kami ke komponen web. Saya akan memberi tahu Anda masalah apa yang harus kami tangani, dan berapa banyak konsep yang kami gunakan ketika bekerja dengan kerangka kerja mudah ditransfer ke komponen web.


Ke depan, saya akan mengatakan bahwa proyek yang diimplementasikan berhasil melewati pengujian pada khalayak luas kami, dan ukuran bundel dan waktu pemuatan berkurang secara signifikan.


Saya kira Anda sudah memiliki pemahaman dasar tentang teknologi, tetapi tanpa itu akan menjadi jelas bahwa cukup nyaman untuk bekerja dengan komponen web dan mengatur arsitektur proyek.


Mengapa komponen web?


, ; ui- . - , html+js, . , . , Svelte , , Preact. , , .


?


, , - . . , (~2kB), , โ€” , . IE11 Edge. , IE , Edge . UC Browser QQ Browser, .


:


  • <slot></slot> IE11 & Edge ;
  • html-. .

LitElement


ยซ boilerplate- ! , HTMLElement ยป โ€” , -. , , โ€” LitElement (~7kb). lit-html .


, LitElement - , .


LitElement โ€” , - ยซ ยป. 7kB, LitElement API. boilerplate-.


-


, lit-html ~3.5kB . DOM- - : , . , ( ):


import { html, render } from 'lit-html'

const ui = data => html`...${data}...`

render(ui('Hello!'), document.body)

:


const myHeader = html`<h1>Header</h1>`
const myPage = html`
  ${myHeader}
  <div>Here's my main page.</div>
`

:


const defineFxComponent = (tagName, FxComponent, Parent = LitElement) => {
  const Component = class extends Parent {
    render() {
      return FxComponent(this.data)
    }
  }
  customElements.define(tagName, Component)
}

defineFxComponent('custom-ui', ui)

render(html`<custom-ui .data="Hello!"></custom-ui>`, document.body)

, , , , lit-html. . , , .


svg


, ( API) โ€” svg``. , , html`` . , TemplateResult ( html``) โ€” , . svg`` SVGTemplateResult .



โ€” , , . lit-html DOM , Part. Part , , , โ€” .


, Part:


  • (NodePart);
  • (AttributePart);
  • (BooleanAttributePart);
  • (PropertyPart);
  • (EventPart).

.


value โ€” , . setValue(). DOM , , commit() ( ).


( NodePart โ€” ), :


import { directive } from 'lit-html'

const renderCounter = directive(() => part =>
  part.setValue(part.value === undefined ? 0 : part.value + 1)
)

Lit-html . , , , , html .


state . .


. . .


shady-render


lit-html Shadow DOM, . lit-html shady-render, .



HOC โ€” , React Vue. , - -. - , HOC , , .



redux, . , store redux. mapStateToProps ( , HOC, redux), , DOM, redux. DOM .


import { bindActionCreators } from 'redux'

export default store => (mapStateToProps, mapDispatchToProps) => Component =>
  class Connect extends Component {
    constructor(props) {
      super(props)
      this._getPropsFromStore = this._getPropsFromStore.bind(this)
      this._getInheritChainProps = this._getInheritChainProps.bind(this)

      //  mapStateToProps
      this._inheritChainProps = (this._inheritChainProps || []).concat(
        mapStateToProps
      )
    }

    //      store
    _getPropsFromStore(mapStateToProps) {
      if (!mapStateToProps) return
      const state = store.getState()
      const props = mapStateToProps(state)

      for (const prop in props) {
        this[prop] = props[prop]
      }
    }

    // Callback     store,    mapStateToProps   
    _getInheritChainProps() {
      this._inheritChainProps.forEach(i => this._getPropsFromStore(i))
    }

    connectedCallback() {
      this._getPropsFromStore(mapStateToProps)

      this._unsubscriber = store.subscribe(this._getInheritChainProps)

      if (mapDispatchToProps) {
        const dispatchers =
          typeof mapDispatchToProps === 'function'
            ? mapDispatchToProps(store.dispatch)
            : mapDispatchToProps
        for (const dispatcher in dispatchers) {
          typeof mapDispatchToProps === 'function'
            ? (this[dispatcher] = dispatchers[dispatcher])
            : (this[dispatcher] = bindActionCreators(
                dispatchers[dispatcher],
                store.dispatch,
                () => store.getState()
              ))
        }
      }

      super.connectedCallback()
    }

    disconnectedCallback() {
      //     store
      this._unsubscriber()
      super.disconnectedCallback()
    }
  }

store , :


// store.js
import { createStore } from 'redux'
import makeConnect from 'lite-redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store

//   
export const connect = makeConnect(store)

// Component.js
import { connect } from './store'

class Component extends WhatEver {
  /* ... */
}

export default connect(mapStateToProps, mapDispatchToProps)(Component)


. . , : get observedAttributes() - get properties() LitElement. , :


const withPassword = Component =>
  class PasswordInput extends Component {
    static get properties() {
      return {
        //   super.properties    type
        ...super.properties,
        addonIcon: { type: String }
      }
    }

    constructor(props) {
      super(props)
      this.type = 'password'
      this.addonIcon = 'invisible'
    }

    setType(e) {
      this.type = this.type === 'text' ? 'password' : 'text'
      this.addonIcon = this.type === 'password' ? 'invisible' : 'visible'
    }

    render() {
      return html`
        <div class="with-addon">
          <!--    -->
          ${super.render()}
          <div @click=${this.setType}>
            <custom-icon icon=${this.addonIcon}></custom-icon>
          </div>
        </div>
      `
    }
  }

customElements.define('password-input', withPassword(TextInput))

...super.properties get properties(), . super.render() render, .


HOC :


  • , ;
  • , - , - , HOC', ;
  • , HOC.

Shadow DOM


, ui- . , : (values, errors, touched), , submit, reset.


, , . , , , : -, <form> . ...


1. - Shadow DOM


, โ€” - , Shadow DOM , HOC , . .


, <form> HTMLFormElement (, submit Enter), . , , <form>, โ€” <form> .


. render- React:


//  
import { LitElement, html } from 'lit-element'

class LiteForm extends LitElement {
  /* ...  ... */

  render() {
    return html`<form @submit=${this.handleSubmit} method=${this.method}>
      ${this.formTemplate(this)}
    </form>`
  }
}

customElements.define('lite-form', LiteForm)

//  
import { html, render } from 'lit-element'

const formTemplate = ({ values, handleBlur, handleChange, ...props }) =>
  html`<input
      .value=${values.firstName}
      @input=${handleChange}
      @blur=${handleBlur}
    />
    <input
      .value=${values.lastName}
      @input=${handleChange}
      @blur=${handleBlur}
    />
    <button type="submit">Submit</button>`

const MyForm = html`<lite-form
  method="POST"
  .formTemplate=${formTemplate}
  .onSubmit=${{/*...*/}}
  .initialValues=${{/*...*/}}
  .validationSchema=${{/*...*/}}
></lite-form>`

render(html`${MyForm}`, document.getElementById('root'))

, . . , withField withError.


, HOC' . ( , โ€” , ), , :


//   IS_LITE_FORM โ€”    ,      
const getFormClass = element => {
  const form = element.closest(`[${IS_LITE_FORM}]`)
  if (form) return form

  const host = element.getRootNode().host
  if (!host) throw new Error('Lite-form not found')
  return host[IS_LITE_FORM] ? host : getFormClass(host)
}

: , . getRootNode, Shadow DOM โ€” .


withField :


const formTemplate = props =>
  html`<custom-input name="firstName"></custom-input>
    <custom-input name="lastName"></custom-input>
    <button type="submit">Submit</button>`

, , โ€ฆ , Shadow DOM, .


Shadow DOM :host :host-context


CSS . Shadow DOM, , . , , , :


:host([rtl]) {
  text-align: right;
}

:host-context(body[dir='rtl']) .text {
  text-align: right;
}

:host , . , rtl.


:host-context , Shadow DOM, . .text dir, body.


Shadow DOM


Shadow DOM : target, -. DOM, , target , .


Shadow DOM composed. Shadow DOM, composed bubbles true. Shadow DOM.


Shadow DOM


-, - (-, , ) Shadow DOM. . ( , ) .


, , , , Shadow DOM . , , , Shadow DOM. , Shadow DOM.


-, - querySelector, Shadow DOM. , , Google Tag Manager document.querySelector(...) .shadowRoot.querySelector(...) .shadowRoot.querySelector(...) .shadowRoot.querySelector(...)


Shadow DOM . LitElement :


createRenderRoot() {
  return this
}

Shadow DOM blur . withField, . , .


2.


Shadow DOM HTMLFormElement <form> โ€” , , :


//  
class LiteForm extends HTMLFormElement {
  connectedCallback() {
    this.addEventListener('submit', this.handleSubmit)
  }

  disconnectedCallback() {
    this.removeEventListener('submit', this.handleSubmit)
  }

  /* ...  ... */
}

customElements.define('lite-form', LiteForm, { extends: 'form' })

, :


//  
const MyForm = html`<form
  method="POST"
  is="lite-form"
  .onSubmit=${{...}}
  .initialValues=${{...}}
  .validationSchema=${{...}}
>
  <custom-input name="firstName"></custom-input>
  <custom-input name="lastName"></custom-input>
  <button type="submit">Submit</button>
</form>`

render(html`${MyForm}`, document.getElementById('root'))

customElements.define is , , ยซยป . customElements.define, , .


. : Safari , . iOS ( Chrome, Firefox .). , Apple , 13 Safari iOS . , , Safari iOS .


3.


, , . :


//    
export const withForm = ({
  onSubmit,
  initialValues,
  validationSchema,
  ...config
} = {}) => Component =>
  class LiteForm extends Component {
    connectedCallback() {
      this._onSubmit = (onSubmit || this.onSubmit || function () {}).bind(this)
      this._initialValues = initialValues || this.initialValues || {}
      this._validationSchema = validationSchema || this.validationSchema || {}
      /* ... */
      super.connectedCallback && super.connectedCallback()
    }

    /* ...  ... */
  }

connectedCallback() (onSubmit, initialValues, validationSchema, .) , withForm(), . , , , . , :


//       :
//  -  LitElement
import { withForm } from 'lite-form'

class LiteForm extends LitElement {
  render() {
    return html`<form @submit=${this.handleSubmit} method=${this.method}>
      ${this.formRender(this)}
    </form>`
  }
}

customElements.define('lite-form', withForm(LiteForm))

//       :
//   
import { withForm } from 'lite-form'

class LiteForm extends HTMLFormElement {
  connectedCallback() {
    this.addEventListener('submit', this.handleSubmit)
  }

  disconnectedCallback() {
    this.removeEventListener('submit', this.handleSubmit)
  }
}

customElements.define('lite-form', withForm(LiteForm), { extends: 'form' })

, , a withForm() , , HOC:


//  
import { withForm } from 'lite-form'

class UserForm extends LitElement {
  render() {
    return html`
      <form method="POST" @submit=${this.handleSubmit}>
        <custom-input name="firstName"></custom-input>
        <custom-input name="lastName"></custom-input>
        <button type="submit">Submit</button>
      </form>
    `
  }
}

const enhance = withForm({
  initialValues: {/*...*/},
  onSubmit: {/*...*/},
  validationSchema: {/*...*/}
})

customElements.define('user-form', enhance(UserForm))

Shadow DOM, . .



ยซ-ยป ยซ ยป โ€” API, , . , , , . LitElement , โ€” .


, Shadow DOM ( ). .


Shadow DOM , . Shadow DOM . CSS , DOM .


- , , , . , , .



webcomponents.org, Polymer, LitElement, lit-html, Vaadin Router, Vaadin Components, lite-redux, lite-form, awesome-lit-html, Polyfills, custom-elements-builtin polyfill


All Articles