Skip to content

此為 Nuxt3+Supabase+Editor.js 結合製作的部落格小專案。

Notifications You must be signed in to change notification settings

WangShuan/nuxt3-notes

Repository files navigation

tags
Vue.js

Nuxt3-project6 Notes

原始碼:https://github.com/WangShuan/nuxt3-notes

線上連結:https://xuan-nuxt3-note.js-app.life/

建立與啟動 Nuxt 專案

開啟終端機,cd 到桌面或任何希望創建該專案的位置, 執行命令:

npx nuxi init 06-notes

完成後,根據提示, 先 cd 到專案目錄 06-notes 中, 接著執行命令:

npm install

安裝所有依賴項目, 此時會發現專案目錄中生成了 node_modules 資料夾

確認您的專案已成功安裝好所有依賴後, 即可執行命令:

npm run dev

啟動 Nuxt 應用程序。

專案說明

本專案將結合使用 supabase 製作一個可登入、註冊的筆記應用程式。

首先請先到supabase 官網註冊帳號(可以使用 GitHub 快速註冊), 接著點擊建立 supabase 專案,輸入隨意的名稱比如 nuxt3-notes 然後設置一個高級密碼、選擇伺服器位置(新加坡或東京)。

安裝與設定 supabase

請在專案中執行命令:

npm install @nuxtjs/supabase --save-dev

參考說明:https://supabase.nuxtjs.org/get-started

接著修改根目錄中的 nuxt.config.ts 檔案:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ['@nuxtjs/supabase'], // 使用 modules
})

最後在項目根目錄中新增 .env 檔案存放 url 與 key:

SUPABASE_URL="從 supabase 專案的設定中點選 API 即可看到 Project URL"
SUPABASE_KEY="從 supabase 專案的設定中點選 API 即可看到 Project API keys"

使用 supabase 中的 Authentication

首先將登入、註冊與登出三種方法作為 composables, 在項目根目錄中新增資料夾 composables, 並在裡面新增檔案 useAuth.ts 開始撰寫登入與註冊等事件。

Authentication 說明文檔:https://supabase.nuxtjs.org/usage/composables/use-supabase-auth-client#signin

預設 supabase 的 auth 會開啟信箱驗證的機制, 註冊後需要到信箱中收信並點擊驗證信箱後才能開通帳號, 如果希望取消驗證信的機制,可以在 supabase 的專案頁面中點擊 auth , 接著點進左側的 Providers 將 Confirm email 關閉後再點擊右下角 Save 即可。

useAuth.ts 檔案最終如下:

const useAuth = () => {
  const supabase = useSupabaseClient(); // 引入使用 supabase
  const user = useSupabaseUser() // supabase 提供的獲取當前 user 資料
  
  const SignUp = async (userData: UserData, name: string) => { // 註冊
    const { data: u, error } = await supabase.auth.signUp({
      ...userData, // 傳入 email 與 password
      options: { // 可選參數,這邊用來保存用戶姓名
        data: {
          name: name,
        }
      }
    })

    if (error) throw error
    return u
  };

  const LogIn = async (userData: UserData) => { // 登入
    const { data: u, error } = await supabase.auth.signInWithPassword(userData)
    if (error) throw error
    return u
  }

  const LogOut = async () => { // 登出
    const { error } = await supabase.auth.signOut()
    if (error) throw error
  }

  const ResetPassword = async (pwd: string) => { // 重設密碼
    const { data: u, error } = await supabase.auth.updateUser({ password: pwd })
    if (error) throw error
    return u
  }

  return { user, SignUp, LogIn, LogOut, ResetPassword } // 將所有功能導出
}

export default useAuth;

接著即可開始建立登入/註冊頁面: 首先於根目錄中新增 pages 資料夾,並新增 sign-in.vue 檔案, 於 sign-in.vue 檔案中撰寫登入與註冊畫面, 並通過 const { SignUp, LogIn, user } = useAuth() 引入登入、註冊方法以及當前用戶資料。

接著新增元件 Navbar.vue 放置在 components 資料夾中, 在 navbar 中放置一個登出的按鈕 並通過 const { user, LogOut } = useAuth() 引入登出方法以及當前用戶資料, 這邊可以通過 user.user_metadata.name 獲取當前用戶的姓名(註冊時藉由 options 保存的 name), 藉由 user 判斷當前是否有人登入,並將 navbar 右側的登入按鈕改為顯示用戶姓名+分頁選單(所有文章、我的文章、更改密碼、登出)。

使用 supabase 中的 database

接下來我們將開始建立筆記,並將筆記保存到 supabase 的 database 中, 在 supabase 中使用的是 PostgreSQL 的資料庫, 首先需要進入 supabase 的專案頁面中點擊 Database, 接著於 Table 中點擊右上角的 + New Table 建立資料表, 這邊輸入 name 為 notes, 然後把 Enable Row Level Security (RLS) 取消勾選(這樣所有人都可以公開讀寫此資料表), 最後設置好 Columns:

  • id 用預設的即可
  • created_at 也是預設即可
  • title 為 text 保存筆記標題
  • note 為 text 保存筆記內容
  • user_id 為 UUID 保存用戶 id
  • user_name 為 text 保存用戶姓名

完成後即可開始建立新增筆記與所有筆記的頁面, 首先於 pages 資料夾中新增檔案 /note/new.vue 用作新增筆記的頁面, 接著將首頁 /pages/index.vue 拿來用於顯示所有筆記。

以首頁來說,使用 database 獲取所有筆記的方式如下:

const isLoading = ref(true) // 建立 isLoading 用於切換顯示載入中畫面
const notes = ref() // 建立 notes 用於存放所有筆記
if (process.client) { // 判斷如果當前是客戶端渲染才執行
  notes.value = await supabase.from('notes') // 使用 supabase.from('table 名稱') 對資料表進行操作
    .select() // 接著用 .select() 方法獲取所有欄位資料
    .order('created_at', { ascending: false }); // 用 order 方法設定排序根據 created_at 遞減
  isLoading.value = false // 將 isLoading 設為結束以顯示資料
}

以新增筆記來說,使用 database 插入數據的方式如下:

const supabase = useSupabaseClient();
const { user } = useAuth();
const submitNote = async () => {
  if (!notesInput.title || !notesInput.note) { // 用於顯示欄位不得為空的錯誤提示
    errMsg.title = !notesInput.title ? true : false;
    errMsg.note = !notesInput.note ? true : false;
    return;
  }
  errMsg.title = false; // 清除錯誤提示
  errMsg.note = false; // 清除錯誤提示
  
  await supabase.from('notes') // 使用 supabase.from('table 名稱') 對資料表進行操作
    .insert({ // 接著用 .insert() 方法插入資料
      title: notesInput.title, // 傳入 title 欄位的值
      note: notesInput.note, // 傳入 note 欄位的值
      user_id: user.value?.id, // 傳入 user id
      user_name: user.value.user_metadata.name // 傳入 user name
    });
  navigateTo('/'); // 跳轉至首頁
};

middleware 使用路由守衛

在 Nuxt 中可以通過於根目錄中建立 middleware 資料夾新增路由守衛, 舉例來說,如果要新增筆記,由於需保存 user_id 以及 user_name 所以需要確保當前有登入帳號, 此時就可以通過 middleware 來設定路由守衛,將尚未登入的用戶,導連至登入/註冊頁面。

建立方式: 於項目根目錄中新增檔案 /middleware/auth.ts:

export default defineNuxtRouteMiddleware(() => {
  const { user } = useAuth();
  if (!user.value) {
    return navigateTo('/sign-in')
  }
})

專案升級,添加 Editor.js

首先,安裝 Editor.js: 開啟終端機,執行命令 npm i @editorjs/editorjs --save

接著安裝 Editor.js 中的段落、標題、圖片上傳等需要用到的工具: 開啟終端機,執行命令 npm i --save @editorjs/header @editorjs/list @editorjs/paragraph @editorjs/image

完成後於項目中新增一個元件 Editor.vue 用來放編輯器(記得放在 components 資料夾裡面):

<template>
  <!-- 新增一個設定好 id 的空元素用來指定 editor.js 生成的地方 -->
  <div id="editorjs-container"></div>
</template>

<script setup>
// 引入 editor.js
import EditorJS from '@editorjs/editorjs';
// 引入安裝的其他工具,這邊安裝最常用的標題、段落文字、列表、圖片上傳這四項
import Header from '@editorjs/header';
import Paragraph from '@editorjs/paragraph';
import List from '@editorjs/list';
import ImageTool from '@editorjs/image';

// 開始創建 editor
const editor = new EditorJS({
  holder: 'editorjs-container', // 這邊傳入上方 template 中設置好的元素 id
  autofocus: true, // 是否要再生成後自動 focus
  minHeight: 0, // 取消整個 editor 的 padding-bottom: 300px
  tools: { // 選用引入的工具
    header: Header, // 標題工具對應 import 的 Header
    paragraph: Paragraph, // 段落公居對應 import 的 Paragraph
    list: List, // 列表工具對應 import 的 List
    image: { // 圖片工具
      class: ImageTool, // 對應 import 的 ImageTool
      config: { // 針對圖片工具的其他設定
        uploader: { 
          // 這裡用來設定圖片檔案上傳功能的 function
        }
      }
    }
  },
});
</script>

使用 storage 上傳&獲取圖片網址

接下來我們將設定上傳圖片的功能,並將圖片保存到 supabase 的 storage 中, 首先需要進入 supabase 的專案頁面中點擊 Storage, 接著於 Storage 中點擊左上角的 + New bucket 建立儲存桶, 這邊輸入 name 為 note-images, 然後把 Public bucket 設為開啟(這樣所有人都可以公開使用此儲存桶), 完成後即可開始撰寫上傳圖片的 function

Storage 說明文件參考:https://supabase.com/docs/guides/storage/quickstart

首先我們使用的圖片工具是:https://github.com/editor-js/image

根據該工具的說明,在 image 底下的 uploader 中可設置圖片上傳到後端的功能, 其中又分為通過純網址上傳、通過自定義函數上傳, 這邊我們採用自定義函數上傳的方式撰寫如下:

image: {
  class: ImageTool,
  inlineToolbar: true,
  config: {
    uploader: { 
      // 主要新增程式碼
      async uploadByFile(file) { // 函數名稱必須是 uploadByFile
        const fileExtension = file.name.substring(file.name.lastIndexOf('.') + 1); // 獲取副檔名
        const name = `${new Date().getTime()}.${fileExtension}`; // 重設圖片保存的名稱(因為上傳中文檔名會報錯,所以改用當前時間當作檔案名稱)
        const { data } = await supabase.storage // 使用 supabase 提供的 storage API 進行上傳圖片
          .from('note-images') // from 括號中的內容為 Supabase 存儲桶的名稱
          .upload(name, file); // upload 括號中第一個值為 檔案保存的名稱,第二個值為上傳的檔案

        const uploadedImageUrl = await supabase.storage.from('note-images').getPublicUrl(data.path); // 接著通過 getPublicUrl API 獲取圖片對外網址
        return { // 最後回傳成功&圖片網址用以顯示圖片
          success: true,
          file: {
            url: uploadedImageUrl.data.publicUrl,
          }
        };
      }
    }
  }
}

保存與顯示 Editor 內容的方法

在新增文章與編輯文章中分別需要保存與顯示 Editor 的內容

保存 Editor 的內容方法

在新增文章中,原本用來保存 Content 的是 textarea, 這邊將 textarea 整塊刪除,改為放置 <div id="editor-container"></div>, 並於 script 中設置 editor, 完成後將送出按鈕綁定的事件進行改寫:

const submitNote = async () => {
  if (!title.value) {
    errMsg.value = true;
    return;
  }
  errMsg.value = false;
  const savedData = await editor.save(); // 獲取 editor 的內容
  const contentJson = JSON.stringify(savedData); // 將內容轉為 json 格式保存到資料庫中以便下次讀取使用
  
  let note = ''; // 新增空字符串用以保存要顯示的 html 內容
  savedData.blocks.forEach(item => { // savedData.blocks 是每個新增的 editor 內容區塊,通過遍歷循環改成 html 格式
    if (item.type === "paragraph") { // 判斷如果類型為 paragraph 段落,則回傳 p 標籤
      note += `<p>${item.data.text}</p>`;
    }
    if (item.type === 'image') { // 判斷如果類型為 image 圖片,則根據是否有邊框、滿版寬、背景,添加 class 並回傳 img 標籤
      let classes = 'img-fluid';
      if (item.data.withBorder) {
        classes += ' border';
      }
      if (item.data.stretched) {
        classes += ' w-100';
      }
      note += `<div class="image-block${item.data.withBackground ? ' img-withBackground' : ''}">
          <img class="${classes}" src="${item.data.file.url}" alt="${item.data.caption}" />
        </div>
        <p class="image-description">${item.data.caption}</p>`;
    }
    if (item.type === "header") { // 判斷如果類型為 header 標題,則根據其 level 回傳對應的 h1~h6 標籤
      const tag = `h${item.data.level}`;
      note += `<${tag} class="fs-${item.data.level}">${item.data.text}</${tag}>`;
    }
    if (item.type === 'list') { // 判斷如果類型為 list 列表,則根據其 style 回傳對應的 ol 或 ul 標籤
      const tag = item.data.style === 'unordered' ? 'ul' : 'ol';
      const lis = '';
      item.data.items.forEach(li => lis += `<li>${li}</li>`);
      note += `<${tag}>
        ${lis}
      </${tag}>`;
    }
  });

  await supabase
    .from('notes')
    .insert({
      title: title.value,
      note: note, // 將 html 內容保存到 note 中
      content: contentJson, // 在 Table 中新增 column 名為 content 保存 editor 完整的內容
      user_id: user.value?.id,
      user_name: user.value.user_metadata.name
    });

  router.push('/');
};

最後將原本的 NoteCard.vue 以及顯示文章的 /notes/[id].vue 檔案, 改為用 v-html 顯示 note 內容即完成(原本是用 textarea 保存並用 pre 顯示純文字)。

顯示 Editor 的內容方法

在保存 Editor 的內容時通過 editor.save() 獲取到了所有數據資料, 並藉由 JSON 格式保存到資料庫中。

顯示則將提取到的內容重新轉為 JSON 格式, 並通過在 new EditorJS({}) 中添加 data 選項為轉換後的 JSON 內容即完成。

這邊我們將稍早新增的 Editor.vue 元件進行改寫, 並在 /pages 中新增檔案 /edit/[id].vue 用來進行編輯文章的功能, 於編輯文章時取用顯示 Editor 的內容方法再結合保存 Editor 的內容方法完成整個專案升級。

supabase 中編輯資料庫的方法:

await supabase
  .from('notes')
  .update({ // 通過 update 方法傳入需要更新的欄位與對應的內容
    title: title.value,
    note: content,
    content: contentJson,
  })
  .eq('id', route.params.id); // 指定更新的項目為 id 相等者