Skip to content

Latest commit

 

History

History
392 lines (315 loc) · 17.3 KB

2018-07-28:NodeJS 最佳實踐筆記.md

File metadata and controls

392 lines (315 loc) · 17.3 KB

NodeJS 最佳實踐 的筆記

文章來源:https://github.com/i0natan/nodebestpractices

1.1 组件式构建你的解决方案

用 feature 來區分 folder

...如果你正在寻找一个图书馆的建筑架构, 你可能会看到一个盛大的入口, 一个 check-in-out 的文员, 阅读区, 小会议室, 画廊, 画廊后面容纳了装载所有图书馆书籍的书架。建筑会声明: 图书馆. 那么, 应用程序的体系架构会声明什么呢? 当您查看顶级目录结构和最高级别包中的源文件时; 他们声明: 医疗保健系统, 或会计系统, 或库存管理系统? 或者他们声明: Rails, 或Spring/Hibernate, 或 ASP?.

範例參考為:像這樣用 feature 來分類。圖書館的例子很好懂。

components

orders
otherFeature
products
users

index.js
user.js
usersAPI.js
usersController.js
usersDAL.js
usersError.js
usersService.js
userTesting.js

workpace

libraries

1.2 分层设计组件,保持Express在特定的区域

TL;DR: 每一个组件都应该包含'层级' - 一个专注的用于接入网络,逻辑,数据的概念。这样不仅获得一个清晰的分离考量,而且使仿真和测试系统变得异常容易。

// bad example
比如把网络层的对象(Express req, res)传给业务逻辑和数据层 - 这会令您的应用彼此依赖,并且只能通过Express使用。

這段沒有很懂,也就是說要好好抽離功能與邏輯。有個範例 gif 是說 別傳 req 進去,而是建一個 Object 傳進去。這樣應該也對,如果某 function 可以接 Object ,那代表大家都能用,如果只能接 req 那代表只有 expressJS 能用。
恩,上面這句心得解釋起來就知道到時候要怎麼拆了。

1.3 封装公共模块成为NPM的包

由大量代码构成的一个大型应用中,贯彻全局的,比如日志,加密和其它类似的公共组件,应该进行封装,并暴露成一个私有的NPM包。这将使其在更多的代码库和项目中被使用变成了可能。

一旦你开始在不同的服务器上增加不同的组件并使用不同的服务器,这些服务器会消耗类似的工具,那么你应该开始管理依赖关系
用自己的代码包装第三方实用工具包,以便将来可以轻松替换,并发布自己的代码作为私人npm包。

這幾句話解釋的蠻清楚了,也就是這些共用的組件,應該要發佈出來,避免未來 code 散布在不同專案、不同機器上面,難以維護、升級等等,用 npm 一切搞定。

1.4 分离 Express 'app' and 'server'

TL;DR: 避免定义整个Express应用在一个单独的大文件里, 这是一个不好的习惯 - 分离您的 'Express' 定义至少在两个文件中: API声明(app.js) 和 网络相关(WWW)。对于更好的结构,是把你的API声明放在组件中。

否则: 您的API将只能通过HTTP的调用进行测试(慢,并且很难产生测试覆盖报告)。维护一个有着上百行代码的文件也不是一个令人开心的事情。

這論點真是令我驚訝!原來有這樣的概念在這邊。

API声明与网络相关配置(端口、协议等)是分开的。这样就可以在不执行网络调用的情况下对API进行在线测试,它所带来的好处是:快速执行测试操作和获取代码覆盖率。它还允许在灵活多样的网络条件下部署相同的API。额外好处:更好的关注点分离和更清晰的代码结构。

代码示例:API声明应该在 app.js 文件里面

var app = express();
app.use(bodyParser.json());
app.use("/api/events", events.API);
app.use("/api/forms", forms);

代码示例: 服务器网络声明,应该在 /bin/www 文件里面

var app = require('../app');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

示例代码: 使用超快的流行的测试包在线测试你的代码

const app = express();

app.get('/user', function(req, res) {
  res.status(200).json({ name: 'tobi' });
});

request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end(function(err, res) {
    if (err) throw err;
  });

長知識了,說起來我還沒有寫過 Server 的 test,難怪這邊沒有概念。看來未來還是該這樣寫才對!!!。
另外,requset 這樣的 library 如果完熟了,也是對 http 的操作加強的一個好方法!!!

1.5 使用易于设置环境变量,安全和分级的配置

TL;DR: 一个完美无瑕的配置安装应该确保

  • (a) 元素可以从文件中,也可以从环境变量中读取
  • (b) 密码排除在提交的代码之外
  • (c) 为了易于检索,配置是分级的。

仅有几个包可以满足这样的条件,比如rc, nconf 和 config。

否则: 不能满足任意的配置要求将会使开发,运维团队,或者两者,易于陷入泥潭。

好主題!這個我確實不知道怎麼搞!!,文件上推薦這兩個 library 來處理

一个可靠的配置解决方案必须结合配置文件和进程变量覆盖。 也就是說會抓 config 檔參數,但也能傳 NODE_ENV 參數進去蓋它

//代码示例 – 分层配置有助于查找条目和维护庞大的配置文件
{
  // Customer module configs 
  "Customer": {
    "dbConfig": {
      "host": "localhost",
      "port": 5984,
      "dbName": "customers"
    },
    "credit": {
      "initialLimit": 100,
      // Set low for development 
      "initialDays": 1
    }
  }
}

看了這範例,我還是沒有很懂!!,看來要多看點才行了。敏感資訊可以用 加密來上傳到 github 中。這種作法真式又多一到工,真是麻煩~囧。

下一節是 error handle,果然 error handle 是 JS 的重點。被放在 ch2 就是代表其重要性。

2.1 使用 Async-Await 和 promises 用來處理 async 的 error handle

直接使用 promise 或 async-await 來處理 error handle,這會讓它像 try-catch 一樣簡潔。
(可直接用成熟的 promise library來玩) 用 callback 來處理 async error handle 會是災難,用 NodeJS 的 callback 設計,最終一定無法維護。

// 使用 promise 來 error handle
doWork()
 .then(doWork)
 .then(doOtherWork)
 .then((result) => doWork)
 .catch((error) => {throw error;})
 .then(verify);
// 使用 callback 來 error handle
getData(someParameter, (err, result) => {
  if(err != null)
    getMoreData(a, function(err, result){
      if(err != null)
        getMoreData(b, function(c){ 
          getMoreData(d, function(e){ 
            if(err != null)
              //看..  這就是 call back 地獄
  });
});

2.2 僅(這是只的意思嗎)使用內建的 Error 對象。 Use only the built-in Error object

很多人 throw error 會使用 string 或 自定義的類型,這會導致 error handle 處理邏輯與模塊間的調用最終變複雜。使用 built-in Error object 會提升設計一制性。

不然你呼叫某些 function、module 時,你不確定會有哪種「錯誤類型」回來,更壞的情況是,使用特定類型的 error 描述錯誤,會導致重要的 error info 缺少,例如 stack trace

// 典型的 throw error 無論是 sync or async
if(!productToAdd)
  throw new Error("How can I add new product when no value provided?");

// 從 EventEmitter throw Error
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));

// 從 promise
return new promise(function (resolve, reject) {
	Return DAL.getProduct(productToAdd.id)
    .then((existingProduct) => {
		 if(existingProduct != null)
			 reject(new Error("Why fooling us and trying to add an existing product?"));
    })})

// anti pattern
// throw string 缺少其他任何 stack trace 的訊息。
if(!productToAdd)
  throw ("How can I add new product when no value provided?");

// =======================
// 從 node 的 Error 派生的集中錯誤對象
function appError(name, httpCode, description, isOperational) {
  Error.call(this);
  Error.captureStackTrace(this);
  this.name = name;
  //...在这赋值其它属性
};

appError.prototype.__proto__ = Error.prototype;
module.exports.appError = appError;

// Client 端拋出一個錯誤
if(user == null)
  throw new appError(
    commonErrors.resourceNotFound,
    commonHTTPErrors.notFound,
    "further explanation",
    true
  )

2.3 區分 運行錯誤 和 程式設計錯誤

operational errors (例如 api 收到一個無效的輸入),指的是已知場景下的錯誤。這類 error 的影響已經被理解並能完整的處理掉。(也就是說你了解發生什麼問題及知道影響範圍、可預期、也知道該怎麼對應這種error 的類型)

programmer errors 指的是未知的coding問題,影響的應用、不知道原因甚至不知道問題發生來源。 ( memory leak 之類得)

區別這兩個,會讓「處理」更有技巧。 操作error 相對好處理,通常有 error log 就夠了 程式error 通常難處理,應用程式常處於不ㄧ致的狀態,大概最好的選擇會是「優雅的重新啟動」

// 將 error 標記為可操作的(受信任的) ?這種做法我不懂
const myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;

// 或者 你使用的是一些集中式 error factory 
function appError(commonType, description, isOperational) {
  Error.call(this);
  Error.captureStackTrace(this);
  this.commonType = commonType;
  this.description = description;
  this.isOperational = isOperational;
};
 
throw new appError(
  errorManagement.commonErrors.InvalidInput,
  "Describe here what happened",
  true
);

從 程序Error 中恢復的最好辦法是「立刻 creah」。你應該使用 restarter 執行程序,讓 crash 自動重啟 (齁~這就 pm2 之類的摟)「立刻 creah」是最快能恢復服務的方法。

除非你真的知道你在做什麼,否則,你收到一個 uncaughtException error 時,應該對 service restart 一次,不然你 Application 的狀態或和第三方libraray 的狀態可能會不一致,存在風險,最終導致各種荒唐錯誤

2.4 集中錯誤處理,不要在 Express middleware 中處理錯誤

比起 send mail alert,log 應該要封裝在一個特定、集中的對象中。當錯誤產生時,所有終端,如(express middleare、cron taks、unit test)都可以呼叫

通過寫入一個「好格式」的logger

集中处理错误,通过但不是在中间件里处理错误 一段解释 如果没有一个专用的错误处理对象,那么由于操作不当,在雷达下重要错误被隐藏的可能性就会更大。错误处理对象负责使错误可见,例如通过写入一个格式化良好的logger,通过电子邮件将事件发送到某个监控产品或管理员。一个典型的错误处理流程可能是:一些模块抛出一个错误 -> API路由器捕获错误 -> 它传播错误给负责捕获错误的中间件(如Express,KOA)-> 集中式错误处理程序被调用 -> 中间件正在被告之这个错误是否是一个不可信的错误(不是操作型错误),这样可以优雅的重新启动应用程序。注意,在Express中间件中处理错误是一种常见但又错误的做法,这样做不会覆盖在非Web接口中抛出的错误。

// DAL層,在這邊我們不處理錯誤
DB.addDocument(newCustomer, (error, result) => {
  if (error)
    throw new Error("Great error explanation comes here", other useful parameters)
});

// API router code,我們同時 cathc sync async error 並轉給其他 middleware
try {
  customerService
    .addNew(req.body)
    .then(result => res.status(200).json(result))
    .catch(error => next(error));
} catch (error) {
  next(error); 
}

// error 處理的middleware,我們委託集中式錯誤處理程序處理錯誤
app.use(function (err, req, res, next) {
  errorHandler
    .handleError(err)
    .then((isOperationalError) => {
      if (!isOperationalError)
        next(err);
    });
});

// 在一個專門的對象裡面處理錯誤 // 我看不懂這隻的精髓啊?
module.exports.handler = new errorHandler();
 
function errorHandler(){
    this.handleError = function (error) {
        return logger
          .logError(err)
          .then(sendMailToAdminIfCritical)
          .then(saveInOpsQueueIfCritical)
          .then(determineIfOperationalError);
    }
}

// bad example anti pattern 在 middleware 中處理錯。
app.use(function (err, req, res, next) {
  logger.logError(err);
  if(err.severity == errors.high)
      mailer.sendMail(configuration.adminMail, "Critical error occured", err);
  if(!err.isOperational)
      next(err);
});

看完這些我還是不清楚怎麼做好 error
應該是最後一定要有一個middlerware 來接 error 並處理吧?

正確執行記載 一般而言,從您的應用程式進行記載的原因有二:

  1. 為了除錯
  2. 為了記載應用程式活動(其實就是除錯之外的每一項)。 使用 console.log() 或 console.err() 將日誌訊息列印至終端機,在開發中是常見作法。 但是當目的地是終端機或檔案時,這些函數是同步的,除非您將輸出引導至另一個程式,這些函數並不適用於正式作業。
  • 為了除錯:用 debug 之類的特殊除錯模組。這個模組可讓您使用 DEBUG 環境變數,來控制哪些除錯訊息(若有的話)要送往 console.err()。
  • 為了應用程式活動:要記載應用程式活動(例如,追蹤資料流量或 API 呼叫),則不要使用 console.log(),請改用 Winston 或 Bunyan 之類的記載程式庫。(就用 Winston 吧)

為了確保您能處理所有的異常狀況,請使用下列技術:

  • 使用 try-catch
  • 使用 promise

expressJS 官網也是這樣說,最好的辦法是 next() 出去 error,透過middleware 來傳播 err

在分別討論這兩個主題之前,您對 Node/Express 錯誤處理方式應有基本的瞭解:使用「錯誤優先回呼」,並將錯誤傳播至中介軟體。Node 從非同步函數傳回錯誤時,會採用「錯誤優先回呼」慣例,其中,回呼函數的第一個參數是錯誤物件,接著是後續參數中的結果資料。如果要指出無錯誤,會傳遞 null 作為第一個參數。回呼函數必須同樣遵循「錯誤優先回呼」慣例,才能實際處理錯誤。在 Express 中,最佳作法是使用 next() 函數,透過中介軟體鏈來傳播錯誤。

禁止事項:接聽 uncaughtException 事件,此事件是在回歸事件迴圈期間不斷引發異常狀況時產生的。新增 uncaughtException 的事件接聽器,會使遇到異常狀況的程序變更其預設行為;儘管發生異常狀況,該程序會繼續執行。阻止應用程式當機,似乎是個好辦法,但是在未捕捉到異常狀況之後,又繼續執行應用程式,卻是危險作法而不建議這麼做,因為程序的狀態會變得不可靠且無法預測。

此外,使用 uncaughtException 被公認為拙劣作法,這裡有一份提案,指出如何將它從核心移除。因此,接聽 uncaughtException 並不可取。這是我們建議採取多重程序和監督程式等事項的原因: =>當機再重新啟動,通常是從錯誤回復最可靠的作法。

again ,ExpressJS 也說了=> 當機再重新啟動,通常是從錯誤回復最可靠的作法。

//利用 try-catch 來處理 JSON 剖析錯誤。 
app.get('/search', function (req, res) {
  // Simulating async operation
  setImmediate(function () {
    var jsonStr = req.query.params;
    try {
      var jsonObj = JSON.parse(jsonStr);
      res.send('Success');
    } catch (e) {
      res.status(400).send('Invalid JSON string');
    }
  });
});
// 不過,try-catch 只適用於同步程式碼,使用 promise catch
app.get('/', function (req, res, next) {
  // do some sync stuff
  queryDb()
    .then(function (data) {
      // handle data
      return makeCsv(data)
    })
    .then(function (csv) {
      // handle csv
    })
    .catch(next);
});

app.use(function (err, req, res, next) {
  // handle error
});

現在,所有非同步與同步錯誤都會傳播到錯誤中介軟體。 不過,請注意下列兩項警告:

您所有的非同步程式碼都必須傳回 promise(不包括發射程式)。如果特定程式庫沒有傳回 promise,請使用 Bluebird.promisifyAll() 等之類的 helper 函數來轉換基本物件。 事件發射程式(例如:串流)仍可能造成未捕捉到的異常狀況。因此,請確定錯誤事件的處理適當;例如:

// 處理eventemitter error handle
app.get('/', wrap(async (req, res, next) => {
  let company = await getCompanyById(req.query.id)
  let stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

2.2 ~ 2.4 就屌打我最近的作品 產圖 api 的設計了 囧