TodoMVC 與 director 示例

pardon110發表於2019-07-01

概述

mvc 通常與模型,檢視,控制器三方配合。vue本身已偏向view渲染層,因此本例用localStorage 作資料持久化儲存器,director路由庫充當控制器,負責頁面節點渲染分發排程,從而實現一個簡易的todo應用。

知識點

  • vuejs單頁面的增刪改查 TodoMVC
  • vue 自定義指令,觀察函式,計算屬性,方法使用
  • localStorage 物件 結合 JSON 的序列化與反序列化作為持久化層
  • director 無依賴輕量級的前端路由庫,可在服務端/客戶端/命令列路由

路由

  • 正則匹配路由,傳參監聽
  • director 基於hash的路由方式 如 /a/c 匹配的對應路徑 index.php#/a/c
Router({
    '/(active|completed|all)/?': function (filter) {
        // 間接渲染vue例項關聯檢視
        app.visibility = filter
    }
}).init()

資料持久化

var STORAGE_KEY =  'pardon110'
var todoStorage = {
    fetch() {
        var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
        todos.forEach((todo, index) => todo.id = index);
        todoStorage.uid = todos.length
        return todos
    },
    save(todos) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
    }
}

效果圖

TodoMVC 與 director

原始碼

  • app.d.js
var STORAGE_KEY = 'pardon110'
// 儲存器例項物件
var todoStorage = {
    fetch() {
        var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
        todos.forEach((todo, index) => todo.id = index);
        todoStorage.uid = todos.length
        return todos
    },
    save(todos) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
    }
}
// 可見性過濾
var filters = {
    all: todos => todos,
    active: todos => todos.filter(todo => !todo.completed),
    completed: todos => todos.filter(todo => todo.completed)
}

var app = new Vue({
    // 初始化應用狀態
    data: {
        todos: todoStorage.fetch(),
        newTodo: '',
        editedTodo: null,
        visibility: 'all'
    },
    // 觀察持久化儲存
    watch: {
        todos: {
            // 注意,不應該使用箭頭函式來定義 watcher 函式 
            // 箭頭函式繫結了父級作用域的上下文, 其this 將不會按照期望指向 Vue 例項
            handler: todoStorage.save,
            // 該回撥會在任何被偵聽的物件的 property 改變時被呼叫,不論其被巢狀多深
            deep: true
        }
    },
    // 計算屬性
    computed: {
        filteredTodos() {
            return filters[this.visibility](this.todos)
        },
        remaining() {
            return filters.active(this.todos).length
        },
        // getter/setter
        allDone: {
            get() {
                return this.remaining === 0
            },
            set(value) {
                this.todos.forEach(todo => todo.completed = value)
            }
        }
    },

    // 檢視過濾器,型別於管道用|
    filters: {
        // 複數轉換
        pluralize: function (n) {
            return n === 1 ? 'item' : 'items'
        }
    },

    // 實現資料增刪改邏輯,注意不直接操作Dom節點
    methods: {
        addTodo() {
            var value = this.newTodo && this.newTodo.trim()
            if (!value) {
                return
            }
            this.todos.push({
                id: todoStorage.uid++,
                title: value,
                completed: false
            })
            this.newTodo = ''
        },

        removeTodo(todo) {
            this.todos.splice(this.todos.indexOf(todo), 1)
        },

        // 編輯資料
        editTodo(todo) {
            // 快取編輯前內容
            this.beforeEditCache = todo.title
            // 保留編輯後內容
            this.editedTodo = todo
        },
        doneEdit(todo) {
            if (!this.editedTodo) {
                return
            }
            this.editedTodo = null
            todo.title = todo.title.trim()
            if (!todo.title) {
                this.removeTodo(todo)
            }
        },
        cancelEdit(todo) {
            this.editedTodo = null
            todo.title = this.beforeEditCache
        },

        removeCompleted() {
            this.todos = filters.active(this.todos)
        }
    },

    // 自定義vue指令,在輸入欄位前實現聚集效果
    directives: {
        'todo-focus': function (el, binding) {
            if (binding.value) {
                el.focus()
            }
        }
    }
})

// 使用director庫,註冊路由
Router({
    '/(active|completed|all)/?': function (filter) {
        // 變更vue各應資料,渲染檢視
        app.visibility = filter
    }
}).init()

// Vue例項掛載Dom節點
app.$mount('.todoapp')
  • index.html 檔案
<!DOCTYPE html>
<html data-framework="vue">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Vue.js • TodoMVC</title>
    <link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
    <!-- 前端路由庫(支援spa應用) -->
    <script src="https://cdn.bootcss.com/Director/1.2.8/director.js"></script>
    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
    <section class="todoapp">
        <header class="header">
            <h1>todos</h1>
            <!-- 雙向繫結新增 -->
            <input v-model="newTodo" placeholder="寫點什麼?" autofocus autocomplete="off" @keyup.enter="addTodo"
                class="new-todo">
        </header>

        <section class="main" v-show="todos.length" v-cloak>
            <!-- 全選/全不選切換 -->
            <input type="checkbox" class="toggle-all" v-model="allDone">
            <!-- 顯示列表 -->
            <ul class="todo-list">
                <li v-for="todo in filteredTodos" :key="todo.id"
                    :class="{ completed: todo.completed, editing: todo == editedTodo }">
                    <div class="view">
                        <input type="checkbox" class="toggle" v-model="todo.completed"><label
                            @dblclick="editTodo(todo)">{{ todo.title }}</label>
                        <button class="destroy" @click="removeTodo(todo)"></button>
                    </div>
                    <input type="text" class="edit" v-model="todo.title" v-todo-focus="todo == editedTodo"
                        @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
                </li>
            </ul>
        </section>

        <footer class="footer" v-show="todos.length" v-cloak>
            <span class="todo-count">
                <strong> {{ remaining }}</strong>
                <!-- 過濾器管道 -->
                {{ remaining | pluralize }} left
            </span>

            <!--使用directory庫進行路由,自動分發排程,觸發vue例項回撥,間接渲染檢視-->
            <ul class="filters">
                <li>
                    <a href="#/all" :class="{ selected: visibility == 'all'}">全部</a>
                    <a href="#/active" :class="{ selected: visibility == 'active'}">活躍</a>
                    <a href="#/completed" :class="{ selected: visibility == 'completed'}">回收站</a>
                </li>
            </ul>
            <button class="clear-completed" @click="removeCompleted" v-show="todos.length >  remaining">
                清空
            </button>
        </footer>
    </section>
    <footer class="info">
        <p>雙擊編輯一個todo應用</p>
        <p>Written by <a href="http://evanyou.me">Evan You</a></p>
        <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>

    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="app.d.js"></script>
</body>
</html>

相關文章