RactiveJS + Redux 调度操作和水合物

RactiveJS + Redux dispatch actions and hydrate

我正在尝试使用 Redux 为小型示例应用程序设置 RactiveJS - 初始化仪表板(来自 AJAX),来自仪表板的 add/remove 元素(小部件)(并将序列化数据保存在服务器上)。因为有几乎专门针对 React 的教程,所以我需要建议。我遵循了一些并得到了如下目录结构:

views
    app.html
    dashboard.html
    widget.html
js
    actions
        DashboardActions.js
    components
        Dashboard.js
        Widget.js
    constants
        ActionTypes.js
    reducers
        dashboard.js
        index.js
    app.js
index.html

这个例子有效,但有几个问题,我想弄清楚如何让它变得更好。例如:

1) 如何将 store 和 actions 传递(我应该传递?)到 Ractive 组件树?现在它在每个组件中使用 bindActionCreators,我认为这不是好的解决方案。

2) 服务器的初始状态水合作用放在哪里?现在它在 reducers/dashboard.js 中被硬编码,但我想使用后端作为数据源和数据保存端点。有中间件方法,但如果这是好的做法,那么如何将其应用于 RactiveJs?

3) 我应该用大的 reducer 还是每个组件一个 reducer?

4)也许核心概念不正确,应该重构?

views/app.html

<Dashboard dashboard={{store.getState()}} store="{{store}}"></Dashboard>

views/dashboard.html

{{#with dashboard}}
<pre>
====
<a on-click="@this.addWidget('Added by click')" href="#">Add New</a>
{{#dashboard}}
    {{#each widgets}}
    <Widget id="{{this.id}}" name="{{this.name}}" size="{{this.size}}" actions="{{actions}}" store="{{store}}"></Widget>
    {{/each}}
{{/dashboard}}
====
</pre>
{{/with}}

views/widget.html

<div>{{id}}-{{name}} (Size: {{size}})<a href="#" on-click="@this.deleteWidget(id)">X</a></div>

actions/DashboardActions.js

import * as types from '../constants/ActionTypes';

// Add widget to dashboard
export function addWidget(name) {
    return {
        type: types.ADD_WIDGET,
        name
    };
}

// Delete widget from dashboard
export function deleteWidget(id) {
    return {
        type: types.DELETE_WIDGET,
        id
    };
}

components/Dashboard.js

import Ractive from 'ractive'
import * as DashboardActions from '../actions/DashboardActions';
import { dispatch, bindActionCreators } from 'redux'
import Widget from './Widget'
import template from '../../views/dashboard.html';

export default Ractive.extend({
    isolated: true,
    components: {
        Widget
    },

    oninit() {
        const store = this.get("store");
        const actions = bindActionCreators(DashboardActions, store.dispatch);

        this.set("actions", actions);
    },

    addWidget(name) {
        this.get("actions").addWidget(name);
    },

    template: template
});

components/Widget.js

import Ractive from 'ractive'
import * as DashboardActions from '../actions/DashboardActions';
import { dispatch, bindActionCreators } from 'redux'
import template from '../../views/widget.html';


export default Ractive.extend({
    isolated: true,
    template: template,
    oninit() {
        console.log(this.get("actions"));
        const store = this.get("store");
        const actions = bindActionCreators(DashboardActions, store.dispatch);

        this.set("actions", actions);
   },

    deleteWidget(id) {
       this.get("actions").deleteWidget(id);
    },
})

constants/ActionTypes.js

// Add widget to dashboard
export const ADD_WIDGET = 'ADD_WIDGET';
// Delete widget from dashboard
export const DELETE_WIDGET = 'DELETE_WIDGET';

reducers/dashboard.js

import * as types from '../constants/ActionTypes';

const initialState = {
    widgets: [
        {id: 1, name: "First widget"},
        {id: 2, name: "Second widget"},
        {id: 3, name: "Third widget"},
    ],
};

export default function dashboard(state = initialState, action) {
    switch (action.type) {
        case types.ADD_WIDGET:
            const newId = state.widgets.length + 1;
            const addedWidgets = [].concat(state.widgets, {
                id: newId,
                name: action.name
            });

            return {
                widgets: addedWidgets
            }

        case types.DELETE_WIDGET:
            const newWidgets = state.widgets.filter(function(obj) {
                return obj.id != action.id
            });

            return {
                widgets: newWidgets
            }

        default:
            return state;
    }
}

reducers/index.js

export { default as dashboard } from './dashboard';

app.js

import Ractive from 'ractive';
import template from '../views/app.html';
import Dashboard from './components/Dashboard.js'
import { createStore, combineReducers, bindActionCreators } from 'redux'
import * as reducers from './reducers'

const reducer = combineReducers(reducers);
const store = createStore(reducer);

let App = new Ractive({
    el: '#app',
    template: template,
    components: {
        Dashboard
    },
    data: {
        store
    }
});

store.subscribe(() => App.update());

export default App;

谢谢!

Ractive 不强加任何关于如何完成的约定。然而,Ractive 的设计类似于其他框架(生命周期钩子、方法等)。所以在其他框架上适合你的东西也应该在 Ractive 中工作。

How to pass (and should I pass?) store and actions down to Ractive component tree? At now it uses bindActionCreators in each component and I think this is not good solution.

Maybe the core concept is incorrect and should be refactored?

我很确定您对是将存储和操作直接分配给组件还是通过祖先将它们传递下去感到困惑。答案是……两者都有。 Redux 的作者其实splits components into 2 kinds:presentational and containers.

简而言之,容器组件保存状态并调用操作。展示组件是无状态的,并从祖先组件接收内容。

假设您有一个显示温度和状况的天气小部件。您将有 3 个组件,小部件组件本身、温度和条件。温度和条件组件都是展示性的。天气组件将成为抓取数据的容器,将数据交给两个组件,并将 UI 交互转换为动作。

Weather.js

// Assume the store is in store.js with actions already registered
import store from './path/to/store';
import Temperature from './path/to/Temperature';
import Conditions from './path/to/Conditions';

export default Ractive.extend({
  components: { Temperature, Conditions },
  template: `
    <div class="weather">
      <!-- pass in state data to presentational components -->
      <!-- call methods when events happen from components -->
      <Temperature value="{{ temperature }}" on-refresh="refreshTemp()" />
      <Conditions value="{{ conditions }}" on-refresh="refreshCond()" />
    </div>
  `,
  data: {
    temperature: null,
    conditions: null
  },
  oninit(){
    store.subscribe(() => {
      // Grab state and set it to component's local state
      // Assume the state is an object with temperature and
      // conditions properties.
      const { temperature, conditions } = store.getState();
      this.set({ temperature, conditions });
    });
  },
  // Call actions
  refreshTemp(){
    store.dispatch({ type: 'TEMPERATURE_REFRESH' }); 
  },
  refreshCond(){
    store.dispatch({ type: 'CONDITIONS_REFRESH' }); 
  }
});

Temperature.js

// This component is presentational. It is not aware of Redux 
// constructs at all. It only knows that it accepts a value and
// should fire refresh.

export default Ractive.extend({
  template:`
    <div class="temperature">
      <span>The temperature is {{ value }}</span>
      <button type="button" on-click="refresh">Refresh</button>
    </div>
  `
});

Conditions.js

// This component is presentational. It is not aware of Redux 
// constructs at all. It only knows that it accepts a value and
// should fire refresh.

export default Ractive.extend({
  template:`
    <div class="conditions">
      <img src="http://localhost/condition-images/{{ value }}.jpg">
      <button type="button" on-click="refresh">Refresh</button>
    </div>
  `
});

Where to put initial state hydration from server?

如果我没记错的话,我看到的一个同构工作流涉及将服务器提供的状态放在一个精心命名的全局变量中。在应用程序启动时,应用程序会获取该全局中的数据并将其提供给商店。 Ractive 不参与此过程。

这将由您的服务器打印在页面上:

<script>
window.__APP_INITIAL_STATE__ = {...};
</script>

然后当您启动应用程序时,您将使用该初始状态创建一个商店:

import { createStore } from 'redux'
import reducers from './reducers'
let store = createStore(reducers, window.__APP_INITIAL_STATE__);

Should I use one big reducer or by each component one reducer?

Redux 在 how to split up reducers as well as how to normalize state shape 上有很好的指南。一般来说,状态形状不是由组件定义的,而是由功能定义的。

对于那些仍在寻找将 ractive 和 redux 结合在一起的方法的人,名为 ractive-rematch 的包可以帮助您将 redux 与 ractive 集成。 rematch 是一个非常好用的 redux 框架,有了它我们可以更轻松地进行 redux 工作。

npm install --save @rematch/core ractive-rematch
import { connectInstance } from "ractive-rematch";
import Ractive from "ractive";

var instance = Ractive({el:element,template:`
<button on-click="@.updateName('newName')">click</button>
`})

var mapStateToData = state => ({
  userName:state.user.userName
})

var mapDispatchToMethods = dispatch => ({
  updateName(userName){
    dispatch.user.updateUserName(userName)
  }
})

epxort default connectInstance(mapStateToData,mapDispatchToMethods)(instance)