vstore 是用於 vecty 框架的相似 redux 的狀態管理庫。node
安裝git
go get marwan.io/vstore
代碼倉庫: https://github.com/marwan-at-work/vstoregithub
通常使用結構定義,好比redux
type Increment struct{}
和app
type AddTodo struct { Id int Text string }
好比 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。spa
vstore 的 Reduce 方法不一樣於 redux 中的 reduce 函數定義 (previousState, action) => newState, vstore 的 Reduce 方法要直接修改 state,而不是建立新的 state。code
組件須要實現 vstore.StoreComponent 接口,即提供 Connect 方法,通常在 Connect 方法中獲取新的 state,而後修改自身的屬性的值。orm
在組件的上一級組件的 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, }, ) }