Yuanlin Lin

Blog

程式也會要「讀空氣」:從零開始認識環境變數

Yuanlin Lin 林沅霖

2024-02-15

在軟體開發的過程中,我們經常遇到一個情況是程式在執行的時候,需要根據不同的「場合」做出不同的行為,就好像我們在處理人際關係的時候,需要「讀空氣」來根據當下的環境和氣氛做出不同的行為。

這種時候,環境變數是一個非常實用且非常通用的解決方案,可以幫助我們的程式更加靈活的運用在不同的場景。

舉個例子:Line 機器人

比如說我開發了一個 Line 機器人秘書的程式,這個程式只要執行了以後,他就會每天早上固定的時間發訊息提醒我今天要做哪些事情。我的朋友聽了以後覺得這個程式好實用,他也想要一個 Line 機器人秘書,所以我就把這個程式傳給他。

他收到了這個程式以後,在自己的電腦上啟動了這個機器人,沒想到之後的每天早上,他的機器人都跑來通知我他今天要做什麼 ...

在這個例子中,很明顯問題是出在了我把「機器人要傳訊息給誰」的這個資料寫死在程式碼裡面了:

const notifyUserID = 'yuanlin-line-id';

因為我直接這樣寫,導致當我把這個程式傳給我朋友以後,雖然是在他的電腦上執行,但程式裡面寫的是我的 ID,所以他的機器人仍然跑來通知我了 🤣 所以我又要打開編輯器幫他改成:

const notifyUserID = 'my-friend-line-id';

有沒有一種方法,可以讓我不用一直改程式碼裡面的 notifyUserID 的值,而是直接讓啟動程式的人自己決定呢?

另一個例子:正式環境 vs 測試環境

前面的 Line 機器人的例子其實只是為了讓剛入門的同學可以更生動的理解這個場景,事實上更常見的情況是在一個後端服務的「正式環境」和「測試環境」之間的切換。

比如說我們公司開發了一個電商網站,然後準備推出一個新功能,這個功能如果直接上線的話,要是發生了 bug 將會影響到我們網站所有的客人!為了避免這樣的悲劇發生,我們可以直接把整個網站複製一套,原本的給客人用,新的這一套拿來我們內部員工自己試用,這就是所謂的「測試環境」。

但這時候發現了一個問題,如果測試環境和正式環境共用同一個資料庫的話,那麼我們的員工在測試環境裡面買東西,客人就會發現商品真的被買走了,但事實上我們只是在測試,沒有真的要買東西。

也就是說,我們希望同一個程式,他在「測試環境」的時候可以連線到「測試資料庫」,而在「正式環境」的時候,可以連線到「正式資料庫」。

如果我們原本的程式碼是這樣寫:

const dbHost = 'production-db.xxxxxx.com'; const dbPort = 3306; const dbUser = 'root'; const dbPass = 'my-password'

那不管我們在什麼環境執行它,他都永遠只會連線到正式資料庫 ... 是不是和前一個例子的情況很類似呢? 我們希望找到一個方法來在不需要一直改程式碼的情況下,讓執行程式的人(或環境)來決定這些變數的值是什麼。

環境變數

前面舉了兩個例子,相信大家已經體會到環境變數究竟想要解決什麼問題了吧!在前面兩個例子中,我們其實是用「寫死(Hard Coding)」的方式把程式需要的參數直接寫死在程式裡面了,這就導致如果我們想改他就一定要重新改寫程式碼才行。

但如果我們把程式這樣改寫:

const notifyUserID = process.env.NOTIFY_USER_ID;

然後在啟動程式的時候,稍微加一點變化:

# 本來是這樣啟動 npm run start # 改成這樣 NOTIFY_USER_ID=yuanlin-line-id npm run start

我們就順利把 notifyUserID 這個變數變成一個由「環境變數」決定的參數啦!所以如果我朋友來和我說他也想要一個 Line 機器人,我不需要改任何程式碼,直接原封不動把程式丟給他,然後叫他在啟動的時候帶上自己的 ID 就好了:

NOTIFY_USER_ID=my-friend-line-id npm run start

這樣他的機器人就可以通知他,而不是跑來通知我了 👍

小試身手

如果你是剛入門開發的新同學,這是你第一次瞭解到「環境變數」這個概念的話,我建議你先停下來做一些實驗,親自體驗一下環境變數的運作方式哦!

// index.js const name = process.env.NAME console.log(`Hello, ${name}!`)

然後執行:

# 把 yuanlin 換成你自己的名字 NAME=yuanlin node index.js

如果你不是寫 Node.js 的同學也不用擔心,因為每個語言都有環境變數可以用,只是語法不太一樣而已:

Python

# index.py import os name = os.getenv('NAME') print(f"Hello, {name}!")

然後執行:

# 把 yuanlin 換成你自己的名字 NAME=yuanlin python index.py

Go

// index.go package main import ( "fmt" "os" ) func main() { name := os.Getenv("NAME") fmt.Printf("Hello, %s!\n", name) }

然後先編譯再執行:

# 編譯 go build index.go # 把 yuanlin 換成你自己的名字 NAME=yuanlin ./index

Ruby

# index.rb name = ENV['NAME'] puts "Hello, #{name}!"

然後執行:

# 把 yuanlin 換成你自己的名字 NAME=yuanlin ruby index.rb

Rust

// index.rs use std::env; fn main() { let name = env::var("NAME").unwrap_or("".to_string()); println!("Hello, {}!", name); }

然後先編譯再執行:

# 編譯 rustc index.rs # 把 yuanlin 換成你自己的名字 NAME=yuanlin ./index

PHP

<?php // index.php $name = getenv('NAME'); echo "Hello, {$name}!\n";

然後執行:

# 把 yuanlin 換成你自己的名字 NAME=yuanlin php index.php

注入環境變數的其他方法

在前面的這些示範,我們是直接在啟動程式的指令前面直接指定了這個程式需要的環境變數,這個作法叫做 Environment Variable Prefixing(環境變數前綴),他的特色是很輕便,但缺點也顯而易見,例如我們一開始提到的第二個例子:

DB_HOST=production-db.xxxxxx.com DB_PORT=3306 DB_USER=root DB_PASS=my-password npm run start

隨著我們程式需要的環境變數越來越多,我們的啟動指令也越來越長,這是一個不好的事情!

為了解決這個問題,我們可以把環境變數寫進一個叫做 .env 的檔案裡面:

# .env DB_HOST=production-db.xxxxxx.com DB_PORT=3306 DB_USER=root DB_PASS=my-password

然後在我們的程式裡面安裝像 dotenv 這樣的工具,讓他去讀取我們的 .env 檔案,然後再把讀取出來的資料寫入到環境中:

import 'dotenv/config' console.log(process.env.DB_HOST) // production-db.xxxxxx.com

但用這個方法的時候,請一定要小心:千萬不要把 .env 檔案一起丟上 GitHub 了!

這個是許多新手必踩的坑之一,通常環境變數裡面除了一些設定之外,還會有很多不可外洩的機密資料,為了避免這些機密外流,我們一定要記得把 .env 加到 .gitignore 檔案裡面,讓 git 記得不要把 .env 上傳到 GitHub 哦!

note:你知道嗎?其實從 Node.js 20.6 版本開始,你不需要為了載入 .env 檔案額外安裝 dotenv 套件了!快來我的 這篇 Instagram 文章看看吧

打破迷思:環境變數和 .env 不一樣!

很多人在跟著網路上的教學影片或線上課程學習的時候,老師並沒有花太多的時間解釋環境變數的概念,而是直接叫同學開一個 .env 檔案然後安裝 dotenv 套件。

這個學習方式導致了許多人誤以為 .env 就是所謂的環境變數,這個錯誤的觀念會導致後面你要學習其他更複雜的部署方式(例如 DockerKubernetes 或是使用 ZeaburVercel 這種 PaaS 平台)的時候腦袋轉不過來。

.env 檔案不是環境變數!

.env 檔案不是環境變數!

.env 檔案不是環境變數!

很重要所以講三次。從這篇文章的介紹以後,相信你已經理解「環境變數」事實上是 不存在於檔案 中的,而是一個存在於環境之間的變數,.env 檔案只是為了輔助我們在本地開發的時候,可以有一個方便的方式來一次設定大量的環境變數。

當我們把服務部署上線以後,就已經沒有 .env 檔案什麼事情了。比如說如果你是用 Kubernetes 來部署服務,環境變數會寫在容器的 env 欄位中:

apiVersion: v1 kind: Pod metadata: name: my-app spec: containers: - name: my-app image: gcr.io/google-samples/node-hello:1.0 env: - name: NAME value: "yualin"

或是如果你把服務部署到 Zeabur 這類的 PaaS 平台,也會有一個 GUI 介面讓你可以設定環境變數:

https://zeabur.com/docs/zh-TW/deploy/variables

所以說,千萬不要再把 .env 檔案當成環境變數了!.env 檔案只是本地開發用的工具,不應該上傳到 GitHub,也不會出現在部署後的服務中。

同場加映:前端有環境變數嗎?

如果你是剛學習前端的同學,看完這篇文章後可能會讓你頭有點痛,因為上面大部分介紹的內容都是在介紹一個運行在電腦或是伺服器的「程式」(例如後端服務)才能用的環境變數。

而大部分的「純前端」應用程式,他的 JavaScript 會被 Webpack 或 Rollup 這些打包工具包起來,這些程式真正「運行」起來的地點是在打開網頁的使用者他的 瀏覽器 裡面!所以你如果嘗試在一個純前端的程式試圖讀取 process.env.XXX 的話,會看到瀏覽器的控制台發生錯誤 process is undefined ,因為瀏覽器裡面根本沒有 process 這個東西。

note:這裡指的「純」前端應用程式指的是像 Vite 這種框架開發的專案,他們會被打包成靜態檔案放進 web server。而 Next.js 或 Remix 這種帶 SSR 功能的框架實際上可以理解為前後端都照顧到了,所以是可以在後端的部分讀取環境變數的哦!

所以在前端專案中,真正能用到環境變數的部分其實是在 build 的階段,因為 build 的過程是在 Node.js 的環境下進行的,他會把這個階段讀取到的環境變數用 hard coding(寫死)的方式寫入到最後打包出來的靜態產物中。

這也就意味著,如果你在打包時指定的是測試環境的環境變數,那這包靜態網站不管用什麼方法部署到哪裡,他打開的內容就都是測試環境的內容哦,沒辦法改變了。

但其實有一個辦法可以在前端程式中做到類似環境變數的功能,那就是使用 window.location.hostname 來根據目前使用者瀏覽器的網域判斷目前是哪個環境。

結論

希望這篇文章能夠讓大家對環境變數這個概念有更多的瞭解,如果你是第一次聽到環境變數這個概念的新同學,可以開始回顧一下自己寫過的程式有哪些地方可以改用環境變數的方式來優化。如果你是之前就已經在使用環境變數的同學,也可以重新檢查一下自己對 .env 檔案的理解和用法是否正確。

環境變數本身可以說是程式開發中不可獲缺的一個關鍵功能,這篇文章用了一個機器人和一個資料庫連線的範例簡單的解釋了他的用途,但實際上他還有非常多可以應用在專案各個地方,讓程式變得更加完善、更加安全且更加強大,歡迎你在下方留言和大家分享你的看法 👍

分享你的看法

暫無留言,你可以成為第一個留言的人!

author-avatar

關於作者

Yuanlin Lin 林沅霖

台灣桃園人,目前就讀浙江大學,主修計算機科學與技術,同時兼職外包全端開發工程師,熱愛產品設計與軟體開發。