vstore 是用于 vecty 框架的类似 redux 的状态管理库。

安装

go get marwan.io/vstore

代码仓库: https://github.com/marwan-at-work/vstore

定义 action

一般使用结构定义,比如

type Increment struct{}

type AddTodo struct {
	Id int
	Text string
}

定义 state 和 reducer

比如 todo app 的 state 定义

type State struct {
	Todos  []*Todo
	Filter Filter
}

要实现 vstore.Reducer 接口,比如

func (s *State) Reduce(action interface{}) {
	switch a := action.(type) {
	case *AddTodo:
		s.Todos = append(s.Todos, &Todo{
			Id:        a.Id,
			Completed: false,
			Text:      a.Text,
		})
	case *SetVisibilityFilter:
		s.Filter = a.Filter
	case *ToggleTodo:
		println("reduce toggle todo:", a.Id)
		for _, todo := range s.Todos {
			if todo.Id == a.Id {
				todo.Completed = !todo.Completed
			}
		}
	}
}

使用 switch action.(type) {} 的控制结构来区别处理不同 action。

vstore 的 Reduce 方法不同于 redux 中的 reduce 函数定义 (previousState, action) => newState, vstore 的 Reduce 方法要直接修改 state,而不是创建新的 state。

定义组件

组件需要实现 vstore.StoreComponent 接口,即提供 Connect 方法,一般在 Connect 方法中获取新的 state,然后修改自身的属性的值。

在组件的上一级组件的 Render 方法中,要调用 store.Connect 方法包装该组件。

比如

type Comp1 struct {
	v.Core
	store vstore.Store
}

func (*c Comp1) Render() {
	return elem.Div(
		c.store.Connect(&Comp2{}),
	)
}

type Comp2 struct {
	v.Core
	Text string
}

func (c *Comp2) Connect(store vstore.Store) {
	state := store.State().(*State)
	c.Text = state.Text
}

func (*c Comp2) Render() {
	return elem.Paragraph(
		v.Text(c.Text),
	)
}

完整例子

package main

import (
	"strings"

	v "github.com/gopherjs/vecty"
	"github.com/gopherjs/vecty/elem"
	"github.com/gopherjs/vecty/event"
	"github.com/gopherjs/vecty/prop"
	"marwan.io/vstore"
)

func main() {
	state := &State{}
	state.Filter = FilterAll
	nextTodoId = 2
	state.Todos = []*Todo{
		{
			Id:   1,
			Text: "todo 1",
		},
		{
			Id:   2,
			Text: "todo 2",
		},
	}
	store := vstore.New(state)
	b := &body{
		store: store,
	}
	v.RenderBody(b)
}

// actions

type AddTodo struct {
	Text string
	Id   int
}

var nextTodoId = 0

func addTodo(text string) *AddTodo {
	nextTodoId++
	return &AddTodo{
		Id:   nextTodoId,
		Text: text,
	}
}

type Filter int

const (
	FilterAll = iota
	FilterCompleted
	FilterActive
)

type SetVisibilityFilter struct {
	Filter Filter
}

type ToggleTodo struct {
	Id int
}

type State struct {
	Todos  []*Todo
	Filter Filter
}

type Todo struct {
	Id        int // ???
	Completed bool
	Text      string
}

func (s *State) Reduce(action interface{}) {
	switch a := action.(type) {
	case *AddTodo:
		s.Todos = append(s.Todos, &Todo{
			Id:        a.Id,
			Completed: false,
			Text:      a.Text,
		})
	case *SetVisibilityFilter:
		s.Filter = a.Filter
	case *ToggleTodo:
		println("reduce toggle todo:", a.Id)
		for _, todo := range s.Todos {
			if todo.Id == a.Id {
				todo.Completed = !todo.Completed
			}
		}
	}
}

type TodoView struct {
	v.Core
	Id        int
	Completed bool   `vecty:"prop"`
	Text      string `vecty:"prop"`
	OnClick   func(e *v.Event)
}

func (tv *TodoView) Key() interface{} {
	return tv.Id
}

func (tv *TodoView) Render() v.ComponentOrHTML {
	textDecoration := "none"
	if tv.Completed {
		textDecoration = "line-through"
	}
	return elem.ListItem(
		v.Markup(
			event.Click(tv.OnClick),
			v.Style("text-decoration", textDecoration),
		),
		v.Text(tv.Text),
	)
}

type TodoListView struct {
	v.Core
	TodoList []*Todo `vecty:"prop"`
	store    vstore.Store
}

func (tlv *TodoListView) Connect(store vstore.Store) {
	tlv.store = store
	state := store.State().(*State)

	var todoList []*Todo
	for _, todo := range state.Todos {
		add := false
		switch state.Filter {
		case FilterActive:
			if !todo.Completed {
				add = true
			}

		case FilterCompleted:
			if todo.Completed {
				add = true
			}

		case FilterAll:
			add = true
		}

		if add {
			todoList = append(todoList, todo)
		}
	}
	tlv.TodoList = todoList
}

func (tlv *TodoListView) Render() v.ComponentOrHTML {
	var todoList v.List
	for _, todo := range tlv.TodoList {
		id := todo.Id
		todoList = append(todoList, &TodoView{
			Id:        todo.Id,
			Completed: todo.Completed,
			Text:      todo.Text,
			OnClick: func(e *v.Event) {
				println("on todo click", id)
				tlv.store.Dispatch(&ToggleTodo{id})
			},
		})
	}

	return elem.UnorderedList(
		todoList,
	)
}

type Link struct {
	v.Core
	Active bool `vecty:"prop"`
	store  vstore.Store
	Text   string `vecty:"prop"`
	Filter Filter
}

func (l *Link) Connect(store vstore.Store) {
	l.store = store
	state := store.State().(*State)
	l.Active = state.Filter == l.Filter
}

func (l *Link) Render() v.ComponentOrHTML {
	if l.Active {
		return elem.Span(v.Text(l.Text))
	}

	return elem.Anchor(
		v.Markup(
			prop.Href(""),
			event.Click(func(e *v.Event) {
				l.store.Dispatch(&SetVisibilityFilter{l.Filter})
			}).PreventDefault(),
		),
		v.Text(l.Text),
	)
}

type Footer struct {
	v.Core
	store vstore.Store
}

func (f *Footer) Render() v.ComponentOrHTML {
	return elem.Paragraph(
		v.Text("Show:"),

		f.store.Connect(&Link{
			Filter: FilterAll,
			Text:   "All",
		}),

		f.store.Connect(&Link{
			Filter: FilterActive,
			Text:   "Active",
		}),

		f.store.Connect(&Link{
			Filter: FilterCompleted,
			Text:   "Completed",
		}),
	)
}

type AddTodoView struct {
	v.Core
	store vstore.Store
}

func (atv *AddTodoView) Render() v.ComponentOrHTML {
	input := elem.Input()
	return elem.Div(
		elem.Form(
			v.Markup(
				event.Submit(func(e *v.Event) {
				}).PreventDefault(),
			),

			input,
			elem.Button(
				v.Markup(
					prop.Type("submit"),
					event.Click(func(e *v.Event) {
						println("btn click")
						node := input.Node()
						value := strings.TrimSpace(node.Get("value").String())
						if value != "" {
							atv.store.Dispatch(addTodo(value))
							node.Set("value", "")
						}
					}),
				),

				v.Text("Add Todo"),
			),
		),
	)
}

type body struct {
	v.Core
	store vstore.Store
}

func (b *body) Render() v.ComponentOrHTML {
	return elem.Body(
		&AddTodoView{
			store: b.store,
		},
		b.store.Connect(&TodoListView{}),
		&Footer{
			store: b.store,
		},
	)
}
04-28 20:46