thumbnails.png

前言

大家好,我是渡邊仁(@Sicut_study)。

過去我開發了各種基於 React 的應用程式,並透過 JISOU 教導了許多工程師。雖然現在在技術上已經有相當的了解,但我並不是一開始就能夠做到這些。

尤其是「我變得對 React 非常了解了」「技術能力提升了」「感受到未來的可能性」的瞬間,無疑是可以建立 CI/CD 管道的時刻。
在沒有經驗的情況下成為一名工程師,對 CI/CD 的理解幾乎是必須的。

然而,許多人甚至不僅僅是沒有 CI/CD 的經驗,更無法深入理解 CI/CD。
實際上,在我的社群中也曾有成員發表過類似的帖子。

image.png

我在剛開始嘗試 CI/CD 的時候也曾有類似的想法,但至今似乎沒有一個網站能夠從基礎開始清楚地解釋 CI/CD,並包含 CI(測試)CD(部署) 的具體教程。

技術不同,所使用的管道環境也不同。
因此,大多數文章只能對 CI/CD 有一個籠統的概念。

這次我製作了一個即使是初學者也能輕鬆理解的 CI/CD 教程,並實際從零開始構建,讓您感受到其驚人的魅力。
如果您從未有過建立的經驗,這個教程或許將成為您工程師生涯的重大轉折點。

希望您能在學習現代開發(DevOps)的同時享受過程。
完成本教程後,您將能在手機上操作這個 Todo 應用。

名稱未設定的設計 (2).gif

也提供了視頻教學

此教材還附有詳細解說的視頻。如果有任何文字教材中無法理解的部分,也可以活用視頻。

<iframe></iframe>

目標對象

  • 想要開始學習 React 的人
  • 想要發布自己製作的網站的人
  • 想了解現代開發方式的人
  • 了解 JavaScript 基本概念的人
  • 希望了解自動化測試實踐的人

這個教程大約需要2小時,前提是您熟悉 HTML/CSS/JavaScript。

什麼是 CI/CD?

CI/CD是指,
CI(持續整合)
CD(持續部署/交付)

是一種自動化軟體開發中建構、測試及部署流程的技術。

image.png

開發者在寫完代碼後,會在一定的任務完成單位內將代碼提交推送到 GitHub 等庫上。這時,GitHub 上的管道(工作流)會啟動並執行以下步驟。

CI(持續整合)

進行代碼建構。也就是將 React 代碼翻譯成機器可讀的零和一的值。
如果其中有語法錯誤等問題,建構會失敗,管道也會停止運行。

接著,我們將執行之前撰寫好的測試針對建構的代碼。
測試將確保應用的正確運作(例如,新增待辦事項時會顯示增加一項等)。

由於測試詳盡地寫明了應用需求,因此如果測試失敗則表示需求未被滿足,可以判定為有錯誤的應用。

若是建構和自動測試都通過,則可判定提交的代碼是可以進入生產環境的代碼。
這樣便為 CD(部署)做好了準備。

前提是測試必須是全面的。
如果有未測試的部分,代碼變更可能會導致錯誤發生。
若自動測試全面,則通過自動測試代表滿足所有需求的無錯誤代碼。

CD(持續交付)

CI 準備好的代碼將自動部署,這就是持續交付。
雖然部署往往是手動執行的,但手動的話可能需要手冊或耗時。

透過 CD,人人只需提交代碼至庫便可進行部署。

沒有 CI/CD 的困難

你在工作場合有見過這樣的狀況嗎?或是聽過?

image.png

這家公司每半年進行一次全公司的大型發佈。
由於只每半年一次發布,必須拿出上一任的發布手冊來手動進行發布作業。不過在這半年期間,基礎設施可能發生變更,或引入了新工具,使得手冊中的七成資訊已經過時。

「嘿?這台伺服器不再使用了吧?」
「這個命令出錯了...」
「那,這個設定原本是...」

發布當天從凌晨2點開始進行。超過10名工程師聚集在會議室,對著手冊與現實之間的差距苦苦應對。期間發現「這個設定檔的路徑竟然是開發環境的!」這樣的致命問題,因此得從頭來過。

儘管勉強在早上6點完成了發布,但接下來測試團隊的手動測試隨之開始,半年來新增的功能與修正一次性地發布,測試項目龐大。5名測試人員分工合作,手動將測試結果記錄在 Excel 表格中。

接著命運的上午10點。測試團隊傳來消息:「基本的登錄功能出現錯誤」「商品搜尋無法運作」「支付處理出現中斷」等等報告。
這些都是致命的錯誤,因此決定緊急停止發布。但是,回滾的手冊同樣是半年前的,與目前環境不匹配,回滾的過程也非常艱難。

最終,服務中止了一整天,客戶的詢問不斷,業務團隊忙於道歉應對。開發團隊徹夜進行錯誤修復,三天後才終於再次發布。

經過半年開發的功能,卻花了一整星期才穩定運行。
「這次一定能順利進行...」
所有人都這樣想,也開始了下一個半年的開發。

CI/CD 的優勢

image.png

如果這家公司導入 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 之後可獲得眾多優勢。

這次使用的技術棧

image.png

首先將開發一個基於 React 的 Todo 應用。
接著使用 Vitest 為這個 Todo 應用撰寫自動測試。

然後將其手動部署到 Firebase Hosting 以確認部署成功,之後使用 GitHub Actions 建立自動進行建構 -> 測試 -> 部署的管道。

1. 環境建置

首先準備 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 的原因有以下三個。

image.png

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

image.png

當出現此畫面時,表示 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";

至此設定完成。您可在後面的樣式檢查是否已正確應用,如果樣式未生效,請檢查設定。

2. 開發 TODO 應用

首先開發 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 被輸出。

image.png

現在將所有 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;

image.png

在 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,因此並不困難。

image.png

但這樣無法完成新增 TODO 的功能。
我們應該使 TODO 狀態 可控。

狀態(state)是用來管理畫面狀態的工具。
例如「目前有多少個待辦」「輸入框輸入了什麼」等變化資訊的存儲位置。相較於普通的 JavaScript 變數,當狀態改變時,React 會自動更新畫面

image.png

接下來我們使用狀態來進行改寫:

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 這個函數。

image.png

除了 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(即輸入框中輸入的內容)更新到狀態中。
因此當實際在畫面上打字時,那個值會顯示出來,這就是「狀態更新會造成畫面更新」。

image.png

同樣地,我們希望 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


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬6   ❤️6
440
🥈
我愛JS
📝1   💬5   ❤️4
93
🥉
AppleLily
📝1   💬4   ❤️1
54
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次