目标是由最简单的东西开始,一步一步理清楚Node.js的MVC架构的一般骨架。
一,模块
1.创建模块
//first.js
var s = 'Hello';
function greet(name){
console.log(s + ',' + name + '!');
}
//把模块中需要对外提供的功能暴露出去
module.exports = {
greet: greet
};
2.导入模块
//如果直接写 first的话
//Node依此检查 内置模块 全局模块 当前模块 找不到就报错
var exp = require('./first'); //导入greet函数
var s = 'mimof';
exp.greet(s); // Hello,mimof!
创建模块:写一个js文件,然后复制给module.exports对象,将其对外开发。
导入模块:require('模块路径') 返回js模块对外提供的对象。
这里还会涉及到模块的加载机制,CommonJS同步加载,AMD异步加载。可参考
最后一些常用的模块有:
- http http连接
- fs 文件处理
- stream 流
- process nodejs进程
- crypto 加密哈希算法
直接导入即可。
二,web框架koa2
先看一下用http模块如何做web开发
var http = require('http');
//创建Http服务器 所有请求都由这个回调函数处理
var server = http.createServer(function(request, response){
console.log(request.method + ":" + request.url);
response.writeHead(200, {'Content-Type': 'text/html'});
response.end('<h1>Hello world!</h1>');
});
server.listen(3000); //监听3000端口
console.log('server is running at http://127.0.0.1:3000/');
再来看koa2框架的用法
const Koa = require('koa'); //这里导入的Koa是一个class 不能直接使用
const app = new Koa(); //创建Koa对象表示web app本身
//koa的核心 middleware
app.use(async (ctx, next) => { //所有请求都由这个回调函数处理
await next(); //调用下个middleware 多个middle处理链就和顺序执行没什么区别
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, wrold!</h1>';
});
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
深入理解一下middleware的调用顺序
const Koa = require('koa'); //这里导入的Koa是一个class 不能直接使用
const app = new Koa(); //创建Koa对象表示web app本身
//输出访问地址的middleware
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`);
await next();
});
//计算耗时的middleware
app.use(async (ctx, next) => {
const start = new Date().getTime();
await next();
const ms = new Date().getTime - start;
console.log(`Time: ${ms}ms`);
});
//koa的核心 middleware
app.use(async (ctx, next) => { //所有请求都由这个回调函数处理
await next(); //调用下个middleware 多个middle处理链就和顺序执行没什么区别
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, wrold!</h1>';
});
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
输出:
server is running at http://127.0.0.1:3000/
GET /
Time: 2ms
GET /favicon.ico
Time: 1ms
对于不同的url,进行不同的处理,也就是koa-router路由模块
const Koa = require('koa'); //这里导入的Koa是一个class 不能直接使用
const app = new Koa(); //创建Koa对象表示web app本身
//路由模块
const router = require('koa-router')(); //和前面的app是一个道理 这里不过是简写
router.get('/hello/:name', async (ctx, next) => { //这里:name 中url变量
var name = ctx.params.name; //获取url中的变量
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});
router.get('/', async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>`;
});
app.use(router.routes()); //把路由middleware添加到 app中
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
使用koa-bodyparser解析 request的body 完成 post请求
const Koa = require('koa'); //这里导入的Koa是一个class 不能直接使用
const app = new Koa(); //创建Koa对象表示web app本身
//路由模块
const router = require('koa-router')(); //和前面的app是一个道理 这里不过是简写
//解析request的body的模块 主要针对post请求
const bodyParser = require('koa-bodyparser');
router.get('/', async (ctx, next) => {
ctx.response.body = `
<h1>Index</h1>
<form action="login" method="post">
账号:<input type="text" name="name"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="登陆"><br/>
</form>`;
});
router.post('/login', async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`login with name:${name}, password:${password}`);
if(name === 'koa' && password === '12345'){
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
}else{
ctx.response.body = `
<h1>登陆失败</h1><br/>
<a href="/">重新登陆</a>`;
}
});
//需要注意的是 由于中间件是按先后顺序执行的 所以注册解析类要在注册路由之前
app.use(bodyParser()); //也是一个用法 调用这个解析类中间件 注册到app
app.use(router.routes()); //把路由middleware添加到 app中
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
三,重构
这个时候,服务器的启动,url的处理都写在一个文件里,就可以考虑把url的处理单独提出来写成一个模块。
先把url处理的的部分单独提出来写成模块,放在controllers文件夹下
var fn_index = async (ctx, next) => {
ctx.response.body = `
<h1>Index</h1>
<form action="login" method="post">
账号:<input type="text" name="name"><br/>
密码:<input type="password" name="password"><br/>
<input type="submit" value="登陆"><br/>
</form>`;
};
var fn_login = async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`login with name:${name}, password:${password}`);
if(name === 'koa' && password === '12345'){
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
}else{
ctx.response.body = `
<h1>登陆失败</h1><br/>
<a href="/">重新登陆</a>`;
}
};
module.exports = {
'GET /': fn_index,
'POST /login': fn_login
};
然后在主文件引用这个文件即可,考虑到controllers包下可能有很多的处理url的模块,主文件内需要一次性加载所有的这种模块。采取的办法是扫描controllers包下的所有js文件,然后根据每个js文件对外提供的对象来建立路由映射,具体做法如下:
const Koa = require('koa');
const app = new Koa();
const router = require('koa-router')();
const bodyParser = require('koa-bodyparser');
var fs = require('fs');
//考虑这里为什么可以用同步?
var files = fs.readdirSync(__dirname + '/controllers'); //因为只在启动时运行一次
//过滤出.js文件
var js_files = files.filter( (f)=>{return f.endsWith('.js');} );
//处理每个js文件
for(var f of js_files){
let mapping = require(__dirname + '/controllers/' + f); //引入js文件提供的模块
for(var url in mapping){ //这里url就是模块对外提供的名称
if(url.startsWith('GET ')){
var path = url.substring(4);
router.get(path, mapping[url]);
}else if(url.startsWith('POST ')){
var path = url.substring(5);
router.post(path, mapping[url]);
}else{
console.log(`无效的url: ${url}`);
}
}
}
app.use(bodyParser());
app.use(router.routes());
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
进一步简化主文件,让功能更加单纯,把扫描controllers包 和 根据js文件建立路由映射这部分也提出来作为一个模块:
var fs = require('fs');
//扫描 controller包 导入启动的模块
function addControllers(router){
var files = fs.readdirSync(__dirname + '/controllers');
var js_files = files.filter( (f)=>{return f.endsWith('.js');} );
for(var f of js_files){
let mapping = require(__dirname + '/controllers/' + f);
addMapping(router, mapping);
}
}
//根据一个模块对外提供的名称和函数 做路由映射
function addMapping(router, mapping){
for(var url in mapping){
if(url.startsWith('GET ')){
var path = url.substring(4);
router.get(path, mapping[url]);
}else if(url.startsWith('POST ')){
var path = url.substring(5);
router.post(path, mapping[url]);
}else{
console.log(`无效的url: ${url}`);
}
}
}
//这样 在app.js中调用的时候直接 app.use(require('./controllers')());即可
module.exports = function(dir){
let
controllers_dir = dir || 'controllers',
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
}
主文件进一步简化成:
const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());
app.use(require('./controllers')()); //把路由middleware添加到 app中
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
四,使用模板引擎
模板引擎,讲到模板引擎,需要展开一下,模板引擎大类可分为两类,服务器端模板引擎和浏览器端模板引擎。
如果使用java做web开发 一定接触过服务器端模板引擎,JSP就是一个服务器端模板引擎。这个服务端和浏览器端的主要是工作原理不一样。服务器端是指:在服务器端通过使用的服务器语言来拼接html字符串,返回给客户端的手法。
相应的,浏览器端模板引擎指的是,数据是在前端由JS语言渲染上模板的,服务器端指负责提供数据即可,现在。
这里既然是学node.js,自然就是使用服务器端模板引擎,Nunjucks。
<!-- base.html -->
<html>
<!-- 用于继承的基类 -->
<body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>
</html>
<!-- hello.html -->
{% extends 'base.html' %}
{% block header %} <h1>{{ header }}</h1> {% endblock %}
{% block body %} <p>{{ body }}</p> {% endblock %}
const nunjucks = require('nunjucks');
//自定义一个创建模板引擎对象的方法
function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader('views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}
//创建Nunjucks模板引擎对象
var env = createEnv('views', {
watch: true,
filters: {
hex: function (n) {
return '0x' + n.toString(16);
}
}
});
//使用引擎对象的render(view, model方法把数据添加到模板上
var s = env.render('hello.html', {
header: '我是标题',
body: '我是内容'
});
console.log(s);
输出的html:
<html>
<!-- 用于继承的基类 -->
<body>
<h1>我是标题</h1>
<p>我是内容</p>
<div>copyright</div>
</body>
</html>
五,综合上述的Koa2和Nunjucks 使用服务器端MVC架构模式
所谓的MVC 就是控制交给controllers来完成 视图交给Nunjucks模板来完成 至于数据这块暂时暂时没有涉及
修改controllers下的文件 之前是直接写html字符串 现在只做逻辑控制 然后直接跳转到相应视图 这里跳转的方法就是render
//ctx的render方法 是templating模块 充当的中间件给ctx添加的
var fn_index = async (ctx, next) => {
ctx.render('index.html', {
title: '登陆'
});
};
var fn_login = async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`login with name:${name}, password:${password}`);
if(name === 'koa' && password === '12345'){
ctx.render('login-success.html', {
title: '登陆成功!',
name: 'koa'
});
}else{
ctx.render('login-failed.html', {
titls: '登陆失败!'
});
}
};
module.exports = {
'GET /': fn_index,
'POST /login': fn_login
};
templating.js模块 是中间件 为ctx添加上render方法 具体的实现方法如下
const nunjucks = require('nunjucks');
function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path || 'views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}
//该方法是一个函数 调用之后返回一个middleware 这个中间件会给ctx添加一个render方法
function templating(path, opts) {
// 创建Nunjucks的env对象:
var env = createEnv(path, opts);
return async (ctx, next) => {
// 给ctx绑定render函数:
ctx.render = function (view, model) {
// 把render后的内容赋值给response.body:
ctx.response.body = env.render(view, model);
// 设置Content-Type:
ctx.response.type = 'text/html';
};
// 继续处理请求:
await next();
};
}
module.exports = templating;
最后在服务器启动文件中 为web app注册上述中间件即可
const Koa = require('koa'); //这里导入的Koa是一个class 不能直接使
const app = new Koa(); //创建Koa对象表示web app本身
//需要注意的是 由于中间件是按先后顺序执行的 所以注册解析类要在注册路由之前
app.use(require('koa-bodyparser')()); //也是一个用法 调用这个解析类中间件 注册到app
//导入继承Nunjucks的中间件 这个中简件只做一件事 就是给ctx添加一个render方法 这也是整合Nunjucks的核心
const templating = require('./templating');
app.use(templating('views', {
noCache: false,
watch: true
}));
app.use(require('./controllers')()); //把路由middleware添加到 app中
app.listen(3000);
console.log('server is running at http://127.0.0.1:3000/');
到这里,就一步一步地把Nunjucks整合进了koa2。总体上来说还是很简单的。