Node.js由简入繁

 

目标是由最简单的东西开始,一步一步理清楚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异步加载。可参考

最后一些常用的模块有:

  1. http     http连接
  2. fs    文件处理
  3. stream    流
  4. process   nodejs进程
  5. 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。总体上来说还是很简单的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值