TodoMVC-Cycle.js(WIP)

这次我们来写一个惯例的todomvc

当前状态:暂停--等待上游修复cyclic-router的bug

这次我们来写一个惯例的todomvc

0x00 前期准备

当然基本的node.js环境要求还是有的。没有就自己安装,推荐安装v8以上的lts版本。
其外还有git和我个人偏好的yarn

通过模板项目创建

我们直接使用npx启动create-cycle-app,就不全局安装了,对于这一类工具,推荐使用npx,因为每次使用前都去更新一下,还不如直接npx自动使用最新版来得便利,当然如果你需要锁定版本,那还是全局安装的好。运行命令:

npx create-cycle-app todomvc-demo 

添加其他依赖

因为create-cycle-app使用的npm安装包依赖的,我个人偏好yarn,所以进去把package-lock.json删掉,然后再添加todomvc相关的包。
其外因为依赖中的husky需要git环境,所以如果你在用git,那么还需要进去git init一下,再运行yarn或者npmhuskygit hook起作用。运行命令:

cd todomvc-demo
rm package-lock.json
git init
yarn add todomvc-common todomvc-app-css @cycle/storage #这一步yarn会完成husky的配置

0x01 基础准备

首当其冲的是把.gitignore改一下,避免添加IDE相关的配置(看个人偏好)。

build/
.tmp/
.nyc_output/
node_modules/
coverage/
public/index.html

*.log
# JetBrains IDE configuration
.idea/

接着修改index.ejs:

<!DOCTYPE html>
<html lang="en" data-framework="cycle">
<head>
    <meta charset="utf-8">
    <title>Cycle • TodoMVC</title>
</head>
<body>
    <div id="app" class="todoapp"></div>
    <footer class="info">
        <p>Double-click to edit a todo</p>
    </footer>
</body>
</html>

其中#app是我们程序的挂载点,#app.todoappfooter.info部分是todomvccss
接下来我们修改src/css/style.scc

@import "~todomvc-common/base.css";
@import "~todomvc-app-css/index.css";

html {
    width: 100%;
    height: 100%;
}

span,
button,
textarea {
    margin: 0.5em;
}

0x03 创建根组件

创建目录结构

我个人习惯,components目录是放跨页面共享的组件,routes目录放对应路由下的组件和逻辑,但是这次我们就一个组件所以就不这么折腾了。

  1. app.tsx放到src下,可以改名为app.ts,因为我并不打算使用jsx
  2. components下添加TaskList目录和Task目录。
  3. TaskListTask下分别添加index.tsinterfaces.tsmodel.tsview.tsintent.ts
    最终目录如下:
tree src
src
├── app.ts
├── components
│   ├── Task
│   │   ├── index.ts
│   │   ├── intent.ts
│   │   ├── interfaces.ts
│   │   ├── model.ts
│   │   └── view.ts
│   └── TaskList
│       ├── index.ts
│       ├── intent.ts
│       ├── interfaces.ts
│       ├── model.ts
│       └── view.ts
├── css
│   └── styles.scss
├── drivers
│   └── speech.ts
├── drivers.ts
├── index.ts
└── interfaces.ts

至此,文件结构就差不多了,剩下还有譬如util这一类文件,需要的时候再添加。

修改周边文件

我们要用到loalStorage和捕捉路由跳转,所以改一下drivers.ts

import { makeDOMDriver } from '@cycle/dom';
import { captureClicks, makeHistoryDriver } from '@cycle/history';
import storageDriver from '@cycle/storage';
import { withState } from '@cycle/state';
import { routerify } from 'cyclic-router';
import switchPath from 'switch-path';

import { Component } from './interfaces';
import speechDriver from './drivers/speech';

const driversFactories: any = {
    DOM: () => makeDOMDriver('#app'),
    history: () => captureClicks(makeHistoryDriver()),
    speech: () => speechDriver,
    storage: () => storageDriver
};

export function getDrivers(): any {
    return Object.keys(driversFactories)
        .map(k => ({ [k]: driversFactories[k]() }))
        .reduce((a, c) => ({ ...a, ...c }), {});
}

export const driverNames = Object.keys(driversFactories)
    .filter(name => name !== 'history')
    .concat(['state', 'router']);

export function wrapMain(main: Component<any>): Component<any> {
    return withState(routerify(main as any, switchPath as any)) as any;
}

其外还需要将默认路由指向TaskList,所以再修改一下app.ts

import { Stream } from 'xstream';
import { extractSinks } from 'cyclejs-utils';
import isolate from '@cycle/isolate';

import { driverNames } from './drivers';
import { Component, Sinks, Sources } from './interfaces';

import { TaskList, State as TaskListState } from './components/TaskList';

export interface State {
    taskList?: TaskListState;
}

export function App(sources: Sources<State>): Sinks<State> {
    const match$ = sources.router.define({
        '/': isolate(TaskList, 'taskList'),
    });

    const componentSinks$: Stream<Sinks<State>> = match$
        .filter(({ path, value }: any) => path && typeof value === 'function')
        .map(({ path, value }: { path: string; value: Component<any> }) => {
            return value({
                ...sources,
                router: sources.router.path(path)
            });
        });

    return extractSinks(componentSinks$, driverNames);
}

目前还跑不起来,接下来开始修改应用的入口组件:TaskList

根组件

英文名为 RootComponent,意指一个组件树中根级节点,在react中一般是一个Container/Provider类型组件,然而Cycle.js中不存在这类组件,所以只是一个常规组件。另外,Cycle.js中对于reactHOC也有相关实现,亦即HOF,因为Cycle.js的一个组件就是一个纯函数,譬如isolatewithStaterouterify等就是HOF
现在我们来创建根组件,个人习惯先从view入手:

// TODO