乐趣区

关于javascript:从零搭建-Nodejs-企业级-Web-服务器九配置项

服务器运行的实在环境

在企业做服务器除了本人的本地环境,还要充沛地思考部署环境。通常部署环境会有日常环境、预发环境、线上环境,在一些稳定性要求更高的我的项目中还会有灰度环境,不同环境之间会存在一些隔离,不同环境自身也存在一些差别,企业级 Web 服务器须要提供良好的机制在各个环境上平滑切换。本章将围绕如何无效治理配置项实现服务器在不同环境平滑切换进行开展。

对于配置项

服务器的配置项次要分为两类:环境变量、配置文件。前者历史能够追溯到上个世纪八十年代,为程序运行提供了最根底的输出,Node.js 中能够通过 process.env 拜访,其中 NODE_ENV 是最为宽泛应用的环境变量,常见的约定值比方:development。后者的应用更是自有程序以来就约定俗称的良好习惯,受害于 Node.js 的精美,个别间接应用写入配置的 .js 文件作为配置文件。

当初从上一章已实现的工程 host1-tech/nodejs-server-examples – 08-security 着手,联合环境变量与配置文件实现程序在环境间的平滑切换。在工程根目录执行命令装置环境变量治理的相干模块 cross-env、dotenv 以及用于合并配置项的模块 lodash.merge:

$ yarn add cross-env dotenv lodash.merge
# ...
info Direct dependencies
├─ cross-env@7.0.2
├─ dotenv@8.2.0
└─ lodash.merge@4.6.2
# ...

配置项革新

接下来将会把配置项分为 3 套,本地配置、部署配置、测试配置,别离对应 3 个关键词 developmentproductiontest。本地配置用于本地环境,部署配置用于日常、预发、线上等的部署环境(本文对应容器环境),测试配置用于单元测试(后续章节再做开展)。动静的或隐衷的配置项将以环境变量提供,同时环境变量 NODE_ENV 将会决定应用哪套配置。服务器最终的配置项由默认配置与 NODE_ENV 抉择的配置合并而成。

写入环境变量管制:

$ mkdir scripts # 新建 scripts 目录寄存工具脚本

$ tree -L 1     # 展现当前目录内容构造
.
├── Dockerfile
├── database
├── node_modules
├── package.json
├── public
├── scripts
├── src
└── yarn.lock
// scripts/env.js
const fs = require('fs');
const {resolve} = require('path');
const dotenv = require('dotenv');

const dotenvTags = [
  // 本地环境
  'development',

  // 测试环境
  // 比方:单元测试
  'test',

  // 部署环境
  // 比方:日常、预发、线上
  'production',
];

if (!dotenvTags.includes(process.env.NODE_ENV)) {process.env.NODE_ENV = dotenvTags[0];
}

const dotenvPath = resolve('.env');

const dotenvFiles = [
  dotenvPath,
  `${dotenvPath}.local`,
  `${dotenvPath}.${process.env.NODE_ENV}`,
  `${dotenvPath}.${process.env.NODE_ENV}.local`,
].filter(fs.existsSync);

dotenvFiles
  .reverse()
  .forEach((dotenvFile) => dotenv.config({path: dotenvFile}));
// package.json
{
  "name": "09-config",
  "version": "1.0.0",
  "scripts": {
-    "start": "node src/server.js",
+    "start": "node -r ./scripts/env src/server.js",
+    "start:prod": "cross-env NODE_ENV=production node -r ./scripts/env src/server.js",
+    "sequelize": "sequelize",
+    "sequelize:prod": "cross-env NODE_ENV=production sequelize",
    "build:yup": "rollup node_modules/yup -o src/moulds/yup.js -p @rollup/plugin-node-resolve,@rollup/plugin-commonjs,rollup-plugin-terser -f umd -n'yup'"
  },
  // ...
}
# Dockerfile
FROM node:12.18.2

WORKDIR /usr/app/09-config
COPY . .
RUN yarn

EXPOSE 9000
-CMD yarn start
+CMD yarn start:prod

抽离创立配置文件:

$ mkdir src/config  # 新建 src/config 寄存配置文件

$ tree src -L 1     # 展现 src 目录内容构造
src
├── config
├── controllers
├── middlewares
├── models
├── moulds
├── server.js
├── services
└── utils
// src/config/index.js
const merge = require('lodash.merge');

const config = {
  // 默认配置
  default: {
    sessionCookieSecret: '842d918ced1888c65a650f993077c3d36b8f114d',
    sessionCookieMaxAge: 7 * 24 * 60 * 60 * 1000,

    homepagePath: '/',
    loginPath: '/login.html',
    loginWhiteList: {'/500.html': ['get'],
      '/api/health': ['get'],
      '/api/csrf/script': ['get'],
      '/api/login': ['post'],
      '/api/login/github': ['get'],
      '/api/login/github/callback': ['get'],
    },

    githubStrategyOptions: {
      clientID: 'b8ada004c6d682426cfb',
      clientSecret: '0b13f2ab5651f33f879a535fc2b316c6c731a041',
      callbackURL: 'http://localhost:9000/api/login/github/callback',
    },

    db: {
      dialect: 'sqlite',
      storage: ':memory:',
      define: {underscored: true,},
      migrationStorageTableName: 'sequelize_meta',
    },
  },

  // 本地配置
  development: {
    db: {storage: 'database/dev.db',},
  },

  // 测试配置
  test: {
    db: {logging: false,},
  },

  // 部署配置
  production: {
    sessionCookieMaxAge: 3 * 24 * 60 * 60 * 1000,

    db: {storage: 'database/prod.db',},
  },
};

module.exports = merge({},
  config.default,
  config[process.env.NODE_ENV || 'development']
);
// src/middlewares/index.js
const {Router} = require('express');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const csurf = require('csurf');
const helmet = require('helmet');
const sessionMiddleware = require('./session');
const urlnormalizeMiddleware = require('./urlnormalize');
const loginMiddleware = require('./login');
const authMiddleware = require('./auth');
+const {sessionCookieSecret} = require('../config');

-const secret = '842d918ced1888c65a650f993077c3d36b8f114d';
-
module.exports = async function initMiddlewares() {const router = Router();
  router.use(helmet());
  router.use(urlnormalizeMiddleware());
-  router.use(cookieParser(secret));
-  router.use(sessionMiddleware(secret));
+  router.use(cookieParser(sessionCookieSecret));
+  router.use(sessionMiddleware());
  router.use(loginMiddleware());
  router.use(authMiddleware());
  router.use(bodyParser.urlencoded({ extended: false}), csurf());
  return router;
};
// src/middlewares/session.js
const session = require('express-session');
const sessionSequelize = require('connect-session-sequelize');
const {sequelize} = require('../models');
+const {sessionCookieSecret, sessionCookieMaxAge} = require('../config');

-module.exports = function sessionMiddleware(secret) {+module.exports = function sessionMiddleware() {const SequelizeStore = sessionSequelize(session.Store);

  const store = new SequelizeStore({
    db: sequelize,
    modelKey: 'Session',
    tableName: 'session',
  });

  return session({
-    secret,
-    cookie: {maxAge: 7 * 24 * 60 * 60 * 1000},
+    secret: sessionCookieSecret,
+    cookie: {maxAge: sessionCookieMaxAge},
    store,
    resave: false,
    proxy: true,
    saveUninitialized: false,
  });
};
// src/middlewares/auth.js
const passport = require('passport');
const {Strategy: GithubStrategy} = require('passport-github');
+const {githubStrategyOptions} = require('../config');

-const GITHUB_STRATEGY_OPTIONS = {
-  clientID: 'b8ada004c6d682426cfb',
-  clientSecret: '0b13f2ab5651f33f879a535fc2b316c6c731a041',
-  callbackURL: 'http://localhost:9000/api/login/github/callback',
-};
-
const githubStrategy = new GithubStrategy(
-  GITHUB_STRATEGY_OPTIONS,
+  githubStrategyOptions,
  (accessToken, refreshToken, profile, done) => {
    /**
     * 依据 profile 查找或新建 user 信息
     */
    const user = {};
    done(null, user);
  }
);
// ...
// src/middlewares/login.js
const {parse} = require('url');
+const {homepagePath, loginPath, loginWhiteList} = require('../config');

-module.exports = function loginMiddleware(
-  homepagePath = '/',
-  loginPath = '/login.html',
-  whiteList = {-    '/500.html': ['get'],
-    '/api/health': ['get'],
-    '/api/csrf/script': ['get'],
-    '/api/login': ['post'],
-    '/api/login/github': ['get'],
-    '/api/login/github/callback': ['get'],
-  }
-) {-  whiteList[loginPath] = ['get'];
+module.exports = function loginMiddleware() {+  const whiteList = Object.assign({}, loginWhiteList, {+    [loginPath]: ['get'],
+  });

  return (req, res, next) => {// ...};
};
// src/controllers/login.js
const {Router} = require('express');
const {passport} = require('../middlewares/auth');
+const {homepagePath, loginPath} = require('../config');

class LoginController {
-  homepagePath;
-  loginPath;
-
  async init() {const router = Router();
    router.post('/', this.post);
    router.get(
      '/github',
      passport.authenticate('github', { scope: ['read:user'] })
    );
    router.get(
      '/github/callback',
      passport.authenticate('github', {
-        failureRedirect: this.loginPath,
+        failureRedirect: loginPath,
      }),
      this.getGithubCallback
    );
    return router;
  }

  post = (req, res) => {
    req.session.logined = true;
-    res.redirect(this.homepagePath);
+    res.redirect(homepagePath);
  };

  getGithubCallback = (req, res) => {
    req.session.logined = true;
-    res.redirect(this.homepagePath);
+    res.redirect(homepagePath);
  };
}

-module.exports = async (homepagePath = '/', loginPath = '/login.html') => {+module.exports = async () => {const c = new LoginController();
-  Object.assign(c, { homepagePath, loginPath});
  return await c.init();};
// src/models/config/index.js
-module.exports = {
-  development: {
-    dialect: 'sqlite',
-    storage: 'database/index.db',
-    define: {
-      underscored: true,
-    },
-    migrationStorageTableName: 'sequelize_meta',
-  },
-};
+const {db} = require('../../config');
+
+module.exports = {[process.env.NODE_ENV || 'development']: db };

这样就有了 NODE_ENVdevelopment 的本地配置与 NODE_ENVproduction 的部署配置,能够别离通过 yarn startyarn start:prod(或者容器)在本地环境与部署环境以隔离的数据库运行,数据库模式与数据能够别离应用 yarn sequelizeyarn sequelize:prod 做初始化。

当初将原来的 Github OAuth 利用只用于本地环境,再新建一个只用于部署环境的 Github OAuth 利用,将两套 clientID 与 clientSecret 改用环境变量形式别离注入,实现认证登录在两套环境的隔离运行:

# .env.local
GITHUB_CLIENT_ID='b8ada004c6d682426cfb'
GITHUB_CLIENT_SECRET='0b13f2ab5651f33f879a535fc2b316c6c731a041'
# .env.production.local
GITHUB_CLIENT_ID='a8d43bbca18811dcc63a'
GITHUB_CLIENT_SECRET='276b97b79c79cfef36c3fb1fceef8542f9e88aa6'
// src/config/index.js
// ...
const config = {
  // 默认配置
  default: {
    // ...
    githubStrategyOptions: {
-      clientID: 'b8ada004c6d682426cfb',
-      clientSecret: '0b13f2ab5651f33f879a535fc2b316c6c731a041',
+      clientID: process.env.GITHUB_CLIENT_ID,
+      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      callbackURL: 'http://localhost:9000/api/login/github/callback',
    },
    // ...
  },

  // ...

  // 部署配置
  production: {
    sessionCookieMaxAge: 3 * 24 * 60 * 60 * 1000,
+
+    githubStrategyOptions: {
+      callbackURL: 'http://localhost:9090/api/login/github/callback',
+    },

    db: {storage: 'database/prod.db',},
  },
};
// ...

再通过 .gitignore 疏忽掉 .env*.local,本地应用 .env.local.env.development.local,在部署环境构建镜像时注入 .env.local.env.production.local,即可将敏感配置齐全地爱护起来。

本地环境运行成果:

$ yarn start  # 本地启动
# ...

部署环境运行成果:

$ # 构建容器镜像,命名为 09-config,标签为 1.0.0
$ docker build -t 09-config:1.0.0 .
# ...
Successfully tagged 09-config:1.0.0

$ # 以镜像 09-config:1.0.0 运行容器,命名为 09-config
$ # 挂载 database 寄存数据库文件
$ # 重启策略为无条件重启
$ docker run -p 9090:9000 -v "$PWD/database":/usr/app/09-config/database -d --restart always --name 09-config 09-config:1.0.0

本章源码

host1-tech/nodejs-server-examples – 09-config

更多浏览

从零搭建 Node.js 企业级 Web 服务器(零):动态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异样解决
从零搭建 Node.js 企业级 Web 服务器(五):数据库拜访
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项

退出移动版