
¡Hola a todos! Mi nombre es Arthur, trabajo como desarrollador frontend en Exness. No hace mucho tiempo, cambiamos uno de nuestros proyectos a componentes web. Te diré con qué problemas tuvimos que lidiar, y cuántos de los conceptos a los que estamos acostumbrados cuando trabajamos con frameworks se transfieren fácilmente a los componentes web.
Mirando hacia el futuro, diré que el proyecto implementado pasó con éxito las pruebas en nuestra amplia audiencia, y el tamaño del paquete y el tiempo de carga se redujeron significativamente.
Supongo que ya tiene una comprensiĂłn básica de la tecnologĂa, pero incluso sin ella quedará claro que es bastante conveniente trabajar con componentes web y organizar la arquitectura del proyecto.
¿Por qué componentes 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)
this._inheritChainProps = (this._inheritChainProps || []).concat(
mapStateToProps
)
}
_getPropsFromStore(mapStateToProps) {
if (!mapStateToProps) return
const state = store.getState()
const props = mapStateToProps(state)
for (const prop in props) {
this[prop] = props[prop]
}
}
_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() {
this._unsubscriber()
super.disconnectedCallback()
}
}
store , :
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)
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,
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' . ( , — , ), , :
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()
, . , , , . , :
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