5/17 部課

TODO

  • graphql
  • 填表單
  • 練習coursemap

GraphQL

上次簡單介紹過GraphQL後端了 (想複習的請參考這裡)
不過只介紹了Query,假設我們需要新增(修改)資料,在GraphQL中是使用 Mutation 這個方法

可以做個簡單對照:
GET <> Query
POST <> Mutation

我們將實作三個功能:
Query
books: 把所有books找出來

Mutation
CreateBook: 新增一本書
DeleteBook: 刪除一本書

以下為範例的程式碼:

mkdir graphql
cd graphql
npm init -y

mkdir backend
cd backend
npm init -y
npm pkg set type="module"
npm install @apollo/server graphql nodemon

# 下列的程式碼都要放在 backend 資料夾裡

定義Types

In schema.gql:

type Book { id: ID! title: String! author: String! publishDate: String! } type Query { books: [Book!]! } type Mutation { createBook(title: String!, author: String!, publishDate: String!): Book! deleteBook(id: ID!): Book! }

創建一些local資料

In data/books.js:

export let books = [ { id: 1, title: "The Great Gatsby", author: "F. Scott Fitzgerald", publishDate: "1925-04-10" }, { id: 2, title: "To Kill a Mockingbird", author: "Harper Lee", publishDate: "1960-07-11" } ];

定義resolvers

In resolvers/Query.js:

import { books } from '../data/books.js'; export const Query = { books: () => books };

In resolvers/Mutation.js:

import { books } from '../data/books.js'; export const Mutation = { createBook: (_, { title, author, publishDate }) => { const newBook = { id: books.length + 1, title, author, publishDate }; books.push(newBook); return newBook; }, deleteBook: (_, { id }) => { const bookIndex = books.findIndex(book => book.id === parseInt(id)); if (bookIndex === -1) return null; const [deletedBook] = books.splice(bookIndex, 1); return deletedBook; } };

把上述function合併至index.js中

In index.js:

import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { readFileSync } from "fs"; import { Query } from "./resolvers/Query.js"; import { Mutation } from "./resolvers/Mutation.js"; // Read type definitions from schema.gql const typeDefs = readFileSync("./src/schema.gql", "utf8"); // Resolvers const resolvers = { Query, Mutation, }; // Create Apollo Server const server = new ApolloServer({ typeDefs, resolvers }); // Start the server const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`🚀 Server ready at: ${url}`);

一樣,要能夠hot reload的話,請安裝nodemon,並且在指令的地方,把node 換成 nodemon。

graphql
├── backend
│   ├── node_modules/
│   ├── package-lock.json
│   ├── package.json
│   └── src
│       ├── data
│       │   └── books.js
│       ├── index.js
│       ├── resolvers
│       │   ├── Mutation.js
│       │   └── Query.js
│       └── schema.gql
└── package.json

後端架設好之後,我們就來用前端操作吧!

# 回到 graphql
cd ..
npm create vite@latest

√ Project name: ... frontend
√ Select a framework: » React
√ Select a variant: » JavaScript

cd frontend
npm install
npm install @apollo/client graphql

創建Apollo Provider:

In src/ApolloProvider.jsx:

import React from "react"; import { ApolloClient, InMemoryCache, ApolloProvider as Provider } from "@apollo/client"; const client = new ApolloClient({ uri: "http://localhost:4000", // Replace with your GraphQL server URL cache: new InMemoryCache(), }); const ApolloProvider = ({ children }) => { return <Provider client={client}>{children}</Provider>; }; export default ApolloProvider;

在App.jsx中定義剩餘介面

In App.jsx:

import React, { useState } from "react"; import { gql, useQuery, useMutation } from "@apollo/client"; import ApolloProvider from "./ApolloProvider"; import "./App.css"; const GET_BOOKS = gql` query GetBooks { books { id title author publishDate } } `; const CREATE_BOOK = gql` mutation CreateBook($title: String!, $author: String!, $publishDate: String!) { createBook(title: $title, author: $author, publishDate: $publishDate) { id title author publishDate } } `; const DELETE_BOOK = gql` mutation DeleteBook($id: ID!) { deleteBook(id: $id) { id } } `; const App = () => { const { loading, error, data, refetch } = useQuery(GET_BOOKS); const [createBook] = useMutation(CREATE_BOOK); const [deleteBook] = useMutation(DELETE_BOOK); const [title, setTitle] = useState(""); const [author, setAuthor] = useState(""); const [publishDate, setPublishDate] = useState(""); const handleAddBook = async () => { await createBook({ variables: { title, author, publishDate }, }); refetch(); setTitle(""); setAuthor(""); setPublishDate(""); }; const handleDeleteBook = async (id) => { await deleteBook({ variables: { id } }); refetch(); }; if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div className="container"> <h1>Books</h1> <ul> {data.books.map((book) => ( <li key={book.id}> {book.title} by {book.author} (Published: {book.publishDate}) <button onClick={() => handleDeleteBook(book.id)}>Delete</button> </li> ))} </ul> <h2>Add a New Book</h2> <div className="form"> <input type="text" placeholder="Title" value={title} onChange={(e) => setTitle(e.target.value)} /> <input type="text" placeholder="Author" value={author} onChange={(e) => setAuthor(e.target.value)} /> <input type="text" placeholder="Publish Date" value={publishDate} onChange={(e) => setPublishDate(e.target.value)} /> <button onClick={handleAddBook}>Add Book</button> </div> </div> ); }; const WrappedApp = () => ( <ApolloProvider> <App /> </ApolloProvider> ); export default WrappedApp;

引入 App.css

In src/App.css:

body { font-family: Arial, sans-serif; background-color: #f0f0f0; margin: 0; padding: 0; } .container { max-width: 800px; margin: 0 auto; padding: 20px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #333; } ul { list-style-type: none; padding: 0; } li { background-color: #fafafa; margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; } button { background-color: #ff4d4d; border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; } button:hover { background-color: #ff3333; } .form { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; } .form input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; } .form button { background-color: #4caf50; border: none; color: white; padding: 10px; border-radius: 4px; cursor: pointer; } .form button:hover { background-color: #45a049; }

最後,把index.css這個檔案刪掉,並且在index.js的地方也把 import "./index.css" 拿掉

再來就是跑npm run dev 就可以啟動前端了!(但是後端要跑起來,前端才會顯示資料)

subscription: 訂閱,就像訂閱youtube頻道,當該youtuber發佈影片時,你會收到他發新片的「通知」
而一個block假如也有訂閱某些事件的時候(例如剛剛的createbook),books的陣列一更新,那個block也會馬上收到相關的通知。這裡是用refetch() 的方法來重新執行Query以獲取最新的資料,對於固定頻率更新資料或是經過mutation的操作較為適用。

練習操作 coursemap

使用 coursemap

  1. Coursemap 的 repo clone 下來

(HTTPS 也可以,用SSH需要設定key,請參照 Generate a new SSH key)
image

cd your-target-directory

# git clone [put the copied url here]
git clone https://github.com/NTUEEInfoDep/NTUEECourseMap.git
  1. 進入coursemap 資料夾裡,安裝專案需要的套件
cd NTUEECourseMap

# yarn 的速度較 npm 快不少,因此使用 yarn 安裝跟執行
# 使用 yarn
yarn install

# 啟動 coursemap (下列兩種都可以)
yarn run dev
# 或
yarn start

接著,前往 http://localhost:8000/ ,看到畫面就成功囉!

使用系學會網站

目前還有一些設定沒有完成,之後有弄好再更新
(demo 連結在上方)
系學會網站 (目前本地跑不動)

實習地圖

未定

styled-component
https://styled-components.com/
MUI
tailwind