iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Vue.js

Vue & GraphQL 探險之旅:30天,從新手村到魔王之巔系列 第 16

[Day16] 實戰演練:在 Vue 中運用 GraphQL 的條件查詢,實現多面貌的搜尋和排序功能

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20231015/20141111NCeT74ZCzB.png

在大數據時代的 Web 應用中,搜尋和排序已成為網站的基本需求,它們確保用戶能在這浩瀚的資訊海洋中迅速找到方向。

  • 搜尋:例如,當在一個電商網站上想要找到「皮卡丘」時,搜尋功能能夠幫助使用者立即找到相關商品,而不是瀏覽所有頁面。
  • 排序:若在查看一個食譜網站的留言部分,排序功能允許依據「最新」或「最熱門」的評論進行篩選,讓使用者更容易找到有價值或相關性高的內容。

透過這兩大功能,不僅提升了網站的使用效率,更增強了使用者的滿意度和體驗。

https://ithelp.ithome.com.tw/upload/images/20231015/20141111qJwxujdoC7.jpg


文章範例程式碼 GitHub

範例程式碼 GitHub 連結

Day16 開始前分支:feat/day_15/enhance_blog_list_view_by_pagination
Day16 進度完成分支:feat/day_16/implement_search_function


條件查詢

在本篇文章中,我們將探討如何在 GraphQL 中進行條件查詢,包含了按照特定條件進行篩選和排序的功能。

開始實作前,我們先使用 Fake Online GraphQL API — GraphQLZero 示範如何撰寫條件查詢。

 

基本搜尋:篩選特定使用者

首先,我們先取得所有的使用者清單,以便確保後續的篩選結果是正確的。

為了達到這個目的,我們將撰寫名為 getUsers 的 GraphQL 查詢。

首先,我們來查看 GraphQLZero Play Ground 的 Docs。
(題外話,感謝 GraphQL 的規範,後端開發 API 時只要根據 Schema 加上簡單的註解,我們就能夠自動生成詳細的文件,大幅減少了人工撰寫文件的時間。)

在下圖中,我們逐步瀏覽,查閱 users Field 的巢狀屬性和其型別
https://ithelp.ithome.com.tw/upload/images/20231015/20141111ZYSvjJcE3M.png

取得所有的使用者清單的 GraphQL 查詢:

query getUsers {
  users {
    data {
      id
      name
      email
    }
  }
}

查詢結果
https://ithelp.ithome.com.tw/upload/images/20231015/20141111Gn9hURhkBe.png

接著,讓我們加上條件搜尋,一樣先查看文件:
https://ithelp.ithome.com.tw/upload/images/20231015/20141111EQvY3hCKIf.png

撰寫查詢搜尋包含關鍵字 Shanna 的特定使用者:

// 查詢
query getUsersBySearch(
  $options: PageQueryOptions
) {
  users(options: $options) {
    data {
      id
      name
      email
    }
  }
}
// 變數
{
  "options": {
    "search": {
      "q": "Shanna"
    }
  }
}

查詢結果
https://ithelp.ithome.com.tw/upload/images/20231015/20141111WpTpezsnQ5.png

 

基本排序:降冪排序,最新的內容在最前面

預設的排序方式,大多數時候是由後端的資料來源或資料庫來決定的,或者是由後端開發者在實作 GraphQL 解析器(resolver)時明確定義某個預設排序方式(例如按照日期降冪排序)。

例如,如果 GraphQL 伺服器是連接到一個 PostgreSQL 資料庫,並且在查詢時沒有明確地指定排序條件,那麼返回的資料順序可能會是基於資料庫中的實際儲存順序。

https://ithelp.ithome.com.tw/upload/images/20231015/20141111VubLThdsW9.png

在前端開發過程中,深入理解伺服器的預設排序行為或主動指定排序條件是至關重要的。這不僅確保我們能夠精準地掌握資料的呈現順序,還有助於我們更高效地整理和展示資料,從而提供更佳的用戶體驗。

反過來說,如果我們自作聰明,擅自認定伺服器的預設排序行為總是與我們的期望相吻合,那麼很可能就會遭遇意外的 Bug。

例如,我們可能期待最新日期的文章總是出現在最前面,但伺服器實際上可能是按照文章ID或其他標準來排序的,這將導致前端展示出現混亂,給使用者帶來困惑。
https://ithelp.ithome.com.tw/upload/images/20231015/20141111Z5Qq7cjkH0.png

GraphQLZero Play Ground Docs

可以看到 API 提供了 sort,其中 ASC 代表「升冪排序」,而 DESC 代表「降冪排序」。
https://ithelp.ithome.com.tw/upload/images/20231015/20141111T6apdFhvYq.png

修改先前的 getPostsPerPage 的變數:

{
  "options": {
    "paginate": {
      "page": 1,
      "limit": 3
    },
    "sort": {
      "field": "id",
      "order": "DESC"
    }
  }
}

查詢結果
https://ithelp.ithome.com.tw/upload/images/20231015/20141111h73AGuSwu2.png

進階用法

考慮到篇幅的限制,我們先介紹基礎的部分。在基礎操作得心應手之後,如果 GraphQL API 伺服器提供更進階的支援,我們還可以進一步利用比較運算子來執行更多元的搜尋和排序策略。

例如,我們可以查詢評論數超過 99 的熱門文章,並結合日期的降冪排序,確保最新的文章始終位於前端展示的最前面。


實戰:為部落格新增文章搜尋功能

讓我們開始實作!首先,我們需要了解文章搜尋功能的實作流程

  1. 新增路由: 新增一條專門用於展示搜尋結果的路由,此路由能接受名為 keyword 的查詢字符串 (query string)。
  2. 新增搜尋結果頁元件: 接著,打造出一個展示搜尋結果的頁面,該頁面應該要能展示搜尋關鍵字和支援分頁功能
    • 透過編寫 GraphQL Query Variables 來取得相對應的查詢結果。
    • 考慮到安全性,需要對搜尋的字串做適當的限制,以預防 XSS 攻擊。
  3. 搜尋表單整合: 最後,我們將在 Header 和 Sidebar 中整合搜尋框,提供使用者直接進行搜尋的功能。

 
完整程式碼範例請參考:feat/day_16/implement_search_function

新增搜尋結果的路由

src/router/index.ts 新增 search-results 路由:

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    // ...
    {
      path: '/search',
      name: 'search-results',
      component: () => import('@/views/BlogSearchView.vue'),
      props: route => ({ keyword: route.query.keyword }),
    },
    // ...
  ],
})

這樣設定之後,當使用者訪問 /search?keyword=someKeyword 這個路由,BlogSearchView Component 將會被渲染。在該元件內,我們可以直接使用 props.keyword 來獲取 someKeyword,並基於此進行相對應的搜尋處理。

開發搜尋結果頁元件

首先,新增 src/views/BlogSearchView.vue,接著我們來分析這個元件需要擔任的職責:

  1. 接收 props 中的 keyword: 當有提供 keyword 時,顯示搜尋的提示框;若缺少關鍵字,則顯示一個錯誤警告。
    https://ithelp.ithome.com.tw/upload/images/20231015/20141111ek4QIqtphn.png
    https://ithelp.ithome.com.tw/upload/images/20231015/20141111K7JXDiVnh3.png
  2. 對輸入的關鍵字進行前置驗證: 這步驟主要是為了避免可能的安全問題,例如:XSS攻擊。
  3. 撰寫條件查詢: 透過撰寫 GraphQL Query Variables 來取得與該關鍵字相關的查詢結果。
  4. 渲染搜尋結果: 將 API 返回的搜尋結果顯示在頁面上。此部分的邏輯與 src/views/BlogListView.vue 是相似的。

接收 props 中的 keyword 並進行驗證

src/views/BlogSearchView.vue 中,取得 props.keyword 並進行驗證

<script setup lang="ts">
// ...
const props = defineProps<{
  keyword?: string
}>()

// 取得 props.keyword
const { keyword } = toRefs(props)

function sanitizeKeyword(inputKeyword: string | undefined): string {
  if (!inputKeyword)
    return ''

  // 只允許英文、數字、底線、減號、空白
  let sanitized = inputKeyword.replace(/[^a-zA-Z0-9\-_ ]/g, '')

  // 避免 DoS 攻擊或其他可能的問題,限制長度 100
  const maxLength = 100
  if (sanitized.length > maxLength)
    sanitized = sanitized.substring(0, maxLength)

  return sanitized
}

const sanitizedKeyword = computed(() => {
  return sanitizeKeyword(keyword?.value)
})
// ...
</script>

注意:我們在此示範的驗證方式可能過於保守和基礎。儘管這種方法可能更加安全,但我們必須認識到安全性和便利性有時可能是相互衝突的。根據實際的產品需求,開發者會需要進行適當的調整。

根據 sanitizedKeyword 渲染搜尋關鍵字提示框

我們透過簡單的 v-if 根據 sanitizedKeyword 去判斷要顯示何種提示框
src/views/BlogSearchView.vue

<template>
  <div
    v-if="sanitizedKeyword"
    class="flex items-center p-4 mb-4 text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400"
    role="alert"
  >
      <!-- ... -->
  </div>
  <div v-else>
      <!-- ... -->
      <div>
        <span class="font-medium">未輸入關鍵字</span>
      </div>
      <!-- ... -->
  </div>
  <!-- ... -->
</template>

撰寫條件查詢

src/views/BlogSearchView.vue

<script setup lang="ts">
// ...
const { result, loading, error } = useQuery(gqlGetPostsPerPage, () => ({
  options: {
    // 分頁
    paginate: {
      page: currentPage.value,
      limit: limit.value,
    },
    // 搜尋 by sanitizedKeyword
    search: {
      q: sanitizedKeyword.value,
    },
    // 根據 id 降冪排序
    sort: [{
      field: 'id',
      order: SortOrderEnum.Desc,
    }],
  },
}))
// ...
</script>

值得注意的是,我們使用了與 src/views/BlogListView.vue 相同的 GraphQL 文件 gqlGetPostsPerPage,這展現了模組化重構的優點,我們只需要修改 GraphQL Variables 就能夠輕鬆獲得不同的查詢結果。

渲染搜尋結果

這部分可以直接重複使用 src/views/BlogListView.vue 同樣的邏輯。

在 Header 和 Sidebar 中實作搜尋框功能

使用 Vue 的 v-model 結合 Vue Router,讓我們能迅速地完成此功能!

首先,在 src/components/layouts/header/Header.vue,我們設定 searchTerm 和定義 handleSearch 方法:

<script setup lang="ts">
// ...

const router = useRouter()

// 使用 ref 雙向綁定搜尋關鍵字
const searchTerm = ref('')

// 利用 Vue Router 的 push 方法來處理當搜尋表單被提交的行為
function handleSearch() {
  if (searchTerm.value)
    router.push({ name: 'search-results', query: { keyword: searchTerm.value } })
}
// ...
</script>

接著,我們將 searchTerm 使用 v-model 綁定到搜尋輸入框上,如下:
src/components/layouts/header/Header.vue

<input
    id="topbar-search"
    v-model="searchTerm"
    type="text"
    name="email"
    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
    placeholder="Search"
>

這樣就完成囉!這就是使用 Vue 框架能夠達到的流暢且高效的開發體驗!

成果展示與驗證

鍵入 temporibus 查看搜尋結果
https://ithelp.ithome.com.tw/upload/images/20231015/20141111PFFBbCoVZk.png

與 Play Ground 的測試結果一致
https://ithelp.ithome.com.tw/upload/images/20231015/20141111mTEMF7fNVN.png


Recap

在今日的文章中,我們透過實際應用 GraphQL 的多種條件查詢技巧,深入探討並實現了部落格的多元搜尋和排序功能。

然而,在實際開發中,我們常常需要更多細緻的策略和考量。例如,目前在 Header 與 Sidebar 中存在了重複的搜尋功能邏輯。而這正是 Vue Composition API 的 Composable 功能可以發揮的地方,讓我們能夠更有效地重構和管理重複的程式碼。

此外,我們在文章中也提到了 XSS 攻擊的風險。雖然現今的瀏覽器和框架已經提供了許多安全防護機制,但對開發者而言,理解這些潛在的風險及其背後的原理仍然十分重要。只有當我們明白哪些做法是不佳的,我們才能確保撰寫更為健全和安全的程式。

期待明天的教學,我們將探討 Vue 框架如何使用插值 (Interpolation) 進行文本轉譯 (Text Interpolation),以及哪些程式碼寫法容易導致安全風險。


上一篇
[Day15] 實戰演練:在 Vue 中,運用 GraphQL 的魔法分頁查詢優化使用者體驗!
下一篇
[Day17] 星塵護盾:防禦 XSS 的第一道防線 – Vue 的文本轉義機制
系列文
Vue & GraphQL 探險之旅:30天,從新手村到魔王之巔31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言