专业 靠谱 的软件外包伙伴

您的位置:首页 > 新闻动态 > 企业级Nodejs软件开发Web框架Egg介绍 - 来至阿里!

企业级Nodejs软件开发Web框架Egg介绍 - 来至阿里!

2017-02-09 09:33:52

I. 写在前面

作为国内顶级的互联网公司阿里巴巴,而且据说也是国内最早开始在生产中使用Node的大佬,发布了一个企业级Nodejs软件开发Web框架Egg。

II. Quick Start

Egg是一个强约束的Node框架,这也会其和Express/Koa最大的不同,后者对开发者相对宽松,主要体现在目录结构,编写方式等均可以自定义。 Egg对目录结构等有一系列要求,幸运的是,虽然官方文档几乎是鸭蛋,但是Git上的官方人员还是很贴心的给我们送上了一个自动生成项目目录以及一些简单例子的方式,我们可以来看下:

  • 执行如下命令来安装egg-init,在*nix系统下有可能需要sudo权限:
    • npm install egg-init -g
  • 执行如下命令生成egg项目框架:
    • egg-init —type simple eggache
  • 执行如下命令进入生成的项目目录:
    • cd eggache
  • 执行如下命令,安装项目依赖:
    • npm install
  • 执行如下命令,启动egg项目:
    • node index.js

好了,到这里egg的样例已经运行起来,我们可以在浏览器中访问:

127.0.0.1:7001/news

来观察Hacker News的页面是否正常展现出来,如果页面正常展现,则表明安装成功。

III. 框架结构概述

用任意的IDE打开项目目录,可以看下大致的文件目录结构:

  • app:核心目录,controller文件夹、public静态资源文件夹,middle中间件文件夹,service数据处理组装文件夹、view层展现相关文件夹以及router.js路由文件都在此目录下,这里也是以后大家使用egg开发的主要场所。
  • config:核心目录,配置文件相关,其中config.default.js中存放的是和当前Node环境无关的配置;config.[env].js文件则存放和Node执行环境相关的配置;plugin.js存放的则是各个插件的package名称和是否开启的配置。这里的Node执行环境,后面会说明。
  • logs:日志文件输出的目录。
  • index.js:项目的入口文件。

这里大致介绍了下Egg框架的组成结构,后面会对两个核心目录app目录和config目录以及入口文件index.js文件的编写方式一一做介绍。

IV. index.js入口文件

我们从简单的地方开始介绍,首先是Egg框架的入口:index.js,当然文件名随意命名,这里使用的是II节中生成的官方样例。项目启动函数非常简单:

require('egg').startCluster({
	baseDir: __dirname,
	port: 7001, 
	workers: 1, // default to cpu count
});

可以看到,启动文件中引入egg包后调用其startCluster函数,并且传入参数就可以了。实际上经过源码分析,这里面的可以传入的参数完整的是这样的:

{
	customEgg: '',
	baseDir: process.cwd(),
	port: options.https ? 8443 : 7001,
	workers: null,
	plugins: null,
	https: false,
 	key: '',
	cert: '',
}

我们来逐个解析下:

  • customEgg:可选,指egg包所在的目录全路径,这个值框架默认会自动寻找填入。
  • baseDir:必选,执行egg框架所在的目录全路径,否则采用Node的启动路径。
  • port:必选,进程的端口号,默认https为8443,http为7001。
  • workers:可选,启动的子进程个数,默认和当前机器的cpu核数一致。
  • plugins:可选,插件的配置,如果填写必须填写插件配置的JSON字符串。
  • https:可选,默认为false。
  • key和cert:如果选择了https为true,则必选,必须填写可用证书路径。

V. config目录

一. config.default.js

这个文件主要是用来存放项目所需要的和Node执行环境无关的配置,比如你定义的项目中的一些常量,可以写到config.default.js中。这里关于Node执行环境详细的说明可以参看本节的ENV相关说明。 这个文件编写方式有两种模式,第一种是官方的示例:

module.exports = appInfo=>{
	return {
		//你需要添加的项目配置,下面是例子
		NAME:”EGG_ACHE
	}
}

可以发现,这个和我们一般编写的配置文件不一样,它exports出来的是一个匿名函数,并且该函数有一个参数appInfo,那么这个appInfo是什么呢? 经过查看egg的源代码(此处忍不住吐槽,0文档看起来真是累…),发现appInfo是Egg框架在自动加载配置文件时传入的一个对象,该对象结构如下:

{
        name: xxx,
        baseDir: xxx,
        env: xxx,
        HOME: xxx,
        pkg: xxx,
}

逐个关键字来说明:

  • name:项目的名称,也就是你的项目主目录的package.json中的name属性对应的值
  • baseDir:项目主目录所在的全路径
  • env:项目启动时配置的环境变量,具体参看本节后面的ENV相关说明
  • HOME:项目启动用户的根路径,process.env.HOME的值,是Node自动生成的
  • pkg:项目的package.json文件读取后得到的JSON对象

好吧,吐槽归吐槽,从这里可以看到设计团队想的比较周到,有了这个我们在写配置文件时可以方便的调用这些传入的参数了。 第二种就比较简单了,和普通的配置文件一样,直接使用exports或者module.exports将配置变量返回出来就行了。 Egg框架在配置文件的处理上比较强大,会自动判断是否为函数,如果是函数则会传入appInfo后执行。

二. config.${env}.js

这个文件则主要是用来存放项目中和环境相关的一些配置,比如在local下的接口A地址 配置为:http://a.org,在production下的接口A地址配置为:http://b.org,那么对于接口的A的地址配置来说,就需要分别写到config.local.js和config.production.js中。 该文件的配置内容写法和上一小节中的config.default.js写法完全一致,同样提供了两种配置文件的写法,关于Node环境相关更详细的可以看本节后面的ENV相关说明。

三. ENV相关配置文件命名

EGG中上述的Node环境,即ENV参数,是用来区分开发/测试/线上的不同配置的,经过查看代码,egg提供的三种环境配置的名称分别为:

  • local:本地开发环境
  • unittest:单元测试环境
  • production:线上生产环境

所以我们在config目录下的环境相关的配置文件可以命名为:config.local.js/config.unittest.js/config.production.js。 这些和env相关的配置文件,会在启动时和config.default.js,由egg依据当前运行设置的env自动merge成一个全局config。

四. ENV的设置

经过查看egg的源代码,可以看到egg框架的env可以采用三种不同的方式进行设置:

  • 项目主目录的config文件夹下新建serverEnv,注意该文件没有.js等后缀!然后将上述的local/unittest/production填写进去即可。
  • 读取process.env.EGG_SERVER_ENV的值,这种方式需要启动前加上env前缀,例如:EGG_SERVER_ENV=‘local’ node index.js。其实这种是正式部署应用比较推荐的环境变量设置方式。
  • 兼容+默认的做法,因为好多公司Node开发使用的env变量名称为NODE_ENV,所以egg判断process.env.NODE_ENV的值为test的话,则ENV更新为unittest;如果process.env.NODE_ENV的值为production的话,则ENV更新为default(?这个相当无厘头,我怀疑是代码写错了,看里面的config加载机制,config.defaule.js是一定会加载的,存储的也是和环境无关的配置);如果都不是,则更新当前的ENV为local。

VI. app目录

好了,前面的铺垫全部说完了,我们来看下最重要的app目录,以及如何编写app目录下的相关文件。

一. app目录结构概览

app目录下又按照设计模式分为了数个更细粒度的子目录,如下:

  • controller:存放controller层的处理文件的位置
  • extend:存放继承一些自定义公共方法的位置,这个在本节的下面详细说下
  • middleware:存放自定义中间件文件,所谓的appMiddleware
  • public:存放项目静态资源的位置
  • service:Egg框架抽象出来的一个概念,可以认为是带有逻辑处理的model层
  • view:存放页面模板文件的位置
  • router.js:编写路由的位置

文件结构大致描述了下,下面我们逐个目录的分析里面的文件的作用以及如何来编写。

二. Egg中隐藏的app实例

在讲解下面的目录结构时,我们必须首先弄清一个概念,那就是Egg框架中实际上有一个核心的app实例,地位和Koa以及Express中的app一致,但是我们在Egg框架中无法像Koa/Express那样直接获取到这个app(app 实例是可以拿到的, 在根目录写个 app.js module.export = app => {},框架支持这样使用app)。 我们来看下这个核心的app如何生成:

const Application = require(options.customEgg).Application;
const app = new Application({
	baseDir: options.baseDir,
	plugins: options.plugins,
});

本文不对这个Application类详细展开,我们只需要知道,这个Application最终继承自Koa,同时Egg框架重载了Koa中的createContext函数,熟悉Koa 1.x源码的朋友都知道,这个createContext函数返回的ctx即为所有中间件中的this对象。由于Egg中重载后的ctx其原型指向的是app.context,所以只要在app.context上的所有函数,均可以在所有中间件(包含路由处理函数)中使用this来直接调用。 为什么要特意说明下这个app呢,因为extend下的所有属性最终都会被框架自动挂载到app以及app.request/app.response/app.context/app.Helper.prototype上去。不理解这一点,就会很难理解中间件路由中的this对象和extend目录下的内容。

三. app/controller

这个目录下文件的概念和express以及koa的基本一致,就是路由调度的处理函数,如果文件仅仅想导出一个函数,编写方式如下:

module.exports = function *myHelloController() {
	this.body = 'Hello My First Egg Page!';
};

由于整个Egg是基于koa 1.x开发的,所以这里做过koa 1.x项目的开发的小伙伴就会很熟悉,和koa 1.x的路由处理函数写法完全一致。 如果controller下的一个js文件想导出多个路由处理函数,编写方式如下:

exports.funcA = funtion * (){}
exports.funcB = funtion * (){}

controller函数里面的this在上面的二节已经说明了,其行为基本和koa1.x一致。 最后,Egg框架会自动将你编写的controller函数挂载到app.controller属性下,挂载的格式为:key是app/controller目录下的文件名进行小驼峰转换为,value是导出的内容,以II节中官方示例为例,其app/controller下的home.js和news.js挂在后为:

app.controller = {   
	home: [Function: homeController],
		news:{ 
		list: [Function: newsListController],
    		detail: [Function: newsDetailController],
    		user: [Function: userInfoController] 
		} 
}

如果我们再命名一个文件叫做my_hello.js,内容就是本小节开头写的路由函数,则得到的挂在后的app.controller为:

app.controller = {   
	home: [Function: homeController],
		news:{ 
		list: [Function: newsListController],
    		detail: [Function: newsDetailController],
    		user: [Function: userInfoController] 
		} ,
	myHello: [Function: myHelloController]
}

看到没,my_hello.js这种风格的会自动被转换为小驼峰形式的名称! 那么到了这里,我们已经明白了如何编写路由文件,以及知道我们所编写的路由文件最后会被挂载到app.controller属性下。

四. app/extend

对于app/extend目录下的内容,如果理解了本大节的第二小节,就比较容易看懂了。app/extend下存在的对应文件分为5类,分别挂载到app的不同属性下:

  • app/extend/application.js:其导出的对象直接merge到app对象上
  • app/extend/request.js:其导出的对象直接merge到app.request对象上
  • app/extend/response.js:其导出的对象直接merge到app.response对象上
  • app/extend/context.js:其导出的对象直接merge到app.context对象上
  • app/extend/helper.js:其导出的对象直接merge到app.Helper.prototype原型上,作为原型app.Helper这个类的原型方法。

这里的1,2,3三个基本上普通开发者无需编写,对于第四点来说,context.js的内容由于最后会merge到app.context中,所以我们如果想在自定义中间件/路由处理函数中的提供一些公共方法,可以直接写到context.js中,然后在自定义中间件/路由处理函数中使用this直接调用,举个例子,context.js内容如下:

module.exports = {
	getAche(){
    	return 'EGGACHE';
	}
};

那么,我们在所有的中间件和controller函数中,可以直接调用this.getAche()来获取常量:EGGACHE 接下来是第五点中的app/extend/helper.js,导出的方法会merge到app.Helper类的原型上去,而且比较有意思的是:app.context.helper强制指向了app.Helper的实例(Egg做了单例模式),所以我们同样可以将公共方法写入helper.js文件中,然后在中间件/controller函数中使用this.helper.xxx的形式调用,举个例子,helper.js的内容如下:

exports.lowercaseFirst = str => str[0].toLowerCase() + str.substring(1);

我们可以在中间件/controller函数中使用this.helper.lowercaseFirst方法,对字符串第一个字母进行小写处理。 app/extend/helper.js还有一个和context.js不一样的地方在于,Egg框架默认将helper传入了模板引擎的locals参数中,所以在helper中定义的公共方法,我们在各种模板文件中同样可以直接调用,nunjucks中的调用形式为:

{{ helper.lowercaseFirst() }}

五. app/middleware

middleware的编写需要和config下的配置文件结合起来,才能编写并且使得一个自定义中间件生效。以一个例子说明,在app/middleware下新建time_access.js,内容如下:

module.exports = (options, app)=> {
	return function * timeAccess(next) {
    	console.time(options.key);
    	yield next;
    	console.timeEnd(options.key);
	}
};

然后在config/config.default.js中编写如下:

module.返回首页] [打印] [返回上页]   下一篇