作为国内顶级的互联网公司阿里巴巴,而且据说也是国内最早开始在生产中使用Node的大佬,发布了一个企业级Nodejs软件开发Web框架Egg。
Egg是一个强约束的Node框架,这也会其和Express/Koa最大的不同,后者对开发者相对宽松,主要体现在目录结构,编写方式等均可以自定义。 Egg对目录结构等有一系列要求,幸运的是,虽然官方文档几乎是鸭蛋,但是Git上的官方人员还是很贴心的给我们送上了一个自动生成项目目录以及一些简单例子的方式,我们可以来看下:
好了,到这里egg的样例已经运行起来,我们可以在浏览器中访问:
127.0.0.1:7001/news
来观察Hacker News的页面是否正常展现出来,如果页面正常展现,则表明安装成功。
用任意的IDE打开项目目录,可以看下大致的文件目录结构:
这里大致介绍了下Egg框架的组成结构,后面会对两个核心目录app目录和config目录以及入口文件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: '',
}
我们来逐个解析下:
这个文件主要是用来存放项目所需要的和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,
}
逐个关键字来说明:
好吧,吐槽归吐槽,从这里可以看到设计团队想的比较周到,有了这个我们在写配置文件时可以方便的调用这些传入的参数了。 第二种就比较简单了,和普通的配置文件一样,直接使用exports或者module.exports将配置变量返回出来就行了。 Egg框架在配置文件的处理上比较强大,会自动判断是否为函数,如果是函数则会传入appInfo后执行。
这个文件则主要是用来存放项目中和环境相关的一些配置,比如在local下的接口A地址 配置为:http://a.org,在production下的接口A地址配置为:http://b.org,那么对于接口的A的地址配置来说,就需要分别写到config.local.js和config.production.js中。 该文件的配置内容写法和上一小节中的config.default.js写法完全一致,同样提供了两种配置文件的写法,关于Node环境相关更详细的可以看本节后面的ENV相关说明。
EGG中上述的Node环境,即ENV参数,是用来区分开发/测试/线上的不同配置的,经过查看代码,egg提供的三种环境配置的名称分别为:
所以我们在config目录下的环境相关的配置文件可以命名为:config.local.js/config.unittest.js/config.production.js。 这些和env相关的配置文件,会在启动时和config.default.js,由egg依据当前运行设置的env自动merge成一个全局config。
经过查看egg的源代码,可以看到egg框架的env可以采用三种不同的方式进行设置:
好了,前面的铺垫全部说完了,我们来看下最重要的app目录,以及如何编写app目录下的相关文件。
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目录下的内容。
这个目录下文件的概念和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下存在的对应文件分为5类,分别挂载到app的不同属性下:
这里的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() }}
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.返回首页] [打印] [返回上页] 下一篇