Comments 27
Посмотрите WPF с его точками расширения/слотами/темплейтами и DataContext'ами.
Вот там можно переопределить всё и даже довольно удобно.
Я имею ввиду посмотреть саму идею расширяемости wpf; понятно, что это не перетащишь в веб просто так (хотя попытки были http://bridge.net/ http://www.cshtml5.com )
У меня просто стойкое ощущение, что всё это было там уже довольно давно.
Сложно читать статью из-за нестандартного синтаксиса JavaScript. Какие babel-transform нужно использовать для того, чтобы преобразовать код из примеров в ES6+JSX?
Примеры 3, 10, 11 и 14. Ничего из перечисленного не встречал даже в будущих версиях стандарта ECMAScript:
- Конструкция
@mem
- Объявление параметров объекта в теле класса
- Типы аргументов функций
- mem — декоратор, если имеется в виду реализация, то это отдельная библиотека, о ней я планировал в другой статье рассказать.
- Параметры: github.com/tc39/proposal-class-fields
- Типы не стандарт ES, но поверх наслаивается: flowtype, typescript.
DI удобная вещь, но если используется redux подход используемый в redux-form мне кажется более логичным. Идея в том что Container, обернутый в connect, вместе со стилями css-in-js, prop-types и селекторами оборачивается еще одним HOC вида createMyAwesomeComponent. В итоге получается функция-конструктор, формирующая с помощью параметров произвольное поведение самого Container и его дочерних Presentationals.
В такой конструктор можно на вход подать кастомные селекторы, которые прогонят через требуемую логику данные из store. Можно в виде параметров передавать мелкие кастомизированные компоненты, чтобы не перегружать сигнатуру конструктора. В store для каждого инстанса Container создается свой state, и соответственно в экшенах в разделе meta указывается имя инстанса.
Если нужна кастомизация side effects, например тулбар с другими кнопками, то проще для тулбара создать отдельный Container и передать его в качестве параметра в основной Container.
Redux немного другие задачи решает, навязывает определенный стиль программирования (экшены, редьюсеры и т.д.) не избавляет от HOC и этого connect в коде. Если нужны транзакции, версионирование и т.д., то решения вроде redux можно накрутить и поверх DI. Например, mobx-state-tree — это как раз пример того, как выглядел бы redux поверх mobx.
Redux предназначен не для хранения состояния компонентов. Redux это слой между хранилищем данных и react. Плюс он играет роль менеджера состояния ПРИЛОЖЕНИЯ. Но нужно помнить, что определять в нем состояние приложения стоит только в том случае, если эта логика таковой является.
Без lifecycle очень плохо. наоборот, чем больше lifecycle тем было бы круче. представьте что помимо constructor в js был бы ещё destructor, update, write, read. Жизнь стала бы сказкой.
Я тоже пробовал внедрить DI в react и так как пишу на typescript, выбран был InversifyJS.
Затем использовал mobx. Затем перешел на angular 4 и просто нарадоваться не могу. Все о чем можно мечтать уже сделано…
В TypeScript строковые литералы не считаются нетипобезопасными, так как есть строковые литеральные типы.В ts да, такая штука будет типобезопасной:
import { InjectionToken } from '@angular/core';
interface AppConfig {
title: string;
}
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
//...
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
Однако такой подход все-равно требует расстановки декораторов, это не нативно из-за InjectionToken. Второй момент — заранее надо продумывать точки расширения, расставляя эти декораторы.
Redux предназначен не для хранения состояния компонентов.Redux лучше впишется туда, где используются транзакции, timetravel, распределенная работа с данными. Если эти навороты не нужны, то есть более простые способы менять состояние, поэтому многие перешли на mobx.
Задачу связывания redux решает не очень хорошо: redux-thunk, reselect, connect — низкоуровневые инструменты. Если основа DI, можно все связывание построить на типах, не привнося фреймворко-специфичных конструкций.
Сравните примеры Hello world, кода сильно меньше при сравнимом уровне абстракции:
import { connect } from 'react-redux'
import React, { PropTypes } from 'react'
const HELLO_WORLD = 'HELLO_WORLD'
const helloWorld = (state = { message: 'Hello' }, action) => {
switch (action.type) {
case HELLO_WORLD:
return Object.assign({}, state, { message: 'Hello, World!' })
default:
return state
}
}
const Hello = ({ onClick, message }) => {
return (
<div>
<h1>{ message }</h1>
<button onClick={onClick}>Click</button>
</div>
)
}
const mapStateToProps = (state, ownProps) => {
return {
message: state.helloWorld.message
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch({ type: HELLO_WORLD })
}
}
}
const HelloWorld = connect(
mapStateToProps,
mapDispatchToProps
)(Hello)
import {mem} from 'lom_atom'
class HelloWorldStore {
@mem message = 'Hello'
onClick() {
this.message = 'Hello, World!'
}
}
function HelloWord(_: {}, store: HelloWorldStore) {
return (
<div>
<h1>{ store.message }</h1>
<button onClick={() => store.onClick()}>Click</button>
</div>
)
}
Через алиасинг HelloWorldStore также можно разделить компонент и реакцию на событие.
Без lifecycle очень плохо.Тут бы глянуть конкретные примеры. Есть мнение, что если правильно построить работу с состоянием, большинство lifecycle не нужными становятся. Я об этом хотел написать в следующей статье.
Например, hello world с загрузкой, обработкой ошибок и крутилкой:
function fetchName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// reject(new Error('Fetch error'))
resolve({name: 'John'})
}, 500)
})
}
interface HelloModel {
name: string;
}
class HelloService {
@mem set hello(next: HelloModel | Error) {}
@mem get hello(): HelloModel {
fetchName()
.then((model) => { this.hello = model })
.catch((error) => { this.hello = error })
throw new mem.Wait()
}
}
function HelloView(props: {greet: string}, service: HelloService) {
return <div>
{props.greet}, {service.hello.name}
<br/><input value={service.hello.name} onChange={(e) => { service.hello = {...service.hello, name: e.target.value} }} />
</div>
}
ReactDOM.render(<HelloView greet="Hello"/>, document.getElementById('mount'))
import {Component} from '@angular/core';
function fetchName() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// reject(new Error('Fetch error'))
resolve({name: 'John'})
}, 500)
})
}
interface HelloModel {
name: string;
}
export class Hello implements HelloModel {
name = 'Vasian'
}
@Component({
selector: 'my-app',
template: `
<div>
<div *ngIf="error; else elseBlock">
{{error.message}}
</div>
<ng-template #elseBlock>
{{greet}}, {{hello.name}}
<br/><input [(ngModel)]="hello.name">
</ng-template>
</div>
`
})
export class AppComponent {
greet = 'Hello';
error: any = '';
hello: Hello = new Hello()
ngOnInit() {
fetchName()
.then(hello => this.hello = hello)
.catch(error => this.error = error)
}
}
Как минимум, загрузку и ошибки в Angular придется обрабатывать вручную. Красивый биндинг данных работает внутри компонента, если нужна декомпозиция Hello (например расшарить между двумя компонентами, ничего не знающими друг о друге), то уже надо тащить стримы, подписываться/отписываться.
Что бы использовать сервис, надо объявить его в provide и помечать Injectable. А typescript работает внутри Angular template, как в JSX?
Объем кода тоже можно отметить, Angular4 core+common+forms = 13459 SLOC, rdi+lom_atom+preact, на котором мои примеры, около 2048 SLOC.
Тут конечно простые примеры рассмотрены, если подкинете сложные, я их запилю на rdi.
Очень тяжело читать статью.
Я вот с react-redux регулярно работаю и имею представление о SOLID-принципах и сложностях проектирования, но почти ничего не понял.
Какие-то отдельные часть — возможно, но сути я не уловил.
Как автор предлагает уменьшать сложность за счёт использования DI — ускользнуло от меня.
Суть такая примерно:
- Развить идею контекста в реакте
- Привнести SOLID на фронтенд наименьшей ценой
- Дать возможность писать код, максимально свободный от каркаса, убрав vendor lock-in
- Использовать типы и DI для связывания частей, вместо решений вроде connect, redux-thunk, reselect и т.д.
Сложность уменьшается за счет простых базовых концепций, которые позволяют писать меньше кода, оставляя его понятным (хочется верить).
Спасибо за разъяснение, кажется, понял.
Постараюсь более вдумчиво перечитать.
Думаю тут не хватает наглядного примера с картинками. Например — мини-профиль пользователя на хабре. Он появляется в 3 разных местах: при наведении на аватарку, под статьёй пользователя, на странице профиля пользователя. Каждый мини-профиль позволяет выполнить определённые действия: изменить карму, подписаться, написать сообщение. Но в зависимости от расположения имеет свои особенности: при наведении на аватар добавляется панелька со статистикой, раскладка элементов разная. Но это текущее состояние, после множества рефакторингов. А если откатиться к самому началу, когда этой кучи требований ещё не было, то как запроектировать "мини-профиль пользователя", чтобы его можно было переиспользовать в разных (пока не известных) местах не переписывая постоянно и не утопая в копипасте?
На мой взгляд компонент должен быть самодостаточным и вставляться одной строчкой кода. Но если надо добавить ему панельку в одном конкретном месте использования — должна быть возможность эту самую панельку добавить, не изменяя все места использования компонента, а желательно и самого компонента.
Пока не представляю как в jsx это также дешево сделать. Можно выделять детали на первом уровне в компоненты и делая компонент, который выстроит последовательность из этих деталей.
Ну, разместить базовом компоненте плейсхолдер, который по умолчанию ничего не выводит, а через DI заменять его на что-то своё — вполне себе решение.
Полностью перенести этот принцип на JSX невозможно,
Почему же невозможно — можно воспользоваться динамическими пропсами
class Component extends React.Component {
render(){
return (
<div class="..." {...this.props.rootProps}>
{this.props.rootChildrenBefore}
{this.props.rootChild1 || <div class="..." {...this.props.rootChild1Props}>
{this.props.rootChild1ChildrenBefore}
{this.props.rootChild1Child1 || <div .... <div/> }
{this.props.rootChild1Child2 || <div .... <div/> }
{...}
{this.props.rootChild1ChildrenAfter}
</div>}
{this.props.rootChild2 || <div ... <div/>}
{...}
{this.props.rootChildrenAfter}
</div>
)
}
}
Легко видеть что таким образом можно описать любую вложенность (включая добавление перед и после имеющимся контентом) и потом можно переопределить любой аспект компонента также гибко как и в $mol.
const ComponentWithSomeChanges = ({...props})=> (
<Component
rootChild1Child1={
<div>...</div>
}
{...props}
/>
)
Да, выглядит это не так лаконично — вместо xml иерархии получаем иерархию разметки в пропсах и нужно печатать чуть больше символов но в отличие от подхода с наследованием когда в методы выносится каждый кусочек разметки или пропсов и получается плоский список методов вместо дерева
class MyPanel extends React.Component {
header() { return <div class="my-panel-header">{this.props.head}</div> }
bodier() { return <div class="my-panel-bodier">{this.props.body}</div> }
childs() { return [ this.header() , this.bodier() ] }
render() { return <div class="my-panel">{this.childs()}</div>
}
здесь же сохраняется древовидная структура шаблона.
DI для полностью переиспользуемых JSX-компонентов