使用 React Router 管理路由、Material UI 作為元件庫
用戶可以
- 登入並瀏覽不同文章類別
- 透過上/下一頁在不同的文章類別之間切換
- 對文章表示「🙂」、「👍」
| 首頁 | 登入 | 菜單 | 儀表板 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| 路徑 | 方法 | 用途 |
|---|---|---|
/auth |
POST |
核對密碼、簽署 token |
/auth |
GET |
驗證、解碼 token |
/post |
GET |
查詢所有貼文 |
/post/reactions |
GET |
查詢所有貼文的回應 |
/post/:category |
GET |
查詢特定類別的貼文 |
- 前端
reactv18react-router-domv6@mui/materialv6
- 後端
expressv4
- 測試
vitestv2@testing-library/reactv16
# 安裝
pnpm install
# 開發
pnpm dev
# 測試
pnpm test- 調整 Apache 使其
- 回應對於三級網域的請求
- 反向代理
/api到後端伺服器的 port - 對於無效的請求路徑一律回應
index.html以便前端管理路由
# /etc/apache2/sites-available/subdomain-ssl.conf
<VirtualHost *:443>
ServerName blog.unconscious.cc
VirtualDocumentRoot /var/www/subdomain/%1
ProxyRequests Off
ProxyPass /api http://localhost:3001
ProxyPassReverse /api http://localhost:3001
<Directory ".">
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
</Directory>
# ...
</VirtualHost>- JWT 學習歷程
- 將用戶 ID 編碼為 JWT
- 驗證 JWT 完整性
// packages/server/src/routers/auth.js
export const auth = express
.Router()
.post("", (req, res, next) => {
const { name, password } = req.body;
// ...
const token = jwt.sign({ name }, process.env.JWT_SECRET);
res.json(token);
})
.get("", (req, res, next) => {
try {
// ...
const secret = process.env.JWT_SECRET;
const decoded = jwt.verify(token, secret);
res.json(decoded);
} catch (err) {
// ...
}
});- React Router 學習歷程
- 以
RouteObject.lazy拆分 bundle - 以
fetcher.Form處理無需跳轉的表單
- 以
// packages/client/src/routes/index.jsx
export default [
// ...
{
path: ":category",
lazy: async () => {
const { Category } = await import("./dashboard");
return {
element: <Category />,
};
},
// ...
},
];// packages/client/src/components/post.jsx
function Reactions({ reactions, title }) {
const user = useContext(Auth);
const fetcher = useFetcher();
return [
// ...
].map(({ icons, isIconButton, value, ...props }) => {
// ...
return (
<fetcher.Form method="put">
<input hidden name="title" defaultValue={title} />
<input hidden name="username" defaultValue={user.name} />
<input hidden name="reaction" defaultValue={value} />
{/* ... */}
</fetcher.Form>
);
});
}- Testing 學習歷程
- 以
vitest測試
- 以
// packages/client/test/routes/dashboard/children/category.test.jsx
describe("Post", () => {
it("changes icons and numbers if reacted", async () => {
vi.mock("localforage");
await setup(
// ...
async (user) =>
user.click(
await screen.findByRole("button", { name: /117/i }),
),
);
expect(
await screen.findByRole("button", { name: /118/i }),
).toContainElement(screen.getByTestId("EmojiEmotionsIcon"));
});
});- 以
msw模擬後端回應



