前一篇我們完成了 Express + Mongoose 的整合,接下來我們要來處理一些環境變數的問題,以及 CORS 的問題。
Cross-origin resource sharing?看不懂沒關係,我相信你應該知道 CORS 是什麼,假設你今天是一個前端工程師的話,你應該會很常看到這個錯誤訊息:
Access to XMLHttpRequest at 'http://localhost:3000/api/v1/users' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這個是什麼意思呢?簡單來講就是你目前的網域以及 Prot 與你要存取的網域不同,所以會被瀏覽器擋下來,這個時候你就需要在你的後端加上 CORS 的設定,讓瀏覽器知道你允許哪些網域可以存取你的 API。
那麼這麼又稱之為「跨來源資源共用」,什麼意思呢?簡單來講,當你從 A 網域存取 B 網域的資源時,就稱之為跨來源資源共用。
而這時候為了安全性考量,瀏覽器會擋下來,這時候你就需要在後端加上 CORS 的設定,讓瀏覽器知道你允許哪些網域可以存取你的 API,這樣子就沒有問題了。
(通常最常發生的時機點是 AJAX 的請求。)
那麼我們並不打算深入探討 CORS 的原理,我們基本上只需要知道幾個小重點
接下來,我們就要來替前一章節的 API 加上 CORS 的設定。
首先我們先回顧一下前一章節寫的範例程式碼
const express = require('express');
const mongoose = require('mongoose');
const app = express();
let connectStatus = false;
async function connectMongoDB () {
try {
await mongoose.connect('mongodb+srv://url')
console.log('Connected to MongoDB...')
connectStatus = true;
} catch (error) {
console.log(error)
}
}
connectMongoDB()
app.use(express.json());
app.use((req, res, next) => {
if (connectStatus) {
next();
} else {
res.status(503).send({
status: false,
message: 'Server is not ready'
});
}
})
const todoSchema = new mongoose.Schema({
id: Number,
title: String,
completed: Boolean,
},{
versionKey: false,
_id: false,
});
const Todo = mongoose.model('Todo', todoSchema);
app.get('/todos', async (req, res) => {
const todos = await Todo.find().select('-__v -_id');
res.send({
status: true,
data: todos,
});
});
app.post('/todos', async (req, res) => {
const { title, completed } = req.body;
const todo = new Todo({
id: new Date().getTime(),
title,
completed,
});
await todo.save();
res.send({
status: true,
message: 'Create todo successfully',
});
});
app.listen(3000)
那麼為什麼要加上 CORS 設定呢?主要原因是跟目前的主流開發模式有關,現在的主流開發模式是前後端分離,也就是說前端會獨立出來,後端也會獨立出來,這樣子的好處是可以讓前後端的開發團隊可以獨立開發,不會互相影響,彼此著重在自己的領域上就好。
因此為了解決這個問題,後端就必須要加上 CORS 的設定,讓前端可以存取後端的 API。
起手式很簡單,由於我們是使用 Express,所以我們可以使用 cors 這個套件來幫我們處理 CORS 的問題。
npm i cors
接著我們就可以在我們的程式碼中加上 CORS 的設定
const express = require('express');
// 加入 cors 套件
const cors = require('cors');
// ...略過其他程式碼
// 加入 cors 設定
app.use(cors());
// ...略過其他程式碼
app.listen(3000);
這樣子就完成了,很簡單對吧?那麼這時候你可能會想說...
「可是我在前面用 Postman 測試的時候,並沒有遇到 CORS 的問題啊?」
其實原因是因為 CORS 這個問題只會發生在瀏覽器上,而 Postman 本身是一套軟體,你也可以把它看成一個後端的程式,因此它不會有 CORS 的問題,這也是為什麼有時候我們在跟後端溝通時,用 Postman 溝通 API 時,都沒有問題,但是當我們把 API 串接到前端時,就會遇到 CORS 的問題。
但是上面的 CORS 設定並不是那麼的正確,因為上面的設定是允許所有網域存取你的 API
{
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}
Note
上面的意思是允許所有網域存取你的 API,並且允許的方法有GET,HEAD,PUT,PATCH,POST,DELETE
。
就會有安全性的問題,因此我們必須要限制存取的網域。
那麼我們可以透過 origin
這個參數來限制存取的網域,例如我們只允許 http://localhost:8080
這個網域存取我們的 API,那麼我們可以這樣子寫
const express = require('express');
// 加入 cors 套件
const cors = require('cors');
// ...略過其他程式碼
const corsOptions = {
origin: 'http://localhost:8080',
};
app.use(cors(corsOptions));
// ...略過其他程式碼
app.listen(3000);
Note
有些比較舊的瀏覽器必須額外加上optionsSuccessStatus: 200
才能正確運作,但是現在的瀏覽器都已經不需要了,所以這邊就不多做介紹。
corsOptions.origin
這個參數可以接受一個字串或是一個函式,如果是字串的話,就是指定一個網域,如果是函式的話,就可以自己定義邏輯,例如我們可以這樣子寫
const express = require('express');
// 加入 cors 套件
const cors = require('cors');
// ...略過其他程式碼
const whitelist = ['http://localhost:8080', 'http://localhost:8081'];
const corsOptions = {
origin(origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}
app.use(cors(corsOptions))
// ...略過其他程式碼
app.listen(3000);
這樣子就可以自己定義邏輯了,這邊我們定義了一個 whitelist
陣列,裡面放了兩個網域,然後我們在 corsOptions
中定義了一個 origin
函式,這個函式會接收兩個參數,分別是 origin
跟 callback
,origin
代表的是存取的網域,而 callback
則是一個回呼函式,我們可以透過這個回呼函式來決定是否允許存取。
Note
whitelist
意指白名單,也就是說我們只允許白名單中的網域存取我們的 API。
當然,如果你不想用 cors
套件的話,你也可以試著自己寫 Middleware 來處理 CORS 的問題
const express = require('express');
const app = express();
// ...略過其他程式碼
const whitelist = ['http://localhost:8080', 'http://localhost:8081'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (whitelist.indexOf(origin) !== -1) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
next();
} else {
res.status(403).send({
status: false,
message: 'Not allowed by CORS'
});
}
});
// ...略過其他程式碼
app.listen(3000);
只是我們還是比較常見用 cors
套件來處理 CORS 的問題,因為這樣子比較簡單。
Environment Variable 中文是「環境變數」,對於某些人來講,可能會無法知道環境變數的用途,但是對於有些人來講,環境變數是一個很重要的東西,因為環境變數可以讓我們的程式更加的彈性,也可以讓我們的程式更加的安全。
雖然我們前面介紹了不少東西,但是 Environment Variable 依然是一個很重要的東西,因此我們必須要學習如何使用 Environment Variable。
那麼環境變數主要是用來做什麼的呢?底下我也列出常見的用途
舉凡資料庫的密碼、金鑰、API 金鑰等等,這些資訊都是機敏資訊,我們不希望這些資訊被外人知道,因此我們可以透過環境變數來隱藏這些資訊。
如果你寫死在程式碼中的話,那麼一旦你的程式碼被外人取得,那麼你的資料庫就會被入侵,因此我們必須要把這些機敏資訊隱藏起來。
有時候我們某些程式碼是只能在特定環境下運作的,那麼我們就可以透過環境變數來切換環境,例如我們可以透過環境變數來切換資料庫,這樣子就可以確保我們測試的環境跟正式環境是不同的。
「什麼?!使用環境變數也可以增加可環護性?」
你沒有看錯,假設有一段程式碼是這樣子的
// a.js
axios.get('http://localhost:3000/api/v1/profile');
// b.js
axios.get('http://localhost:3000/api/v1/users');
// c.js
axios.get('http://localhost:3000/api/v1/posts');
你可以看到上面的程式碼都是存取 http://localhost:3000
這個網域,但是如果有一天我們要把網域改成 http://localhost:3001
的話,那麼我們就必須要把所有的程式碼都改一遍,因此透過環境變數可以讓我們的程式碼更加的彈性,也可以讓我們的程式碼更加的可維護。
Note
axios
是一個 HTTP 請求套件。
首先一開始我們要來安裝一個套件,也就是 dotenv 套件,這個套件可以讓我們在程式碼中使用環境變數。
npm i dotenv
接著我們要來建立一個 .env
檔案,這個檔案就是用來存放環境變數的地方,我們可以在這個檔案中定義環境變數,例如我們可以這樣子寫
PORT=3000
這樣子就定義了一個 PORT
的環境變數,接著我們要來修改我們的程式碼,讓我們的程式碼可以使用環境變數。
const express = require('express');
// 加入 dotenv 套件
require('dotenv').config();
const app = express();
// ...略過其他程式碼
app.listen(process.env.PORT);
這樣子就可以使用環境變數了,這邊我們使用了 process.env.PORT
這個環境變數,這個環境變數就是我們在 .env
檔案中定義的 PORT
環境變數。
Note
process.env
是 Node.js 中的一個全域變數,可以用來存放環境變數;環境變數的命名通常會以大寫字母為主,例如:PORT
、DB_URL
、API_KEY
等等。
那麼剛剛也有提到可以增加可維護性,那麼我們就來看看怎麼使用環境變數來增加可維護性
// a.js
axios.get('http://localhost:3000/api/v1/profile');
// b.js
axios.get('http://localhost:3000/api/v1/users');
// c.js
axios.get('http://localhost:3000/api/v1/posts');
我們可以看到上面的程式碼都是存取 http://localhost:3000
這個網域,但是如果有一天我們要把網域改成 http://localhost:3001
的話,那麼我們就必須要把所有的程式碼都改一遍,這時候我們就可以透過環境變數來解決這個問題,只需要在 .env
檔案中定義一個 API_URL
環境變數,然後在程式碼中使用這個環境變數就可以了。
API_URL=http://localhost:3000
// a.js
axios.get(`${process.env.API_URL}/api/v1/profile`);
// b.js
axios.get(`${process.env.API_URL}/api/v1/users`);
// c.js
axios.get(`${process.env.API_URL}/api/v1/posts`);
很輕鬆吧?這樣子就可以增加可維護性了。
那麼這邊也提一下,為什麼要使用 dotenv
套件來使用環境變數呢?因為 dotenv
套件可以讓我們在程式碼中使用環境變數,而且 dotenv
套件會自動幫我們讀取 .env
檔案,並且把 .env
檔案中的環境變數加到 process.env
中,這樣子我們就可以在程式碼中使用環境變數了。
因此這邊最後做個結論,Node.js 本身就內建 process.env
的環境變數物件,但 dotenv 套件提供了更好的方式來管理、組織和使用環境變數。
那麼這一篇就先到這邊結束,我們下一篇見哩。
其實 Env 官方後來有出一個 Dotenv Vault,我認為還滿不錯用的,詳情可以參考這裡唷~