NCU+ Frontend開發筆記

前言

1
NCU+ 是一個學生自主開發的網站平台,希望在未來能夠取代學校在臉書版上所有不好用的交流平台。藉由新平台的建立,創造更優質的校園資訊分享與討論版。

計畫的源起是我在翻交大的課程資料時,發現了NCTU+,在課程資料分享上的功能非常齊全,相較之下,中央校園內使用的臉書討論版無論是在功能或是資訊搜尋方面都遠遠不及,為了改善這個問題,我決定開始著手建立一個類似的網站。

這次的文章會分成幾個段落,把不同的 tech stack 拆開,在看文章的時候會比較好找到想讀的內容。

正文

TypeScript

簡介

這次無論是前端和後端都是用 TypeScript 撰寫,前端採用的是 Vite(Vue3 with TypeScript),後端則是用 Nest.js。

TS 比起 JS 多了型別檢查,在寫 code 的時候常常可以檢查出一些常見錯誤,增快我們的開發效率。當專案變大時,型別宣告也能幫助我們理解程式碼。除此之外,擴增的封裝、介面、抽象等特性讓我們在設計程式碼的時候更加靈活。

寫過以後,我覺得 TS 是 JS 的無痛升級版

tsconfig.json

tsconfig 是 typescript 的設定檔,TypeScript 的 compiler 會自動讀取裡面的設定。

比較常用的設定

  • compilerOptions
    • target
      • 編譯的目標 es 版本
    • module
      • module 的編譯方式,調整這個可以對應到不同平台的 module 宣告
    • moduleResolution
      • module 的解析方式
    • resolveJsonModule
      • 是否可以解析 json 檔,沒開的話就不能 import json 了
    • esModuleInterop
      • 參考這篇文
      • 基本上是 import default 產生的問題,如果 import 出問題的話就開吧
    • types
      • 指示 TypeScript 要去哪邊抓型別檔案來用
    • paths
      • 可以改寫 import 路徑 (path remapping)
    • skipLibCheck
      • 在編譯時跳過型別聲明文件的型別檢查,可以加速編譯,偶爾有一些型別文件的錯誤也可以用這個方法規避
  • include
    • 要包含在程式裡面的檔案
  • exclude
    • 有被 include 描述到但是要跳過的檔案

ex.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client", "@types/jest", "@types/node"],
"paths": {
"@/*": ["./src/*"]
},
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist"]
}

官方的 tsconfig 文件

型別聲明/轉型

型別聲明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 自動抓型別
let a = 1; // number
// 手動宣告
let b: number = 1;
// 型別錯誤
let c: number;
c = "1"; // error

// array型別宣告
let d: number[] = [1, 2, 3];
// 其他寫法
let e: Array<number> = [1, 2, 3];

// object型別宣告
let obj: { name: string; age: number } = { name: "John", age: 30 };

// 常見用例
let f: string[] = [];
f.push("item"); // ok
f.push(1); // error

// 型別聯集
let g: string | number = 1;
g = "1"; // ok

值得注意的是,假設一個 function 接受string型別的參數,如果我們要塞string | null的型別進去會是不合法的。

轉型

1
2
3
4
5
// 兩種寫法
// as
a as string;
// 用<>刮起來
<string>a;

雖然能轉型但不一定跑起來會正確,有可能跑起來以後因為錯誤的型別跳 error。除此之外,如果 TypeScript 不讓我們轉型,也可以用下面的方法強制轉。

1
a as unknown as string;

要謹慎使用。

interface / class

TypeScript 的 interface 和其他語言的不太一樣,比較像是用來定義資料型別的,compile 過後基本會直接不見,但會留下型別宣告檔。

1
2
3
4
interface Person {
name: string;
age: number;
}

class 多了修飾子,可以達成封裝。

1
2
3
4
5
6
7
8
9
10
class Person {
private name: string;
private age: number;
public getName(): string {
return this.name;
}
public getAge(): number {
return this.age;
}
}

另外還有抽象類別和方法可以用,這邊不做介紹了。

Vue3 Composition API with SFC

由於 Vite(Vue3 + TypeScript)的專案已經是用 SFC 了,此外還有用到<script setup>,讓我一開始摸不著頭續。經過了解之後,發現這是 Vue3 加的新東西,可以讓我們寫 SFC 的時候更乾淨,下面來簡單介紹一下。

setup() vs <script setup>?

直接說結論,setup()是 composition api 的進入點,後者也差不多,不過可以省略掉前者寫法的一大段,如下面的例子

setup()

1
2
3
4
5
6
7
8
9
import { ref } from "vue";
export default {
setup(props, context) {
const data = ref("data can be used in template");
return {
data,
};
},
};

<script setup>

1
2
3
import { ref } from "vue";

const data = ref("data can be used in template");

變得非常整潔,下面會再說明 props 和 emit 等用法,其他剩下的部分就到官方文件上面翻吧。

props / emits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const props = defineProps<{ name: string; age: number }>();
const emits = defineEmits<{
(event: "custom-event"): void;
(event: "another-event", data: number): void;
}>();

// emit costom-event
emits("custom-event");
// with data
emits("another-event", 123);

// use props
console.log(props.name);
console.log(props.age);

這些帶有型別宣告的 emit 是 TypeScript 才支援的

Vite

簡介

Vite 是新的前端開發工具,支援 React 和 Vue 等主流框架,不過沒有支援 Angular。最核心的特色是快還要更快,在開發的時候,啟動和 hot reload 的速度屌打使用其他打包方式的工具,用過都說讚。能這麼快的原因是在開發過程中,Vite 用 esbuild 來做打包,esbuild 本身就比其他打包工具快了不知道幾倍,最後的結果就是完美的開發體驗。

坑點

  • Vite 是一個前端開發工具,而不是前端框架,在專案開發時,專案架構/測試框架那些都要自己手動搞定,不太適合用在大型網站開發。
  • 雖然官方文檔寫支援 SSR,但是實際上支援度不太好,還要自己搞定很多設定,而且還可能踩到很多 bug。

Nuxt3 beta 現在有實驗性的 Vite 打包支援,改天我再去玩玩看

踩坑實錄

Vite 環境變數在配合 jest 測試時錯誤

Vite 的環境變數可以在程式中用import.meta.env來取得,但 jest 再跑測試的時候跑到import.meta就會出錯。

最後找到的 dirty solution

vite.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import dotenv from "dotenv";

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
if (mode === "production") {
dotenv.config({ path: ".env.production.local" });
return {
define: {
"process.env.VITE_APP_API_URL": `"${process.env.VITE_APP_API_URL}"`,
},
plugins: [vue()],
resolve: {
alias: { "@/": `${path.resolve(__dirname, "src")}/` },
},
};
} else {
dotenv.config({ path: ".env.local" });
return {
define: {
"process.env.VITE_APP_API_URL": `"${process.env.VITE_APP_API_URL}"`,
},
plugins: [vue()],
resolve: {
alias: { "@/": `${path.resolve(__dirname, "src")}/` },
},
};
}
});

寫成process.env再用 Vite 來把字串取代掉...

結語

寫了這麼多天,主要就在這些東西上打轉,另外有摸了一下 GitHub Actions,之後會再另外寫一篇小文章紀錄關於 GitHub Actions 和 Unit Test 的一些觀念。