昨天我們強化了後端專案結構,加入了services、daos的資料夾,這有助於切割商業邏輯,強化維護性。
觀察一下,我們將大部分的物件建立移到 app.js
中,方便控制物件相依
const echoDao = new EchoDao({mongoClient: client});
const mongoService = new MongoService({mongoClient: client, echoDao});
const {createRouter: createRootRouter} = require('./routes/index');
const indexRouter = createRootRouter({mongoService});
這延申出幾個問題:
我們要使用依賴注入套件awilix解決上面問題。
./configs/config.js
process.env
過程請見 github commit log ithelp-30dayfullstack-Day17 或 codesandbox
先問幾個問題:
大致上的流程是
asClass
, asFunction
, asValue
: 物件建立的方法,再用 container.register()
註冊在 container 中
container.register({
objName1: asClass(Class),
objName2: asFunction(factoryFunction),
objName3: asValue(value),
});
container.loadModules(globPatterns, options)
:loadModules()
會掃描資料夾,套用我們設定的「物件建立方法」resolve(name)
: 取出物件,這時才開始建立相依的物件所以任何地方只要有能存取 container,就可以透過 resolve(name)
取出物件。
npm install awilix --save
// app.js
const { createContainer, asClass, asValue, asFunction, Lifetime } = require('awilix');
// 建立 awilix container
const container = createContainer();
container.register({
mongoClient: asValue(client, { lifetime: Lifetime.SINGLETON }), // 註冊為 mongoClient,且生命期為 SINGLETON (執行中只有一個物件)
indexRouter: asFunction(createRootRouter, { lifetime: Lifetime.SINGLETON }), // 註冊為 indexRouter,利用工廠函數 createRootRouter 建立物件
});
// app.js
container.loadModules([
'daos/*.js',
'services/*.js',
], {
formatName: 'camelCase',
resolverOptions: {
lifetime: Lifetime.SINGLETON,
register: asClass
}
});
掃描到的檔案,因為是 module.exports = clsss
所以用 asClass
註冊。名稱命名規則為 camelCase ,生命期為 SINGLETON。例如:找到 MongoService
就好像用
// app.js
container.register({
mongoService: asClass(MongoService, { lifetime: Lifetime.SINGLETON }),
});
註冊// app.js
const indexRouter = container.resolve('indexRouter');
indexRouter 有指定用 createRootRouter(dependencies)
建立 (asFunction(createRootRouter)
),它會拿 const {mongoService} = dependencies
, mongoService 就會被 asClass(MongoService)
建立,所以會一直建立相關的物件
建立物件路徑:
createRootRouter({mongoService}) -> mongoService -> mongoClient and echoDao
|
∟ -> mongoClient
awilix 幫助我們從
關注「建立物件」和「串接物件關係」
變成
關注「物件建立方法」
關連性透過命名串接,相依物件自動建立。
程式中的 MongoDB 連線有寫死(hard code) 常數值
// daos/EchoDao.js
class EchoDao {
...略
async insert(data) {
const dbName = 'myproject';
const db = this.mongoClient.db(dbName);
...略
}
}
和
// app.js
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017';
const client = new MongoClient(url, { useNewUrlParser: true });
...略
接下來要將它們抽出到一個 config.js
檔案,方便未來修改
./configs/config.js
提出連線的常數
// configs/config.js
module.exports = {
mongodb: {
url: 'mongodb://localhost:27017',
dbName: 'myproject',
}
}
EchoDao
加入 config
相依EchoDao
加入 config
相依
// daos/EchoDao.js
class EchoDao {
/**
*
* @param {object} config
* @param {MongoClient} mongoClient
*/
constructor({ config, mongoClient }) {
this.config = config;
this.mongoClient = mongoClient;
}
...略
}
// app.js
const config = require('./configs/config');
container.register({
config: asValue(config, { lifetime: Lifetime.SINGLETON }),
...略
});
為了套用控制反轉(Inversion of Control, IoC),把連線過程放到 createMongoClient()
這工廠函數:
createMongoClient()
用來產生 MongoClient 物件
// app.js
/**
*
* @param {object} config
* @returns {MongoClient}
*/
function createMongoClient({config}) {
const url = config.mongodb.url;
const client = new MongoClient(url, { useNewUrlParser: true });
// 立即連線
client.connect()
.then((connectedClient) => {
console.log('mongodb is connected');
})
.catch(error => {
console.error(error);
});
return client;
}
createMongoClient()
// app.js
container.register({
...略
mongoClient: asFunction(createMongoClient, { lifetime: Lifetime.SINGLETON }), // 註冊為 mongoClient,且生命期為 SINGLETON (執行中只有一個物件)
...略
});
// app.js
// 預先引起建立 mongoClient
const mongoClient = container.resolve('mongoClient');
const indexRouter = container.resolve('indexRouter');
這樣就大工告成了,剩下的像是:相依注入、建立物件…等會由awilix 幫我們完成
在 Day 12 - 二周目 - 準備起程深入後端 提過 Node.js 執行 js 檔時是單一行程、單一執行緒。有 golbals 的變數叫 process
,它是指當前執行時的 process。 當我們執行 node ./bin/www
時 process
就會被建立,它是外界串連的橋樑。
我們常常希望 node ./bin/www
可以從外部設定參數,像是 PORT 環境變數,就是會拿來設定 Web Server 會開啟的 port。你可以直接打開檔案./bin/www
會看到 process.env.PORT
,
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
...略
這裡的 process.env
物件是 process 執行時的環境變數(environment)。
因此,下指令 PORT=3001 node ./bin/www
,前面的 PORT
就會被設定在 process.env.PORT
中。若要更多環境變數就用「空白」區隔,例如 MONGODB_URL=http://localhost:27017 PORT=3001 node ./bin/www
config.js
加入環境變數利用 process.env
就可以從外界修改環境變數,不用動程式
const config = {
mongodb: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
dbName: process.env.MONGODB_DB_NAME || 'myproject',
}
}
module.exports = config;
小技巧:process.env.MONGODB_URL || 'mongodb://localhost:27017'
可以在 MONGODB_URL
沒設定值時使用預設值 'mongodb://localhost:27017'
注意: process.env.XXX
是 undefined
或 string
,所以萬一變數是其它的型態要進行轉型,如:
function __defaultFalse(bool) {
if(bool === "true") {
return true;
}
if(bool === "false") {
return false;
}
return false;
}
const enable = __defaultFalse(process.env.ENABLE);
若你是透過 VSCode debug 模式執行 js,一樣可以加入環境變數
打開 .vscode/launch.json
,加入 env
參數
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "www",
"program": "${workspaceFolder}/bin/www",
"env": {
"PORT": "3001",
}
}
]
}
process.argv 是另一個方法,它是執行參數的字串陣列
範例直接來自官網:
當執行
node process-args.js one two=three four
process.argv
陣列如下:
0: /usr/local/bin/node
1: /Users/mjr/work/node/process-args.js
2: one
3: two=three
4: four
若要讓程式可以輸入參數,我推薦使用 argparse,可以做 cli (command-line interface) 做的比較完整。
今天我們介紹如何用 awilix 做依賴注入,讓我們從 關注「建立物件」和「串接物件關係」
轉為關注「物件建立方法」
。另外,用 process.env
來組態化專案,藉此來從外界接收設定值。