
Bonjour à tous! Je m'appelle Arthur, je travaille comme développeur frontend chez Exness. Il n'y a pas si longtemps, nous avons transféré l'un de nos projets vers des composants Web. Je vais vous dire quels problÚmes nous avons dû gérer et combien de concepts auxquels nous sommes habitués lorsque nous travaillons avec des frameworks sont facilement transférés aux composants Web.
Pour l'avenir, je dirai que le projet mis en Ćuvre a rĂ©ussi les tests sur notre large public, et que la taille du bundle et le temps de chargement ont Ă©tĂ© considĂ©rablement rĂ©duits.
Je suppose que vous avez dĂ©jĂ une comprĂ©hension de base de la technologie, mais mĂȘme sans elle, il sera clair qu'il est trĂšs pratique de travailler avec des composants Web et d'organiser l'architecture du projet.
Pourquoi des composants 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