大家好,我是渡邊仁(@Sicut_study)。
過去我開發了各種基於 React 的應用程式,並透過 JISOU 教導了許多工程師。雖然現在在技術上已經有相當的了解,但我並不是一開始就能夠做到這些。
尤其是「我變得對 React 非常了解了」「技術能力提升了」「感受到未來的可能性」的瞬間,無疑是可以建立 CI/CD 管道的時刻。
在沒有經驗的情況下成為一名工程師,對 CI/CD 的理解幾乎是必須的。
然而,許多人甚至不僅僅是沒有 CI/CD 的經驗,更無法深入理解 CI/CD。
實際上,在我的社群中也曾有成員發表過類似的帖子。
我在剛開始嘗試 CI/CD 的時候也曾有類似的想法,但至今似乎沒有一個網站能夠從基礎開始清楚地解釋 CI/CD,並包含 CI(測試) 和 CD(部署) 的具體教程。
技術不同,所使用的管道環境也不同。
因此,大多數文章只能對 CI/CD 有一個籠統的概念。
這次我製作了一個即使是初學者也能輕鬆理解的 CI/CD 教程,並實際從零開始構建,讓您感受到其驚人的魅力。
如果您從未有過建立的經驗,這個教程或許將成為您工程師生涯的重大轉折點。
希望您能在學習現代開發(DevOps)的同時享受過程。
完成本教程後,您將能在手機上操作這個 Todo 應用。
此教材還附有詳細解說的視頻。如果有任何文字教材中無法理解的部分,也可以活用視頻。
<iframe></iframe>
這個教程大約需要2小時,前提是您熟悉 HTML/CSS/JavaScript。
CI/CD是指,
CI(持續整合)
CD(持續部署/交付)
是一種自動化軟體開發中建構、測試及部署流程的技術。
開發者在寫完代碼後,會在一定的任務完成單位內將代碼提交推送到 GitHub 等庫上。這時,GitHub 上的管道(工作流)會啟動並執行以下步驟。
進行代碼建構。也就是將 React 代碼翻譯成機器可讀的零和一的值。
如果其中有語法錯誤等問題,建構會失敗,管道也會停止運行。
接著,我們將執行之前撰寫好的測試針對建構的代碼。
測試將確保應用的正確運作(例如,新增待辦事項時會顯示增加一項等)。
由於測試詳盡地寫明了應用需求,因此如果測試失敗則表示需求未被滿足,可以判定為有錯誤的應用。
若是建構和自動測試都通過,則可判定提交的代碼是可以進入生產環境的代碼。
這樣便為 CD(部署)做好了準備。
前提是測試必須是全面的。
如果有未測試的部分,代碼變更可能會導致錯誤發生。
若自動測試全面,則通過自動測試代表滿足所有需求的無錯誤代碼。
CI 準備好的代碼將自動部署,這就是持續交付。
雖然部署往往是手動執行的,但手動的話可能需要手冊或耗時。
透過 CD,人人只需提交代碼至庫便可進行部署。
你在工作場合有見過這樣的狀況嗎?或是聽過?
這家公司每半年進行一次全公司的大型發佈。
由於只每半年一次發布,必須拿出上一任的發布手冊來手動進行發布作業。不過在這半年期間,基礎設施可能發生變更,或引入了新工具,使得手冊中的七成資訊已經過時。
「嘿?這台伺服器不再使用了吧?」
「這個命令出錯了...」
「那,這個設定原本是...」
發布當天從凌晨2點開始進行。超過10名工程師聚集在會議室,對著手冊與現實之間的差距苦苦應對。期間發現「這個設定檔的路徑竟然是開發環境的!」這樣的致命問題,因此得從頭來過。
儘管勉強在早上6點完成了發布,但接下來測試團隊的手動測試隨之開始,半年來新增的功能與修正一次性地發布,測試項目龐大。5名測試人員分工合作,手動將測試結果記錄在 Excel 表格中。
接著命運的上午10點。測試團隊傳來消息:「基本的登錄功能出現錯誤」「商品搜尋無法運作」「支付處理出現中斷」等等報告。
這些都是致命的錯誤,因此決定緊急停止發布。但是,回滾的手冊同樣是半年前的,與目前環境不匹配,回滾的過程也非常艱難。
最終,服務中止了一整天,客戶的詢問不斷,業務團隊忙於道歉應對。開發團隊徹夜進行錯誤修復,三天後才終於再次發布。
經過半年開發的功能,卻花了一整星期才穩定運行。
「這次一定能順利進行...」
所有人都這樣想,也開始了下一個半年的開發。
如果這家公司導入 CI/CD,或許會避免這樣的情況。
Before: 半年前的手冊七成過時且無法使用
After: 所有步驟均已編碼,每次自動執行,始終保持最新
手動手冊需要人撰寫並由人執行,每當環境變更都需要更新。CI/CD 則是將部署步驟作為代碼管理,環境變更會自動檢測並反映。
Before: 從凌晨2點開始,10人進行長達6小時的工作
After: 只需按下一個按鈕,約15分鐘即可自動部署
手動發布需要確保「能夠應對任何突發狀況的人員」,因此通常需要大量人力。通過自動化,在工作時間內只需1人按下按鈕即可完成發布。
Before: 半年功能集中測試才發現致命錯誤
After: 每次小改動都執行自動測試,即時發現問題
這家公司在開發半年後才進行測試,因此難以確定錯誤的根源。而 CI/CD 則是每撰寫一段代碼便會執行自動測試,能清晰判斷是哪些更改導致問題的發生。
Before: 全公司參與的大賭注發布
After: 小規模頻繁發布,降低每次發布的風險
半年內的所有變更一次性發布,就如同背著一個巨大的炸彈。CI/CD 每周或每日進行發布,因此若發生問題,影響範圍將會相對有限。
Before: 回滾手冊過時,難以恢復
After: 一鍵即可立即返回舊版本
手動環境下,「恢復的操作」一般非常繁瑣。而在 CI/CD 中,所有的舊版本都被管理,遇到問題時可以在幾分鐘內恢復到原狀。
Before: 整整一天下來服務中斷,客戶的抱怨頻繁
After: 即使出現問題,也能在幾分鐘內恢復,幾乎不會讓客戶察覺
若公司長時間中止服務,將可能導致客戶流失與信任下降的風險。CI/CD 透過快速恢復,能將業務影響降到最低。
Before: 半年因為「這次一定能順利進行」的焦慮
After: 每次都能放心發布的內心平靜
每半年一次的大規模發布,對開發者來說是巨大壓力。CI/CD 的導入,使得發布成為日常任務,精神負擔大幅減輕。
Before: 剛開發的功能積壓半年後再發布
After: 開發完成後立即為用戶提供價值
在這家公司,半年內用戶無法使用新功能。導入 CI/CD 後,能迅速將新功能推向市場,確保商業優勢。
這樣導入 CI/CD 之後可獲得眾多優勢。
首先將開發一個基於 React 的 Todo 應用。
接著使用 Vitest 為這個 Todo 應用撰寫自動測試。
然後將其手動部署到 Firebase Hosting 以確認部署成功,之後使用 GitHub Actions 建立自動進行建構 -> 測試 -> 部署的管道。
首先準備 Node.js 環境。
Node.js 是執行 JavaScript 的開發環境,是一種允許在電腦上直接執行 JavaScript 的程式。
由於 React 是 JavaScript 的一個程式庫,因此需要有能執行 JavaScript 的環境。
您可以從以下網站進行安裝。
<iframe id="qiita-embed-content__f83f16db9b398923a262a0bb5d911d4a"></iframe>
如果不清楚如何安裝,可以在 Qiita 查詢安裝方法,會有很多文章介紹,因此在這裡不贅述。
安裝完成後確認是否成功安裝:
node -v
v22.04
若沒有出現錯誤,即表示 Node.js 已正確安裝並可以使用。
接下來使用 Vite
來構建 React 環境。
Vite 是一款最新的前端開發工具,特別是提供高速的開發環境的建構工具。使用 Vite 開發 React 的原因有以下三個。
React 是使 JavaScript 更易於書寫的程式庫,因此最終需要將其轉換為純 JavaScript。透過 Vite,我們可以將 React 翻譯為純 JavaScript。
環境建置可以參考官方網站,會相當簡單。
<iframe id="qiita-embed-content__43045db20edc0cc48c358bc07db5e43a"></iframe>
npm create vite@latest
Ok to proceed? (y) y
> npx
> "create-vite"
│
◇ 專案名稱:
│ todo-cicd
│
◇ 選擇框架:
│ React
│
◇ 選擇變種:
│ TypeScript
│
cd todo-cicd
npm i
npm run dev
當伺服器啟動後,請訪問 http://localhost:5173。
當出現此畫面時,表示 React 環境已建置完成。
接著為了樣式設計,我們導入 TailwindCSS。
<iframe id="qiita-embed-content__06ee133797c01c931222d04a3e3c085c"></iframe>
透過使用 TailwindCSS,只需加入類別即可使用 CSS。
請按照文件指示進行安裝:
npm install tailwindcss @tailwindcss/vite
接下來修改 vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});
在 src/index.css 中編寫引入的代碼:
@import "tailwindcss";
至此設定完成。您可在後面的樣式檢查是否已正確應用,如果樣式未生效,請檢查設定。
首先開發 Todo 應用。
通過修改 src/App.tsx
進行畫面開發。
首先將「Vite + React」的部分修改為如下:
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
{/* 修正 */}
<h1>你好,世界</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
數量 {count}
</button>
<p>
編輯 <code>src/App.tsx</code> 來測試 HMR
</p>
</div>
<p className="read-the-docs">
點擊 Vite 和 React 的標誌了解更多
</p>
</>
);
}
export default App;
修改後,Vite 會自動監測到變更並在畫面上反映出來。
接下來,我們準備一些測試資料以顯示待辦清單。
React 允許將 JavaScript 和 HTML 同時書寫。請將現有代碼全部刪除並更改如下:
import "./App.css";
function App() {
// 這裡是 JavaScript 部分
const todos = [
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
];
console.log(todos);
return (
<>
<div>這裡是 HTML 部分</div>
</>
);
}
export default App;
打開畫面並通過「右鍵」->「檢查」開啟開發者工具,點擊「Console」應該可以看到 Todo
被輸出。
現在將所有 Todo 渲染於畫面中。
利用 React 中簡單地在 HTML 部分使用 JavaScript 的能力,我們可以使用 map 來顯示:
import "./App.css";
function App() {
const todos = [
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
];
return (
<>
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
</>
);
}
export default App;
在 HTML 中寫 { }
就可以使用 JavaScript。
{todos.map((todo) => (
// 這裡寫HTML
))}
透過 todos.map 我們為每一個 todo 創建 HTML,最終呈現的是一個 HTML(JSX)數組。
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
// [<div key={1}>待辦1</div>, <div key={2}>待辦2</div>, <div key={3}>待辦3</div>]
React 有一個機制,即「在數組中如果包含 JSX 元素,則將其順序呈現」。
接著我們將實現新增待辦事項的功能。
首先添加一個輸入框和按鈕:
import "./App.css";
function App() {
const todos = [
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
];
return (
<>
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
<input type="text" name="title" placeholder="新增待辦" />
<button type="submit">新增</button>
</div>
</>
);
}
export default App;
這只是編寫 HTML,因此並不困難。
但這樣無法完成新增 TODO 的功能。
我們應該使 TODO 狀態 可控。
狀態(state)是用來管理畫面狀態的工具。
例如「目前有多少個待辦」「輸入框輸入了什麼」等變化資訊的存儲位置。相較於普通的 JavaScript 變數,當狀態改變時,React 會自動更新畫面。
接下來我們使用狀態來進行改寫:
import { useState } from "react";
import "./App.css";
type Todo = {
id: number;
title: string;
completed: boolean;
};
function App() {
const [title, setTitle] = useState("");
const [todos, setTodos] = useState<Todo[]>([
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
]);
return (
<>
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="新增待辦"
/>
<button type="submit">新增</button>
</div>
</>
);
}
export default App;
為了使用狀態,我們使用 useState
這個函數。
除了 Todo,我們還新增了一個名為 title 的狀態:
const [title, setTitle] = useState("");
將 title 的值顯示在畫面中:
<div>{title}</div>
每當在輸入框中輸入時,屬性 onChange
的事件處理器將執行。
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="新增待辦"
/>
onChange
會在表單內容發生變化時執行以下函數:
(e) => setTitle(e.target.value)
我們將 e.target.value
(即輸入框中輸入的內容)更新到狀態中。
因此當實際在畫面上打字時,那個值會顯示出來,這就是「狀態更新會造成畫面更新」。
同樣地,我們希望 Todo 也能在新增後顯示於畫面,因此也需要使用狀態。
const [todos, setTodos] = useState<Todo[]>([
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
]);
useState()
中的數組作為狀態的初始值。
接下來我們實現當點擊按鈕時能夠新增值。
import { useState } from "react";
import "./App.css";
type Todo = {
id: number;
title: string;
completed: boolean;
};
function App() {
const [title, setTitle] = useState("");
const [todos, setTodos] = useState<Todo[]>([
{
id: 1,
title: "待辦 1",
completed: false,
},
{
id: 2,
title: "待辦 2",
completed: false,
},
{
id: 3,
title: "待辦 3",
completed: false,
},
]);
// 追加
const handleAddTodo = () => {
setTodos([
...todos,
{
id: todos.length + 1,
title: title,
completed: false,
},
]);
setTitle("");
};
return (
<>
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="新增待辦"
/>
<button type="submit" onClick={handleAddTodo}>
新增
</button>
</div>
</>
);
}
export default App;
當按下按鈕時, onClick
事件處理器中的函數(handleAddTodo)會被執行。
<button type="submit" onClick={handleAddTodo}>
新增
</button>
const handleAddTodo = () => {
setTodos([
...todos,
{
id: todos.length + 1,
title: title,
completed: false,
},
]);
setTitle("");
};
在 handleAddTodo 中我們使用了一種稍微特殊的寫法,因此進行細節說明。
setTodos([
...todos,
新的 TODO
]);
這個 ...todos
部分使用了展開運算符,展開了一個數組。
例如:
const todos = [
{ id: 1, title: "買菜" },
{ id: 2, title: "打掃" },
];
// 使用展開運算符的情況
const newTodos = [...todos, { id: 3, title: "洗衣服" }];
// 結果:
// [
// { id: 1, title: "買菜" },
// { id: 2, title: "打掃" },
// { id: 3, title: "洗衣服" }
// ]
這種寫法能將已存在的數組與新待辦項目合併成一個新的數組。接下來,我們的 TODO 應用便能夠成功新增待辦事項了。
原文出處:https://qiita.com/Sicut_study/items/9792f2f01dc887d4cb31