许多人已经知道效应器状态管理器,不仅有人观看了它,还在产品中使用了它。从秋天末开始,它的作者一直在积极地为效果器开发devtools,在此工作过程中,他设法编写了一个非常有趣的库来渲染应用程序-dom-dom。
我们将了解此渲染-在本教程中,我们将创建一个简单的Todo应用程序。

为了使用逻辑,我们将使用effector来渲染应用程序-effector-dom。
对于视觉部分,让我们以现成的todomvc-app-template模板为模板,其模板以astejs为基础的todomvc-app-css样式。
1.项目准备
我假设您已经熟悉webpack和npm,因此我们跳过安装npm,使用webpack创建项目并启动应用程序的步骤(如果我们不熟悉,请使用google webpack样板)。
使用撰写本文时使用的版本安装必要的软件包:
npm install effector@20.11.5 effector-dom@0.0.10 todomvc-app-css
2.开始吧
首先,让我们确定应用程序的结构。
, , , - best practices .
, .
:
srs/
  view/
    app.js 
    title.js 
    footer.js 
    header.js 
    main.js 
    todoItem.js 
  model.js 
  index.js 
3.
, : , , .
( undefined) Store, — Event.
combine, .
import {createStore, createEvent, combine} from 'effector';
export const $todos = createStore([]); 
export const $activeFilter = createStore(null); 
export const $filteredTodos = combine(
  $todos,
  $activeFilter,
  (todos, filter) => filter === null
    ? todos
    : todos.filter(todo => todo.completed === filter)
);
export const appended = createEvent(); 
export const toggled = createEvent();
export const removed = createEvent();
export const allCompleted = createEvent();
export const completedRemoved = createEvent();
export const filtered = createEvent();
.
, store.on
...
$todos
  
  .on(appended, (state, title) => [...state, {title, completed: false}])
  
  .on(removed, (state, title) => state.filter(item => item.title !== title)) 
  
  .on(toggled, (state, title) => state.map(item => item.title === title 
    ? ({...item, completed: !item.completed})
    : item))
  
  .on(allCompleted, state => state.map(item => item.completed
    ? item
    : ({...item, completed: true})))
  
  .on(completedRemoved, state => state.filter(item => !item.completed));
$activeFilter
  
  .on(filtered, (_, filter) => filter);
. .
4. view
view effector-dom. , SwiftUI .
, , . dom- .
effector-dom dom , . using, dom- , :
import {using} from 'effector-dom';
import {App} from './view/app';
using(document.body, () => {
  App();
});
h, dom- , .
dom- spec, :
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {Header} from './header';
import {Main} from './main';
import {Footer} from './footer';
export const App = () => {
  
  h('section', () => {
    
    spec({attr: {class: classes.todoapp}});
    
    Header();
    Main();
    Footer();
  });
};
.
5.
h1 .
dom- , , . spec , :
import {h} from 'effector-dom';
export const Title = () => {
  h('h1', {text: 'todos'});
};
6.
, effector-dom effector , , ..
, header. dom- spec ( ), handler.
, input , $value input.
sample . api sample — , — , : event = sample($store, triggerEvent).
import {h, spec} from 'effector-dom';
import {createEvent, createStore, forward, sample} from 'effector';
import classes from 'todomvc-app-css/index.css';
import {Title} from './title';
import {appended} from '../model';
export const Header = () => {
  h('header', () => {
    Title();
    h('input', () => {
      const keypress = createEvent();
      const input = createEvent();
      
      const submit = keypress.filter({fn: e => e.key === 'Enter'});
      
      const $value = createStore('')
        .on(input, (_, e) => e.target.value)
        .reset(appended); 
      
      forward({ 
        
        
        from: sample($value, submit).filter({fn: Boolean}), 
        to: appended,
      });
      spec({
        attr: {
          class: classes["new-todo"],
          placeholder: 'What needs to be done?',
          value: $value
        },
        handler: {keypress, input},
      })
    });
  });
};
7.
, effector-dom list.
— , , . list($store, itemCallback) .
.
, todomvc-app-css - , . .
import {h, spec, list} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {TodoItem} from './todoItem';
import {$filteredTodos, allCompleted} from '../model';
export const Main = () => {
  h('section', () => {
    spec({attr: {class: classes.main}});
    
    h('input', {
      attr: {id: 'toggle-all', class: classes['toggle-all'], type: 'checkbox'}
    });
    h('label', {attr: {for: 'toggle-all'}, handler: {click: allCompleted}});
    
    h('ul', () => {
      spec({attr: {class: classes["todo-list"]}});
      list({
        source: $filteredTodos,
        key: 'title',
        fields: ['title', 'completed']
        
      }, ({fields: [title, completed], key}) => TodoItem({title, completed, key})); 
    });
  });
};
8.
, effector-dom - , ..
, toggled removed - — .
effector — event.prepend.
store.map
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {toggled, removed} from '../model';
export const TodoItem = ({title, completed, key}) => {
  h('li', () => {
    
    spec({attr: {class: completed.map(flag => flag ? classes.completed : false)}});
    h('div', () => {
      spec({attr: {class: classes.view}});
      h('input', {
        attr: {class: classes.toggle, type: 'checkbox', checked: completed},
        
        handler: {click: toggled.prepend(() => key)},
      });
      h('label', {text: title});
      h('button', {
        attr: {class: classes.destroy},
        
        handler: {click: removed.prepend(() => key)},
      });
    });
  });
};
9.
,
import {h, spec} from 'effector-dom';
import classes from 'todomvc-app-css/index.css';
import {$todos, $activeFilter, filtered, completedRemoved} from '../model';
export const Footer = () => {
  h('footer', () => {
    spec({attr: {class: classes['footer']}});
    h('span', () => { 
      spec({attr: {class: classes['todo-count']}});
      const $activeCount = $todos.map(
        todos => todos.filter(todo => !todo.completed).length
      );
      h('strong', {text: $activeCount});
      h('span', {text: $activeCount.map(count => count === 1
        ? ' item left'
        : ' items left'
      )});
    });
    h('ul', () => { 
      spec({attr: {class: classes.filters}});
      h('li', () => {
        h('a', {
          attr: {class: $activeFilter.map(active => active === null
            ? classes.selected
            : false
          )},
          text: 'All',
          handler: {click: filtered.prepend(() => null)},
        });
      });
      h('li', () => {
        h('a', {
          attr: {class: $activeFilter.map(completed => completed === false
            ? classes.selected
            : false
          )},
          text: 'Active',
          handler: {click: filtered.prepend(() => false)},  
        });
      });
      h('li', () => {
        h('a', {
          attr: {class: $activeFilter.map(completed => completed === true
            ? classes.selected
            : false
          )},
          text: 'Completed',
          handler: {click: filtered.prepend(() => true)},
        });
      });
    });
    h('button', {
      attr: {class: classes['clear-completed']},
      text: 'Clear completed',
      handler: {click: completedRemoved},
    });
  });
};
10.
事实证明,这样一个简单的应用程序是经过特别简化的,尽可能减少了分离成单独实体的可能性。
当然,如果需要,可以将相同的过滤器按钮取出到一个单独的实体中,该实体可以用于传达过滤器的类型和名称。
const FilterButton = ({filter, text}) => {
  h('li', () => {
    h('a', {
      attr: {class: $activeFilter.map(completed => completed === filter
        ? classes.selected
        : false
      )},
      text: text,
      handler: {click: filtered.prepend(() => filter)},  
    });
  });
};
h('ul', () => { 
  spec({attr: {class: classes.filters}});
  FilterButton({filter: null, text: 'All'});
  FilterButton({filter: false, text: 'Active'});
  FilterButton({filter: true, text: 'Completed'});
});
同样,由于其基于堆栈的工作,effector-dom使您不仅可以自由渲染单个元素,还可以自由渲染常规行为。
呈现的代码将专门应用于必要的元素,例如:
const WithFocus = () => {
  const focus = createEvent();
  focus.watch(() => console.log('focused'));
  spec({handler: {focus}});
};
h('input', () => {
  ...
  WithFocus();
  ...
});
11.总计
使用新渲染的简单性给我个人留下了深刻的印象,特别是因为我已经在大型应用程序中使用过效果器,因此我对此印象深刻。
我期待一个稳定的版本,某种形式的普及,以便有机会在产品中使用渲染。