iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 23
0
Modern Web

用 JavaScript 打造全端產品的入門學習筆記系列 第 23

產品工匠日常:打磨全端產品的實作細節——全端刻意練習 II

developer

developer from Unsplash

產品工匠日常:打造全端產品的宏觀程序 中,彙整了從頭到尾開發一個全端產品(web app)所需的各個程序。仔細閱讀後會發現「3.0 開發主要功能」的部分著墨不多,原因是只要以筆記中的五個步驟,基本上大部分的功能應該都能實現,決定速率的通常是對解決特定技術問題的熟練程度。

細節藏在強化的功能裡

然而,經過一連串的功能實作後,我發現有些用以強化使用者體驗的功能其實會互相影響。原因是他們都針對同一批資料進行處理,並且產生交集或聯集。本篇將記錄我在整合「排序 sort、篩選 filter 及 搜尋 Search」功能時遇到的問題,及如何尋求協助以解決問題。

筆記目的

本篇筆記將將接續 前篇,提供刻意練習的心得,並將解決以下問題:

  • 如何「打磨資料處理細節」以整合「排序 sort、篩選 filter 及 搜尋 Search」?

誰適合閱讀:

  • 想整合互相影響的功能者

 

打磨資料處理細節以強化並整合功能

由於在 課程 的上個階段,已經有整合多個處理資料功能的經驗,這次我在實作前就已經安排好順序,並思考過可能發生衝圖及矛盾的地方。印象中,前次就是做到一半才發現功能會互相抵觸,而且資料處理的邏輯散亂在各支函式當中,折騰了一番才梳理出解決方案。

以下整理出我這次的處理邏輯:

1. 構想目標結果

Yourator filter search bar

Yourator 的搜尋篩選功能

在實作這項功能前,我想起之前使用 Yourator 時,對他們的搜尋功能印象深刻,使用體驗也不錯;但由於這份作業時間有限,我只取部分元素來優化。打算在之後的 side project 中再來復刻。

2. 安排開發順序

KL tracker filter search bar

這是一個記帳程式的部分功能,初步的構想是

  • 身為使用者,我能針對部分類別來核對帳款,以比較某些類別的開支
  • 身為使用者,我能以關鍵字搜尋特定紀錄,以避免忘記時還得大量瀏覽
  • 身為使用者,我能按金額排序紀錄,以確認較大比例的開支集中在哪裡
  • 身為使用者,我能查看篩選紀錄的總金額,已確認某些情況的具體支出

由於當初只用紙筆畫下需求,這邊提供最終結果的截圖。在首頁的最上方依序排列「依複數類別篩選 filter」、「以名稱搜尋 search」、「按金額排序 sort」等功能,並共用一個確認送出按鍵(submit button)。總金額則是加總篩選結果的金額。

我由左至右依序開發,如此設計的邏輯是:以「資料處理的難易度」由高至低排序。因為隨著進度推進,多個功能開始共同處理資料時,邏輯複雜度會逐步上升;若一開始就把複雜的邏輯處理完,後續僅需稍加處理就能整合新增的功能。

3. 實作各項功能

開發各項功能的紀錄可以參考我的 GitHub,這邊提供第一版功能的路由內 MVC 邏輯:

// Set routes to filter, search record
router.get('/', (req, res) => {
  const { filter } = req.query             // 篩選的類別
  const keyword = req.query.keyword.trim() // 搜尋的關鍵字
  const sort = req.query.sort              // 排序的方式

  Category.find()
    .lean()
    .sort({ _id: 'asc' })
    .then(categories => { // 撈出所有類別是為了用在篩選類別的 select options 中
      Record.find({ category: filter })
        .populate('category')
        .lean()
        .sort({ amount: sort })
        .then(records => {
          // search keyword
          records = records.filter(record => record.name.toLowerCase().includes(keyword.toLowerCase()))

          // checked total amount
          let totalAmount = 0
          records.forEach(record => totalAmount += record.amount)

          // render records
          res.render('index', { records, totalAmount, categories, keyword, sort })
        })
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
})

4. 優化使用體驗

其實上面這版已經優化部分體驗,如保留關鍵字及排序方式。但由於 Handlebars 的限制,在第一次完成時,始終找不到好的解決方案,以保留使用者選取的類別。

在經過一番觀摩和查詢資料後,我重新設計了篩選器,並且加上了「選取全部」及「清除全部」的兩顆按鈕,方便使用者要更換篩選類別時,不必逐一取消、再逐一開啟。並且區分選取及未選取的類別,分開渲染,已達到保留使用者選取紀錄的目的。

  • 路由內部邏輯
// Set routes to filter, search record
router.get('/', (req, res) => {
  const { filter } = req.query
  const keyword = req.query.keyword.trim()
  const sort = req.query.sort

  Category.find()
    .lean()
    .sort({ _id: 'asc' })
    .then(categories => {
      // checked select options
      let checkedCategories = []
      let otherCategories = []
      categories.forEach(category => {
        if (filter.includes(category._id.toString())) {
          checkedCategories.push(category)
        } else {
          otherCategories.push(category)
        }
      })

      Record.find({ category: filter })
        .populate('category')
        .lean()
        .sort({ amount: sort })
        .then(records => {
          // search keyword
          records = records.filter(record => record.name.toLowerCase().includes(keyword.toLowerCase()))

          // checked total amount
          let totalAmount = 0
          records.forEach(record => totalAmount += record.amount)

          // render records
          res.render('index', { records, totalAmount, keyword, sort, checkedCategories, otherCategories })
        })
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
})
  • 篩選器設計
    {{!-- filter switch --}}
    <div class="filter col-md-3 my-1">
      <div class="card mb-3" id="category-filter">
        <div class="card-header bg-secondary text-light">
          篩 選 類 別
        </div>
        <div class="card card-body container">
          <div id="options" class="row">
            {{#each checkedCategories}}
            <div class="custom-control custom-switch col-4 col-md-6 pb-3">
              <input type="checkbox" class="custom-control-input" id="customSwitch{{this._id}}" name="filter"
                value="{{this._id}}" checked>
              <label class="custom-control-label" for="customSwitch{{this._id}}">{{this.title}}</label>
            </div>
            {{/each}}
            {{#each otherCategories}}
            <div class="custom-control custom-switch col-4 col-md-6 pb-3">
              <input type="checkbox" class="custom-control-input" id="customSwitch{{this._id}}" name="filter"
                value="{{this._id}}">
              <label class="custom-control-label" for="customSwitch{{this._id}}">{{this.title}}</label>
            </div>
            {{/each}}
          </div>
          <div id="all-none-btn" class="btn-group mt-3" role="group" aria-label="all-none-btn">
            <button id="selectAll" type="button" class="btn btn-outline-dark">選擇全部</button>
            <button id="clearAll" type="button" class="btn btn-outline-dark">清除全部</button>
          </div>
        </div>
      </div>
    </div>
  • 全選及清除按鈕:引入 javascript 靜態檔案,以 DOM 操作按鈕
document.querySelector('#selectAll').addEventListener('click', () => {
  $('input[type="checkbox"]').prop('checked', true)
})

document.querySelector('#clearAll').addEventListener('click', () => {
  $('input[type="checkbox"]').prop('checked', false)
})

 


閱讀更多

Infinite Gamer
關於本系列更多內容及導讀,請閱讀作者於 Medium 個人專欄 【無限賽局玩家 Infinite Gamer | Publication – 】 上的文章 《用 JavaScript 打造全端產品的入門學習筆記》系列指南


上一篇
產品工匠日常:打造全端產品的宏觀程序——全端刻意練習 I
下一篇
用 MongoDB 及 Mongoose Model.Populate() 實作關連式資料庫——全端刻意練習 III
系列文
用 JavaScript 打造全端產品的入門學習筆記30

尚未有邦友留言

立即登入留言