精华 深夜放毒——阿里开源的企业级Node框架Egg使用指南
发布于 7 年前 作者 hyj1991 79409 次浏览 来自 分享

I. 写在前面

Egg框架开源都快2个月了,嗯,本以为能看到一些讨论的,结果等了一个月完全没见到大家写点和这个相关的东西,加上官方文档实在是。。。 回归正题,作为国内不错的互联网公司,而且据说也是国内最早开始在生产中使用Node的大佬,我觉得不管外界如何评价,阿里开源的东西还是值得学习下的,最近读了一些里面的实现代码,有一些思路还是很值得公司自己编写组织强约束的Node框架学习的,所以这里算是抛砖引玉,写个基本的controller、router、service、自定义middleware和第三方的koa 1.x插件如何转换应用到egg中来。 本文基于的egg版本为v0.2.1。

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.exports = appInfo=>{
	return {
		//“middleware”是固定的key不可变,值是一个数组,数组中每一个元素都表示开启的中间件名称(将上面的文件面进行小驼峰转换)。
		middleware: [‘timeAccess’],
		//中间件所需的参数,key同样是上面的文件面进行小驼峰转换后的字符换,value就是中间件执行需要的参数。
		timeAccess: {key: ‘Cost Time: ’}
	}
}

这样启动项目后,该中间件就生效了。config下文件编写可以参看V大节,我们这里主要来看下自定义中间件的写法,和参数的含义。 可以看到time_access.js导出的是一个匿名函数,该函数的两个参数options和app,其中options对应的就是config.default.js中的timeAccess的值,这里是{key: ‘Cost Time: ’},app则是本节第二小节中所描述的app实例,并且要使得该中间件生效,必须在config.default.js中的middleware值对应的数组里面有:’timeAccess’。 看到这里你也许会有疑问,我的文件名字明明是time_access.js,为啥config配置中所有填写都是timeAccess呢,这里和controller一样,Egg框架为了统一变量的风格,所以会自动的对文件名进行小驼峰转换,那么time_access转换为小驼峰就是timeAccess。 最后这个匿名函数执行后,返回的是一个标准的koa 1.x的中间件,写法用法和koa 1.x的中间件完全一致,这一块不清楚的可以看Koa 1.x的官方文档学习下。

六. app/public

这个文件夹下存放的是Web服务器所需的静态资源,这一块没什么好说的,随意放,访问使用/public/xxx就行了,xxx为你的静态资源在public目录下的相对路径。比如我们在app/public下新建了一个css文件叫index.css,则我们可以这样访问下载该文件: 127.0.0.1:7001/public/index.css

七. app/service

好吧,终于到了service层了,这一块属于Egg设计的一个服务层,具体来说相当于带了业务逻辑的model,数据的获取和数据的组装都可以在service中完成,然后对于controller函数来说可能就只有两个动作了:

  • Service调用
  • Page页面渲染

这样整体的逻辑看起来会更清楚一些。我们以一个例子来理解下service文件的编写方式,这个例子做的比较简单,全量获取百度主页的数据,文件名为BaiduService.js:

module.exports = app=>(class BaiduService extends app.Service {
	constructor(ctx) {
    	super(ctx);
    	this.config = this.app.config;
	}
	
	* getBaiduHomePage() {
    	let data = yield new Promise((resolve, reject)=> {
        	require('request').get('http://www.baidu.com', function (err, res, data) {
            if (err) return reject(err);
            	return resolve(data);
        	})
    	});
    	return data;
	}
});

可以看到,BaiduService.js导出的依旧是一个匿名函数,该匿名函数的参数app就是本大节中第二小节所描述的app。 这个匿名函数由Egg框架执行后,返回的则是一个class,这个class继承自app.Service,其实在app.Service中,仅仅是对this.ctx和this.app赋值而已。 返回的BaiduService类的构造函数没什么好说的,既然是继承了,必须先super(),然后就可以直接使用this.app来获取app对象,以及this.ctx来获取本次请求的context对象(这个context对象就是中间件和路由中的this对象)。 那么我在这里定义了一个获取百度主页数据的generator成员函数,至此,整个service层样例已经写完了。接下来我们看看如何在controller中调用:

module.exports = function *myHelloController() {
	let data = yield this.service.baiduService.getBaiduHomePage();
	this.body = data;
};

可以看到,Egg框架将BaiduService这个类new了一个实例,挂载到了this.service.baiduService属性上,因此我们可以直接按照上述的方式调用我们编写的数据获取方法,而且这里的BaiduService.js同样被自动转换成小驼峰的baiduService,所以在controller中的调用形式为:

this.service.小驼峰文件名.成员函数名

这里的类名是无关紧要的,调用依旧以service目录下创建类的文件名为绑定的key。 这里要多说一句,经过源码阅读,理清了service加载的逻辑后,可以看到阿里的开发组成员在Service层的逻辑设计还是蛮用心的,this.service和this.controller不一样,它是在每一次请求中第一次使用到时才会new出来的;并且刚才在app/service目录下编写的类也一样,仅在每一次请求中第一次使用到该类时才会实例化;而且不管是this.service还是编写的类,一次会话请求的生命周期中都是单例运行的,这样节省了new类的开销,提升了Egg运行的效率。

八. app/view

这一块没什么好说的了,模板引擎该怎么用就是怎么用的,但是就和本大节中第四小节描述的那样,app/extend/helper.js中的方法会自动merge到模板执行的locals中,因此在nunjucks中可以使用:

{{ helper.xxx }}

在ejs中可以使用:

<% helper.xxx %>

来调用编写到helper.js中的公共方法。

九. app/router.js

终于到了app目录下的最后一个文件了,顾名思义,router.js是编写路由的文件,编写形式如下:

module.exports = app => {
	app.get('/home', app.controller.home);
	app.get('/myPage', app.controller.myHello);
	app.redirect('/', '/news');
	app.get('/news', app.controller.news.list);
	app.get('/news/item/:id', app.controller.news.detail);
	app.get('/news/user/:id', app.controller.news.user);
};

这里依旧是返回的一个匿名函数,参数为app,其实整个Egg的加载逻辑大同小异,花点时间搞清楚controller/service/middleware/config中的一块,别的模块也很容易读懂。 那么这里Egg显然做了一些处理,使得基于Koa 1.x的路由编写看起来和express的风格一致。 由于在本大节第三小节中的controller编写和加载已经阐述过了,我们所编写的路由文件最后会被挂载到app.controller属性下,因此可以直接使用

app.get(‘/index’, app.controller.xxx)

的形式来编写路由函数。

VII. 结语

写这东西,也算是挑战了下自己,本文总共5200多字全部纯手打; Egg框架总体来说设计思路还是非常值得借鉴的,在公司内部协作中,使用这样的强约束框架更能统一风格,提升项目阅读和维护性。 还有就是我现在源码逻辑理的比较清楚集中在master进程fork出来的app子进程,但是对于agent子进程的作用不是很清楚,我大致看了下agent的实现,似乎目前给出的仅仅用到本地开发时由agent监听几个文件目录——只要这些文件目录下的文件发生变更,就由agent来重启app子进程。 最后的是parent——master——app——agent四者间的通信的意义也没有了解的比较清楚,希望社区有阿里的大神看在我写的辛苦的份上来给我解答下~

62 回复

虽然估计深夜不会有人在了,但是还是说一下吧: 因为本文比较长,所以断断续续上传,想看的可以直接等明天起来看~

沙发,先mark

来自酷炫的 CNodeMD

其实也不怎么样的一个框架,可能就因为是啊系受人关注吧。 From Noder

@zouzhenxing 思路拿来借鉴下还是可以的,没必要全盘照搬哇:)

@hyj1991 仔细拜读完文章,写得很不错,可以看出非常仔细看过源代码。egg目前文档真是非常惭愧,我们已经在完善文档中,会尽快跟随1.0一起发布。其实你文章中已经说出egg最核心的东西了,其实用不用egg真的无所谓,最重要是一个大规模的团队需要遵循一定的约束和约定。目前能看到的文档就是egg的实现规范和约定 https://github.com/eggjs/egg/blob/master/SPECIFICATION.zh_CN.md ,里面也会说明agent进程的作用。例如alinode监控的任务调度,就可以通过agent实现了。 自豪地采用 CNodeJS ionic

@fengmk2 谢啦,有空我再仔细看看这个里面的描述 我其实对这些开源框架的建议都是,从源码理解其实现,然后根据自己的实际情况借鉴值得借鉴的部分来应用到自己公司的项目中去,哈哈

@hyj1991 仔细派读了几遍, 写的非常不错~~ 非常赞. 对于文档的事, 非常抱歉.

关于文中的一些疑问:

parent——master——app——agent

  • agent 是一个特殊的进程, 专门做脏活累活的.
    • egg-development 里面用它来监控文件修改后重启 worker;
    • egg-schedule 里面用来计算时间, 然后通知 worker 执行任务
    • 如 egg-configserver 这类场景是需要跟后端保持配置同步, 此时就只需要 agent 定时同步, 然后消息给 workers
  • parent 是给启动脚本用的, 如果只是 npm start 的话用不上
  • master 一般不用理
  • app 即 worker, 默认启动 cpu 个数

其他的一些:

  • 启动建议用 npm run dev
  • NODE_ENV 那个, 在最新版里面是 production 对应 prod 的. 一般我们都是用 EGG_SERVER_ENV 启动.
  • app 实例是可以拿到的, 在根目录写个 app.js module.export = app => {}
  • egg 内建不绑定 view 实现, 只内置了约定, 具体的模板引擎需要使用者自行引入, 如 egg-view-nunjucks
  • require('request') 不需要, 内置了 urllib, 直接调用 this.curl(url, opts) 即可
  • egg 还有个 framework 机制, 一般建议团队封装一个适合自己业务场景的框架层(egg + plugin集合 + 自定义逻辑), 来给到上层应用开发者来使用.

最后~ 有没有兴趣加入 eggjs group?

卧槽,好文啊,有后续吗?

@atian25 谢谢你的回答哇,我看agent部分的时候,确实看到了配置一些目录后由它进行的自动重启,提到的另外两个egg-schedule和晚egg-configserver模块上我再翻翻源码学习下设计思路~ 对于您提到的:parent是给启动脚本用的,指的是egg-bin下的dev/debug/test/cov四种启动方式吗,这一块也大致看了下,确实是使用fork子进程的方式启动了start-cluster这个入口文件,那么这里的parent指的就是egg-bin脚本启动的进程吧。

后面的几点我看了下,确实是看源码的时候没有想清楚,app是可以拿到的。在/mixin/custom.js里面的loadCustomApp()方法提供了,参数不带inject并且是函数的话会传入this.app执行该函数。 egg不绑定view实现倒是看到了的,可能上文对view的编写描述的太简陋了。Egg设计的大致流程应该是view插件的extend/applicaton.js创建一个Symbol.for(‘egg#view’)]属性对应View引擎类,然后在egg的extend/appliction中创建的view基类实现了一些类似setLocals等基本方法,再继承上述的真正的模板引擎类,render方法则调用的是父类的render方法(super.render)。

然后提到的framework机制我回头要仔细看看,之前也只看到了egg-init能生成framework,但是没去理解它的作用,我其实当时还在想,app.Service要是能对外暴露一个接口,让公司自己实现一些核心接口调用进去,那开发者直接编写service层的东西继承app.Service就能更加方便的本土化了。听描述framework就是定制这一类的公司私有业务吧

我本人其实非常有兴趣加入eggjs的开发哇,其实看完了egg的实现后,我原本打算自己写一个koa 2.x的升级版(只包含application,用PM2管理),因为核心的重载createContext方法的思路在koa 2.x中没有问题,koa2.x主要是它核心的compose方法被改了,导致中间件写法的一些区别。话说要怎么样才能参与现在的Eggjs呢:)

最后还是一个问题:是关于agent的,按照您描述的agent设计思路,能否将一些cpu密集的复杂运算丢给agent做(以前),这样节省主业务app进程的资源呢?

@rwing 如果后面有空的话,我打算写一个Egg的核心源代码解析,主要用来给感兴趣的朋友参考借鉴下其设计思路,或许能在自己的团队或者项目中使用到一些呢:)

@hyj1991 谢谢,再问下,还有其他类似的适合企业的框架吗,从其他语言转node需要一些项目来看看

@rwing 强约束的Node框架国内还有貌似是360搞的thinkJS,这个感觉怎么说呢,你用一下就明白了。弱约束的MVC框架主流就是Express和Koa两个了,这两个框架基础功能完备但是你要拿来给自己公司或者项目团队使用,尚且需要大量定制,就像楼上说的那样最重要是一个大规模的团队需要遵循一定的约束和约定

@hyj1991 多谢 我也是这么想的,express和koa确实对于企业应用来说还缺失很多,我看看thinkjs,国外没有吗?

@hyj1991 问个小白问题… 为啥我用vs code直接启动index.js,就失败。 在命令行内打node index.js或者npm run dev都木有问题

@shadow88sky 没用过vs code哇…话说错误提示是什么呢,还有vs code的Node环境变量配置对吗

@rwing 国外不清楚了哇,我们公司现在的Node服务端框架都是自己写的,定制一些私有业务在里面,并且会考虑到公司的自动化发布系统做一些适配~

@shadow88sky 失败是啥意思,要看具体报错,我一直再用vscdoe

缩进看着好难受。

@rwing 意思是没法进vscode的debug模式。
按ctrl+F5是可以正常启动并访问的

@hyj1991 github 已邀请. framework 那块, 如下图. (摘自我们团队在 jsconf 的 slide) egg-architecture.png 其实对于一线开发者来说, 不会直接接触到 egg 的, 团队的架构师或技术负责人, 可以封装一个适合自己业务场景的框架. 一个大规模的团队需要遵循一定的约束和约定, 这个约定的产出物的落地就是 framework.

  • 如 chair 是适合蚂蚁那边的业务场景的, 集成了很多他们内部的插件, 提供了很多蚂蚁+阿里的基础能力.
  • 如 nut 是适合 UC 这边的, 支持了我们的私有云/自动发布系统/前端工程化体系等很多特性.
  • 还有适合开发自己公司内部应用的, 集成了登陆鉴权/操作日志/权限控制等插件, 这样开发者就不用太多配置, 框架层会做好适配
  • aliyun-egg 是开箱即用的, 适合托管到阿里云平台的 egg 上层框架, 集成了 alinode / oss / rds 等等插件.
  • 未来也可以有人封装一个 social-egg 之类的框架, 直接集成了 wechat/支付宝/qq 等功能来做一个开放平台后台的框架, 都有可能.
  • 插件和框架里面除了 controller 和 router, 和应用区别不大
    • 故我们甚至可以在框架里面集成一些公共的 service / helper 等逻辑
    • 集成公共插件
    • 还可以用 egg 的 loader 机制去加载自定义文件,
      • app/model/*.js -> app.model.user.create()
      • app/db/**/*.js, app/rpc/**/*.js
      • egg-view-nunjucks 里面实现的 app/filters/*.js 文件的加载
  • egg 本身是不包含阿里内部任何逻辑的, 并且具备高度的可定制性, 故有兴趣的同学都可以在它之上搭建适合自己的框架, 不一定要从头来过, 毕竟这是一个轮子套轮子的前端时代. 专业的人做专业的事, 社区可以去共建这些插件, 如 egg-security

参与到 egg 的方式其实很简单:

  • egg 完全遵循 github 的开源协作模式
    • 所有的提案和讨论, 都通过 github issue 的方式去讨论
    • 我们倡导并亲身践行这种 硬盘式异步协作模式
      • 曾经无数次我对 egg 的某个设计点有疑问时, 有人会丢给我一个 url, 去看当时大家的讨论和结论
      • 曾经无数次在深夜看到来自十几个不同团队的参与者, 在 issue 里面讨论的热火连天
  • 参与方式
    • 使用 egg, 遇到问题或好点子, 给我们发 issue
    • 编写插件并开源给社区
    • 使用心得 && 文档补全
    • 通过 pull request 的方式去贡献代码

  • parent 没场景的时候其实可以无视掉, 可以是你自己的一个启动脚本, 如这边有个团队的启动脚本里面, 会同时启动 nginx 和 egg, 所以在 parent 里面需要通过事件获取到 master/agent/app 的一些运行状态. 这个脚本以前是 shell, 现在都是 node 写的, 因为后者才能被单元测试覆盖.
  • agent 那个, 看具体场景吧, 从以往实践来看, 总会需要有一个进程来为大家做一些事, 如上面说的几点, 还有类似 egg-logrotator 里面切割日志文件等场景. 所以 egg 单独把这个进程的概念明确出来.

@rwing 参见 https://github.com/atian25/blog/issues/15

目前的 vscode 对多进程的 debug 支持不好, 要等下个版本.(https://github.com/Microsoft/vscode/issues/13342)

个人建议是在外面 npm run dev -- --debug 然后 vscode 里面 attach.

或者直接 npm run dev -- --inspect 用 chrome 来 debug

很类似于SailsJs框架 个人表示基本没啥新鲜的

@atian25 多谢,不过vscode的迭代很快,下一个版本11月初就发了吧

@atian25 可是我vscode 开启egg的时候,只启动1核啊

@shadow88sky 上面有讨论过, egg 开发模式下默认会启动三个进程, 一个 master, 一个 agent, 一个 worker, 已经是 cluster 了.

这是 vscode 目前的局限性, 没办法, 绕过的方法可以看我上面给的 blog 链接里面的探索

@atian25 这样就了解了 ,灰常感谢

目前来讲,是开源的Node框架中最强的了, 主要是参与到这个项目中的大神实在太多, 而且承载阿里太多的业务,有着良好的扩展性,稳定性也有绝对的保障。

站在开发者的角度,比官方文档通俗易懂多了,赞一个

没Think 好

感觉很用心的一个作品

来自酷炫的 CNodeMD

还需要加油啊,感觉文档没有thinkjs完善,设计也与thinkjs差不多,另外thinkjs也有一些很人性的设计。

看了 eggjs.org 上的文档,一脸茫然

看了 eggjs.org 上的文档,一脸茫然 如果团队有时间, 希望在提供一套 功能\流程 相对完全项目, 也提供 egg-init 生成, 觉得这样会降低新手的学习成本.

@maosiyu egg-init 脚手架一直都有哇,而且现在 egg.js 的文档比较全了吧,你是不是看错网站了。。。

参考: egg 文档 egg-init 脚手架

@hyj1991 他应该是想要完整的全站 demo,包括前后端,甚至包括 webpack

@atian25 好像 CNODE 社区也有比较完整的项目吧,但是直接拿来用肯定是不行的,还是要理解下文档的 API 这些才能做

有深度,收藏了。

@hyj1991 大神 我举个例子, 在项目运行时的 500 错误页面, 这个我如果想要换成 友好界面, 应该怎么做, 我在API上只看到了404的配置, 但500不应是这样的配置吧

@atian25 谢谢你 找到了

// config/config.default.js
module.exports = {
  onerror: {
    // 线上页面发生异常时,重定向到这个页面上
    errorPageUrl: '/50x.html',
  },
};

@hyj1991 在问个问题 官方API 对service 的一段说明, 不是很理解 一个 Service 文件只能包含一个类, 这个类需要通过 module.exports 的方式返回。 注意事项

但是向我这样写 也没错啊, 感觉代码更少了, 更清晰, 那么 这样写会有什么问题呢?

service/user.js

module.exports.find = async (uid) => {
  // console.log(uid);
  // const user = await this.ctx.db.query(`select * from user where uid = ${uid}`);
  // return user;
  return 'find_SUCCESS';
};

module.exports.create = async () => {
  // console.log(uid);
  // const user = await this.ctx.db.query(`select * from user where uid = ${uid}`);
  // return user;
  return 'create_SUCCESS';
};

controller/user.js

module.exports.find = async (ctx) => {
  ctx.body = await ctx.service.user.find(ctx.params.uid);
  ctx.status = 200;
};

@maosiyu commonjs 方式的 Service 是支持的,但我们更推荐 class 的方式,commonjs 的方式没有你给出那个链接上 属性 里面这些功能了,而且你也不能定义自己的 Service 基类来做一些共享的事。

@maosiyu 都支持的,我觉得这属于项目的一种约定吧

@atian25 谢谢你, 我理解你说的意思了, class 的这种写法更像 java的面向对象写法了, 遵从作者, 我也使用class的方式开发

@hyj1991 非常感谢作者, 解决了我好多问题,非常感谢

@maosiyu 更应该谢谢 eggjs 的作者们呀: @atian25,哈哈

@atian25 实在不好意思在问一个问题 官方api示例:

module.exports = app => {
  class User extends app.Service {
    * find(uid) {
      const user = yield this.ctx.db.query(`select * from user where uid = ${uid}`);
      return user;
    }
  }
  return User;
};

我把Generator 换成了 ES7的Async/Await , 想了解一个, 官方没有这么做的原因是什么?

module.exports = app => {
  class User extends app.Service {
    async find(uid) {
      let user = await this.ctx.db.query(`select * from user where uid = ${uid}`);
      return user;
    }
  }
  return User;
};

@maosiyu async / await 还没有应用到 LTS 版本,v7.5.0 以上才能开启,但是目前的 Node 8 尚未稳定到 LTS 版本 因此线上推荐的还是用 v6.x 的 LTS 版本,这个版本不支持 async/await 特性吧

@hyj1991 确实api在一开始就提到过这个问题, 之前为了体验一下node v8.0.0, 所以自己开发时用的是 node 8的版本, 是我自己弄错了 抱歉 .

有什么问题么?目前支持 async 的。只是 lts 前我们只会推荐 gernerator

@atian25 暂时没有了, 谢谢你的耐心帮助!

新人,正准备撸蛋,很不错的学习资料。感谢楼主

** 5年的文章 也是极好的**

回到顶部