Koa.js

koa.js的官网,联同官方文档都只有一页,这本身就说明了他是一个简单,小巧并且基础的Node.js的web框架。正因为其基础,所以一般企业级项目现在也很少直接使用koa了。2020年Nest.js势头正旺,在中国egg.js使用的也很多,不过这些框架都是或基于express,或基于koa的上层框架。另外,从connect、express到koa,这是一脉相承的。

开发要点

when env is dev:nodemon

nodemon:nodemon是当前比较流行的开发启动工具,他提供了代码热更新功能;
关于项目启动,比较常规的做法是:在package.json中,配置scrpit:

  1. "scripts": {
  2. "start": "NODE_ENV=development nodemon ./app.js"
  3. },

比如上面的配置,当npm start之后,就用nodemon执行app.js—-我们程序的入口。NODE_ENV=development是在配置参数,这个参数用途很多,比如说在项目的配置文件中,可以更加参数设置程序的监听端口。

  1. // 设置端口
  2. if (process.env.NODE_ENV === 'development') {
  3. cfg = {
  4. ...cfg,
  5. port: 12580,
  6. };
  7. }
  8. if (process.env.NODE_ENV === 'production') {
  9. cfg = {
  10. ...cfg,
  11. port: 80,
  12. };
  13. }

@koa/router

@koa/router,是koa的路由中间件,最为常用。
基本用法如下,更多用法参见API

  1. const Koa = require('koa');
  2. const Router = require('@koa/router');
  3. const app = new Koa();
  4. const router = new Router();
  5. router.get('/', (ctx, next) => {
  6. // ctx.router available
  7. });
  8. // 最终还是要和其它中间件一样要use
  9. app
  10. .use(router.routes())
  11. .use(router.allowedMethods());

不过自己的项目组织时候,最好是把router.get、post等具体监听路由的逻辑放在别处,千万别放在app.js中。
通常情况下,router属于MVC中的C—contrllor,是整个应用运行起来对前端的接口,有Controller层分发具体的业务逻辑,或者响应具体的行为。
所以可在项目根目录下建一个controllers目录,比如我们在app.js中调用initRouter();此方法来自controllers/index.js,有如下代码:

  1. const Router = require('@koa/router');
  2. const ApiController = require('./ApiController');
  3. const IndexController = require('./IndexController');
  4. const router = new Router();
  5. const indexController = new IndexController();
  6. const apiController = new ApiController();
  7. function initRouter(app) {
  8. // 页面路由
  9. router.get('/', indexController.actionIndex);
  10. // router.get('/vue', vueController.actionVue);
  11. // 接口路由
  12. router.get('/api/test/List', apiController.actionData);
  13. app.use(router.routes())
  14. .use(router.allowedMethods());
  15. }
  16. module.exports = initRouter;

view: ejs / swig

模版引擎一直是后端渲染不可缺少的一块。MVC中V—View大部分借助模版引擎来实现。比如JavaWeb中古老但鼎鼎大名的JSP(Jave Server Page)。当下Node端常见的模版引擎是ejs、swig。
koa-swig基本用法是这样:

  1. // koa v2.x
  2. var co = require('co');
  3. var koa = require('koa');
  4. var render = require('koa-swig');
  5. var app = koa();
  6. app.context.render = co.wrap(render({
  7. root: path.join(__dirname, 'views'),
  8. autoescape: true,
  9. cache: 'memory', // disable, set to false
  10. ext: 'html',
  11. locals: locals,
  12. filters: filters,
  13. tags: tags,
  14. extensions: extensions
  15. }));
  16. app.listen(1234);

上面这段代码最终做了什么事情呢?:在context对象中内置了render方法(co.wrap我们可以猜测其内部是用了generator),在设置render方法时,执行了了我们要从__dirname/views下去读取.html文件。
比如,我在views下有文件vue.html,那么当某个controller需要在此模版下灌入数据并返回时候:

  1. ctx.body = await ctx.render('vue', {
  2. desc: 'This is a Test of Vue'
  3. })

vue.html中:

  1. <body>
  2. <input type="text" v-model="message">
  3. <p>[[desc]]</p>
  4. {{message}}
  5. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  6. <script>
  7. var app = new Vue({
  8. el: '#app',
  9. data: {
  10. message: 'Hello Vue!'
  11. }
  12. })
  13. </script>
  14. </body>

desc我们其实向此html中灌入的数据是desc,swig帮我们将变量desc替换成了我们需要的值,这里需要再说明的是,模块的变量都是要被特殊自符标记的,比如vue渲染的标记就是{{ }},不巧的是swig默认其实也是{{ }},如果我们不改而项目里面有用了vue的化,势必会冲突。好在这个标记能在swig里面改,配置属性:varControls就行。

  1. app.context.render = co.wrap(render({
  2. root: config.viewDir,
  3. cache: config.viewCache,
  4. ext: 'html',
  5. varControls:['[[',']]']
  6. }));

静态资源服务器

静态资源服务器比较简单,就是资源是静态文件。当请求到了server之后,直接在该目录下去寻找文件就行。
一般使用koa-static
用法也很简单:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. app.use(require('koa-static')(rootDir, opts));

真假路由

真假路由这个词儿,叫法很奇怪,但是知道他是干什么的就随便叫吧…

‘真’路由

所谓‘真’路由,这里指的的是从服务器视角看,有些需要切换的页面的请求,前端发起后,服务端应该返回一个对应的页面(HTML字符串)。
这在纯粹的SSR中没有问题,就是这样玩的。
比如,地址栏输入:/friend/list想切换好友列表页面,纯粹的SSR渲染,服务器端会将列表和HTML模版整合好了之后返回给你,浏览器端拿到的页面就是渲染好的。

‘假’路由

那么,何为假路由?
这个就是我们单页面应用SPA的事情了。SPA的模式下,只从后端拿一个html页面,剩下的所有页面都是由js生成页面并挂在此html的根节点上,所以,这种情况下,本应该不需要再发请求向后端索要页面了。所有的,看上去的页面切换都由JS去做,无非就是用JS生成不一样的元素挂在相同的位置而已。这种js路由我们称之前端路由。
纯粹SPA模式的前端路由也有两种,一种是基于页面哈希,监听hash值的变化,从而动态决定页面内容,这种不会向后端发起请求,不存在所谓真假的问题。另一种是直接监听url变化,从而用js渲染页面,但是这个url是真的变了,会向后端server发起请求。
那么,这个请求后端收到了要作何处理?是响应还是不相应,如果响应,怎么响应,拿什么响应?如果不相应,后端如何区别那些需要url路由需要响应,那些不需要?
这就是所谓真假路由问题。解决思路说白了,就是server要能知道哪些是要被响应的。
connect-history-api-fallback就是用来解决此问题的。
他的原理就是,配置一个白名单,当命中白名单,就按照真路由处理,在白名单之外的任何路由,都返回前端根html。这样js会根据前端路由进行渲染了。
koa2-connect-history-api-fallback这个是上面的库的koa2移植版。用法当然很简单:

  1. const {
  2. historyApiFallback
  3. } = require('koa2-connect-history-api-fallback');
  4. app.use(historyApiFallback({
  5. index: '/',
  6. whiteList: ['/api'], // api路由不做真假路由
  7. }));

日志

服务端一定要保留日志,这样对后续定位问题,修复bug,甚至各种统计等工作都是十分必要的。
log4js用法很简单(细节参照文档):

  1. // 日志初始化
  2. log4js.configure({
  3. appenders: { cheese: { type: "file", filename: "./log/err.log" } },
  4. categories: { default: { appenders: ["cheese"], level: "error" } }
  5. });
  6. const logger = log4js.getLogger();
  7. // 使用
  8. logger.error(e)

错误边界

Node.js一定要做好try-catch工作,因为一旦有异常或错误没有捕获到,程序直接崩溃…
我们写一个类ErrorHandler,在静态方法globalHandle中,将app中注册一个中间件,用来处理错误。
我们当然可以给整个中间件最外层整一个try-catch。但是,通常服务器逻辑问题(错误会从 next();中抛上来),我们会返回500,页面没找到,我们返回404,当然没权限等也有对应的状态。
还可以做的更多,当区分出这些错误原因,我们当然可以利用模版引擎,SSR对应的友好页面,以及把错误打在日志中:

  1. class ErrorHandler {
  2. static globalHandle(app, logger) {
  3. // 捕获 服务端逻辑错误
  4. app.use(async (ctx, next) => {
  5. try{
  6. await next();
  7. }catch(e){
  8. ctx.body = await ctx.render('error_500');
  9. logger.error(e)
  10. }
  11. });
  12. // 捕获 404
  13. app.use(async (ctx, next) => {
  14. await next();
  15. if(ctx.status === 404){
  16. ctx.body = await ctx.render('error_404');
  17. }
  18. });
  19. }
  20. }
  21. module.exports = ErrorHandler;

测试

// todo