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:
"scripts": {
"start": "NODE_ENV=development nodemon ./app.js"
},
比如上面的配置,当npm start之后,就用nodemon执行app.js—-我们程序的入口。NODE_ENV=development是在配置参数,这个参数用途很多,比如说在项目的配置文件中,可以更加参数设置程序的监听端口。
// 设置端口
if (process.env.NODE_ENV === 'development') {
cfg = {
...cfg,
port: 12580,
};
}
if (process.env.NODE_ENV === 'production') {
cfg = {
...cfg,
port: 80,
};
}
@koa/router
@koa/router,是koa的路由中间件,最为常用。
基本用法如下,更多用法参见API:
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx, next) => {
// ctx.router available
});
// 最终还是要和其它中间件一样要use
app
.use(router.routes())
.use(router.allowedMethods());
不过自己的项目组织时候,最好是把router.get、post等具体监听路由的逻辑放在别处,千万别放在app.js中。
通常情况下,router属于MVC中的C—contrllor,是整个应用运行起来对前端的接口,有Controller层分发具体的业务逻辑,或者响应具体的行为。
所以可在项目根目录下建一个controllers目录,比如我们在app.js中调用initRouter();此方法来自controllers/index.js,有如下代码:
const Router = require('@koa/router');
const ApiController = require('./ApiController');
const IndexController = require('./IndexController');
const router = new Router();
const indexController = new IndexController();
const apiController = new ApiController();
function initRouter(app) {
// 页面路由
router.get('/', indexController.actionIndex);
// router.get('/vue', vueController.actionVue);
// 接口路由
router.get('/api/test/List', apiController.actionData);
app.use(router.routes())
.use(router.allowedMethods());
}
module.exports = initRouter;
view: ejs / swig
模版引擎一直是后端渲染不可缺少的一块。MVC中V—View大部分借助模版引擎来实现。比如JavaWeb中古老但鼎鼎大名的JSP(Jave Server Page)。当下Node端常见的模版引擎是ejs、swig。
koa-swig基本用法是这样:
// koa v2.x
var co = require('co');
var koa = require('koa');
var render = require('koa-swig');
var app = koa();
app.context.render = co.wrap(render({
root: path.join(__dirname, 'views'),
autoescape: true,
cache: 'memory', // disable, set to false
ext: 'html',
locals: locals,
filters: filters,
tags: tags,
extensions: extensions
}));
app.listen(1234);
上面这段代码最终做了什么事情呢?:在context对象中内置了render方法(co.wrap我们可以猜测其内部是用了generator),在设置render方法时,执行了了我们要从__dirname/views下去读取.html文件。
比如,我在views下有文件vue.html,那么当某个controller需要在此模版下灌入数据并返回时候:
ctx.body = await ctx.render('vue', {
desc: 'This is a Test of Vue'
})
vue.html中:
<body>
<input type="text" v-model="message">
<p>[[desc]]</p>
{{message}}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
</body>
desc我们其实向此html中灌入的数据是desc,swig帮我们将变量desc替换成了我们需要的值,这里需要再说明的是,模块的变量都是要被特殊自符标记的,比如vue渲染的标记就是{{ }},不巧的是swig默认其实也是{{ }},如果我们不改而项目里面有用了vue的化,势必会冲突。好在这个标记能在swig里面改,配置属性:varControls就行。
app.context.render = co.wrap(render({
root: config.viewDir,
cache: config.viewCache,
ext: 'html',
varControls:['[[',']]']
}));
静态资源服务器
静态资源服务器比较简单,就是资源是静态文件。当请求到了server之后,直接在该目录下去寻找文件就行。
一般使用koa-static
用法也很简单:
const Koa = require('koa');
const app = new Koa();
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移植版。用法当然很简单:
const {
historyApiFallback
} = require('koa2-connect-history-api-fallback');
app.use(historyApiFallback({
index: '/',
whiteList: ['/api'], // api路由不做真假路由
}));
日志
服务端一定要保留日志,这样对后续定位问题,修复bug,甚至各种统计等工作都是十分必要的。
log4js用法很简单(细节参照文档):
// 日志初始化
log4js.configure({
appenders: { cheese: { type: "file", filename: "./log/err.log" } },
categories: { default: { appenders: ["cheese"], level: "error" } }
});
const logger = log4js.getLogger();
// 使用
logger.error(e)
错误边界
Node.js一定要做好try-catch工作,因为一旦有异常或错误没有捕获到,程序直接崩溃…
我们写一个类ErrorHandler,在静态方法globalHandle中,将app中注册一个中间件,用来处理错误。
我们当然可以给整个中间件最外层整一个try-catch。但是,通常服务器逻辑问题(错误会从 next();中抛上来),我们会返回500,页面没找到,我们返回404,当然没权限等也有对应的状态。
还可以做的更多,当区分出这些错误原因,我们当然可以利用模版引擎,SSR对应的友好页面,以及把错误打在日志中:
class ErrorHandler {
static globalHandle(app, logger) {
// 捕获 服务端逻辑错误
app.use(async (ctx, next) => {
try{
await next();
}catch(e){
ctx.body = await ctx.render('error_500');
logger.error(e)
}
});
// 捕获 404
app.use(async (ctx, next) => {
await next();
if(ctx.status === 404){
ctx.body = await ctx.render('error_404');
}
});
}
}
module.exports = ErrorHandler;
测试
// todo