【打造一个nodejs Web框架(1)】搭建框架

前言

最近有点小空,打算做一个nodejs框架玩玩。作为一个一直被Java和PHP荼毒的developer,所以框架大体会参考PHP的CI,ThinkPHP和JAVA的Jfinal。

预备

其实一个WEB框架并不难,按预定规则搭好能接收request处理request再返回response,就已经是一个框架了,而难就是难在如何完善整个框架,去打磨每一个环节,例如页面缓存、数据缓存、与数据库交互是ORM还是active record等等等等一大通问题也是决定一个框架质量的因素。

作为一个练手项目,我这里就没有太远大志向了。。第一篇的目的就是显示一下Helloworld,至于框架概念的话也就用我们熟悉的MVC。

入口

npm init 项目之后我也加进了babel(被JAVA和PHP荼毒太深,还是希望写近JAVAPHP语法的class)

图1. 框架文件架构

如图1所示,这是框架的物理文件架构。app里面装的就是controller,view和model三个文件夹,里面放的就是用户的application文件。core装的是框架的核心文件。node_modules不用说了吧。test装的是测试文件。

index.js 就是框架的入口文件。

/**
  *  BrownJs
  *
  *  @author SevensChan
  *
 **/
import Brown from './core/Brown.js'

/**
 *   Server Config
**/ 
const HTTP_IP = '127.0.0.1';
const HTTP_PORT = 8899;
var brown = new Brown();
brown.listen(HTTP_PORT,HTTP_IP);
brown.set("view engine","ejs");

主要是调用了core里面的核心文件Brown

import http from 'http'
import router from './Router.js';

let instance = null;

class Brown{

	constructor(){
		if (!instance){
			instance = this;
		}

		this.config = [];

		return instance;
	}
	
	static getInstance(){
		if (!instance){
			instance = new Brown();
		}

		return instance;
	}

	set(key,val){
		this.config[key] = val;
	}

	get(key){
		return this.config[key];
	}

	listen(port,ip){
		try{
			var server = http.createServer(function(req,res){
				router.handle(req,res);
			});
			server.listen(port,ip);
			console.log('Server Start Successfully');
		}catch(err){
			console.log(err);
		}
	}
}

export default Brown;

Brown目前主要作用是用来记录config参数,以及开启服务器(后面会有其他作用,这里先不提)

看到createServer中间会把所有请求都转向到router去处理,router是什么鬼,下面介绍

Router

把请求想象成一列火车,交叉路上就需要有一个中转站获取火车上的信息来决定火车要走向哪条轨道。
这个中转站就是我们需要的Router。

Router的主要作用就是读取request的参数来判断要调用哪个controller哪个方法去处理这个Request

常见的路由规则有
/Controller/Action/Param/querydata

/?controller=x&action=x&param=querydata

在这里为了偷懒就用第二个好了

import url from 'url'
import fs from 'fs'
import qs from 'querystring';
import {firstUpperCase} from './Common.js'

//controller缓存
let Controllers = [];

exports.handle = function(req,res){
	req.requrl = url.parse(req.url,true);
	//请求参数
	var queryUrl = qs.parse(url.parse(req.url).query);
	//请求路径
	var path = req.requrl.pathname;
	//是否是静态文件
	if (/.(css)$/.test(path)){
		res.writeHead(200,{
			'Content-Type': 'text/css'
		});
		fs.readFile(__dirname+path,'utf8',function(err,data){
			if (err) throw err;
			//输出静态文件
			res.write(data,'utf8');
			res.end();
		})
	}else{
		//读取请求的controller、action
		var controller = queryUrl['cl'];
		var action = queryUrl['ac'];

		var controllerClass;
		var controllerObj;

		//如果没有指定controller、action 默认为index
		if (typeof controller === "undefined"){
			controller = 'index';
		}

		if (typeof action === "undefined"){
			action = 'index';
		}

		try{
			//是否有缓存
			if (Controllers[firstUpperCase(controller)]){
				controllerObj = Controllers[firstUpperCase(controller)];
			}else{
				//没有缓存读取controller
				controllerClass = require('../app/controller/'+firstUpperCase(controller)+'Controller.js');
				controllerObj = new controllerClass['default']();
				Controllers[firstUpperCase(controller)] = controllerObj;
			}
			//设置request,response和请求参数
			controllerObj.setHttp(req,res,queryUrl);
			//执行action方法
			controllerObj[action]();
		}catch(err){
			console.log(err);
		}
	}
}

然后接下来controller就可以被call到了

Controller

别人用你的框架当然不能让别人什么都要自己写,所以要准备一堆父类的方法,别人的controller继承父类的controller就可以用到一大堆写好的函数,做一个函二代

import View from './View.js'
class Controller{
	constructor() {
		this.req = null;
		this.res = null;
		this.queryUrl = {};
		//设置view
		this.view = new View();
	}

	//设置request,response和请求参数
	setHttp(req,res,queryUrl){
		this.req = req;
		this.res = res;
		this.queryUrl = queryUrl;
		this.view.setRes(res);
	}

	//获取get请求的参数
	get(query){
		return this.queryUrl[query];
	}

	//获取post请求的参数
	post(query){
		//To Be Continue
	}

	//返回当前controller的view对象
	view(){
		return this.view;
	}
}

export default Controller;

在第一篇里面用到的东西并不多,就主要设置Request,response和获取参数和View对象,到这一步,来看一下用户要用controller的时候要怎么写

import Controller from '../../core/Controller.js'
class IndexController extends Controller{
	constructor() {
		super();
	}

	index(){
		var cl = super.get('cl');
		super.view().output("Hello World!" + cl);
	}
}

export default IndexController;

其实就这样了,super.get是父类实现的方法,获取到参数cl的值 然后通过view的output来输出。

View

view的话第一篇有两个目标,第一个直接输出内容,第二个使用一个模板引擎来实现

import Brown from './Brown.js'
import Engine from './view-engine/engine.js';
/**
 *   View 处理
 *
 **/
class View{
	constructor(){
		this.res = null;
	}

	setRes(res){
		this.res = res;
	}

	output(str){
		const body = str;
		this.res.writeHead(200, {
			'Content-Type': 'text/html' });
		this.res.write(body);
		this.res.end();
	}
}

export default View;

首先完成第一个目标,很简单,就实现一个output方法调用res去write内容

Hello World

到这里为止,我们可以测试一下了

图2. Hello World测试

完美!

使用模板

计划是自己写一个模板的,但是觉得这个可以放在最后再做,所以这里先实现和 ejs 模板整合

ejs 是js里面比较有名的前端框架,具体介绍:www.baidu.com/自己搜索

但是我们整合的话,为了以后可以使用到其他的模板引擎,是不能直接把ejs代码嵌入到我们的View中去的。因此,可以看到接口的作用了吧,通过定义一个模板引擎接口,提供固定的方法,我们的View只会关心和调用这些接口的固定方法,至于后面怎么实现,就另写引擎接口实现类去做。

翻了翻手册,发现 js ES6实现接口是屌麻烦的,因此,这些的接口定义先交给规范定义了(意思就是在使用文档上写 用这套框架的人要注意了,我的接口要这样这样写,错了别指望语法会提醒你了)

先写一个ejs引擎的实现类

import ejs from 'ejs'
import fs from 'fs'
class ejsEngine{
    //render方法显示模板
	render(res,page,data){
		var body = ejs.renderFile(__dirname +'/../../../app/view/'+page,data,function(err,result){
			if (!err){
				res.writeHead(200, {
				'Content-Type': 'text/html' });
				res.end(result);
			}else{
				res.end(err.toString());
				console.log(err);
			}
		});
	}
}
export default ejsEngine;

renderFile是ejs提供的方法,没什么好说的的,读取到内容就res显示

然后要和View整合的话,中间最好写一个工厂类,根据Brown的view engine配置去生成相应的引擎类

//引擎缓存
let Engines = [];

class Engine{
	static createEngine(engineType){
		engineType = engineType;
		if (Engines[engineType]){
			return Engines[engineType];
		}
		var engineClass = require('./engine/'+engineType+'Engine.js');
		Engines[engineType] =  new engineClass['default']();
		return Engines[engineType];
	}
}

export default Engine;

对了,就是这样子了,最后改装一下View

import Brown from './Brown.js'
import Engine from './view-engine/engine.js';
/**
 *   View 处理
 *
 **/
class View{
	constructor(){
		this.res = null;
		this.engine = Engine.createEngine(Brown.getInstance().get('view engine'));//1
	}

	setRes(res){
		this.res = res;
        this.data = https://my.oschina.net/u/203607/blog/{};
	}

	output(str){
		const body = str;
		this.res.writeHead(200, {'Content-Type': 'text/html' });
		this.res.write(body);
		this.res.end();
	}
   
    //2
	display(){
		this.engine.render(this.res,'index.ejs',this.data);
	}
    
    //3
	assign(key,val){
		this.data[key] = val;
	}
}

export default View;

分别多了1,2,3.
1. 注入一个由工厂类生成的引擎对象
2. 显示模板
3. 注入data

好了,改一下controller

import Controller from '../../core/Controller.js'
class IndexController extends Controller{
	constructor() {
		super();
	}

	index(){
		var cl = super.get('cl');
		super.view().output("Hello World! " + cl);
	}

	show(){
		super.view().assign('title','Hello World');
		super.view().display('index.ejs');
	}

	about(){
		super.view().assign('title','I am ABOUT');
		super.view().display('index.ejs');
	}
}

export default IndexController;

show和about会调用模板去显示内容,模板很简单这里就不放出来了,直接看看最后效果

搞定

 

结论

作为第一篇是比较简单的了,里面也没有太多的优化,主要把MVC概念清晰起来,然后下一篇就加入ActiveRecord,主要参考JFinal

 

赞 (0) 评论 分享 ()