第一个组件

对于目前写好的 BMI 小程序,它的结构还是不合理的,不适用于更大型的 Web App。在 View 和 Intent 中都出现了很多的重复:

function intent(DOMSource) {
  const weightChange$ = DOMSource.select('.weight').events('input').map(evt => evt.target.value);
  const heightChange$ = DOMSource.select('.height').events('input').map(evt => evt.target.value);
  return { weightChange$, heightChange$ };
}

function view(state$) {
  return {
    DOM: state$.map(({ bmi, weight, height }) =>
      div([
        div([
          label(`Weight: ${weight} kg`),
          input('.weight', { type: 'range', min: 40, max: 150, value: weight })
        ]),
        div([
          label(`Height: ${height} cm`),
          input('.height', { type: 'range', min: 140, max: 220, value: height })
        ]),
        h2(`BMI is ${bmi}`)
      ]))
  };
}

实际上,用组件化开发的视角,我们可以抽象一个更加通用的 滑块组件 -- LabeledSlider。首先,让我们忘掉 BMI 小程序,专注于撰写这个滑块组件,在 Cycle.js 的世界里,组件也应当满足 MVI 模式:

  • 在 Model 部分,滑块进行状态管理,返回一个状态流。
  • 在 View 部分,滑块进行内容的渲染,返回一个 DOM 渲染流,即输出 DOM 副作用。
  • 在 Intent 部分,滑块通过 source 读入 DOM 副作用,绑定上 DOM 事件。
function intent(DOMSource) {
  const change$ = DOMSource.select('.slider').events('input').map(evt => evt.target.value);
  return change$;
}

function model(change$) {
  return change$.startWith(70);
}

function view(state$) {
  return state$.map(value =>
      div('.label-slider', [
        label('.label', `Weight: ${weight} kg`),
        input('.slider', { type: 'range', min: 40, max: 150, value })
      ])
    );
}

function main(sources) {
  const change$ = intent(sources.DOM);
  const state$ = model(change$);
  const vtree$ = view(state$);
  return {
    DOM: vtree$
  };
}

const drivers = {
  DOM: makeDOMDriver('#app')
};

Cycle.run(main, drivers);

对于一个组件,它往往还需要接受一些参数作为其属性(property),以便于我们更好的自定义这个组件,我们怎么传入这些参数呢?记住,在 Cycle.js 中,driver 沟通了外部世界和内部逻辑,因此,我们可以新建一个用于属性声明的 driver:

const drivers = {
  DOM: makeDOMDriver('#app'),
  props: () => Rx.Observable.of({
    label: 'Height',
    unit: 'cm',
    min: 140,
    max: 220,
    init: 170
  })
};

然后,就可以在 main() 中,将属性传递给 Model 使用:

function model(newValue$, props$) {
  const initialValue$ = props$.map(props => props.init).first();
  const value$ = initialValue$.concat(newValue$);
  return Rx.Observable.combineLatest(value$, props$, (value, props) => {
    return {
      label: props.label,
      unit: props.unit,
      min: props.min,
      max: props.max,
      value
    };
  });
}

function view(state$) {
  return state$.map(state =>
      div('.label-slider', [
        label('.label', `${state.label}: ${state.value} ${state.unit}`),
        input('.slider', { type: 'range', min: state.min, max: state.max, value: state.value })
      ])
    );
}

function main(sources) {
  const newValue$ = intent(sources.DOM);
  const state$ = model(newValue$, sources.props);
  const vtree$ = view(state$);
  return {
    DOM: $vtree
  };
}

查看示例

results matching ""

    No results matching ""