第一个组件
对于目前写好的 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
};
}