1. 目录结构 #

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

2. 初始化项目 #

mkdir egg-news
cd egg-news
npm init -y
cnpm i egg --save
cnpm i egg-bin --save-dev

3. 添加 npm scripts 到 package.json: #

"scripts": {
    "dev": "egg-bin dev"
}

4. 跑通路由 #

├─app
│  │─router.js
│  ├─controller
│  │      news.js
├─config
│      config.default.js
|─package.json

4.1 配置路由 #

app/router.js

module.exports = app => {
    const { router, controller } = app;
    router.get('/news', controller.news.index);
}

4.2 编写控制器 #

app\controller\news.js

const { Controller } = require('egg');
class NewsController extends Controller {
    async index() {
        this.ctx.body = 'hello world';
    }
}
module.exports = NewsController;

4.3 编写配置文件 #

exports.keys = 'zfpx';

5. 静态文件中间件 #

6. 使用模板引擎 #

├─app
│  │─router.js
│  ├─controller
│  │      news.js
│  ├─public
│  │  ├─css
│  │  │      bootstrap.css
│  │  └─js
│  │          bootstrap.js
│  └─view
│          news.ejs
├─config
│   config.default.js
│   plugin.js

6.1 安装依赖的插件 #

cnpm install egg-view-ejs --save

6.2 启用插件 #

{ROOT}\config\plugin.js

exports.ejs = {
    enable: true,
    package: 'egg-view-ejs'
}

6.3 配置模板 #

{ROOT}\config\config.default.js

exports.view = {
    defaultViewEngine: 'ejs',
    mapping: {
        '.ejs': 'ejs'
    }
}

6.4 编写模板 #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="/public/css/bootstrap.css">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>百度新闻列表</title>
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-sm">
                <div class="card">
                    <div class="card-header">
                        百度新闻列表
                    </div>
                    <div class="card-block">
                        <ul class="list-group">
                            <%news.forEach(item=>{ %>
                                <li class="list-group-item">
                                    <a href="<%=item.url%>">
                                        <%=item.name%>
                                    </a>
                                </li>
                                <%})%>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

6.5 编写控制器 #

const { Controller } = require('egg');
class NewsController extends Controller {
    async index() {
        const news = [
            {
                name: '魅族:高不成、低不就 15系列的求变生存恐怕不易',
                url: 'https://baijia.baidu.com/s?id=1599513253231710086&wfr=pc&fr=idx_lst'
            },
            {
                name: '从应届技术男到百度VP,这是低调到没百科的吴海锋首次受访',
                url: 'https://baijia.baidu.com/s?id=1599508189171446369&wfr=pc&fr=idx_lst'
            }
        ]
        await this.ctx.render('news', { news });
    }
}
module.exports = NewsController;

7. 读取远程接口服务 #

在实际应用中,Controller 一般不会自己产出数据,也不会包含复杂的逻辑,复杂的过程应抽象为业务逻辑层 Service。

7.1 添加配置 #

config.default.js

exports.news = {
    url: 'https://baijia.baidu.com'
}

7.2 编写Service #

const { Service } = require('egg');
class NewsService extends Service {
    async list() {
        const result = await this.ctx.curl(this.config.news.url);
        let regexp = /<a href="(\/s\?id\=\d+[^"]+)".+>([\s\S]+?)<\/a>/g;
        let news = [];
        result.data.toString().replace(regexp, function () {
            if (!(arguments[2].includes('img') || arguments[2].includes('查看详情'))) {
                news.push({ url: 'https://baijia.baidu.com' + arguments[1], name: arguments[2] });
            }
        });
        return news;
    }
}
module.exports = NewsService;

7.3 编写控制层 #

//controller\news.js
const { Controller } = require('egg');
class NewsController extends Controller {
    async index() {
        const news = await this.ctx.service.news.list();
        await this.ctx.render('news.ejs', { news });
    }
}
module.exports = NewsController;

8. 扩展工具方法 #

app\extend\helper.js

const moment = require('moment');
moment.locale('zh-cn');
exports.relative = time => moment(new Date(time)).fromNow();

news.ejs

  <%=helper.relative(item.time)%>

9. 中间件 #

打印请求的处理时间

app\middleware\time.js

module.exports = (options, app) => {
    return async function (ctx, next) {
        const start = Date.now();
        await next();
        console.log(options.name + (Date.now() - start) + ' ms');
    }
}

config.default.js

exports.middleware = [
    'time'
]
exports.time = {
    name: '总耗时:'
}

10. 单元测试 #

测试文件应该放在项目根目录下的 test 目录下,并以 test.js 为后缀名,即 {ROOT}/test/*/.test.js。 请注意是放在项目的根目录下,而非app目录下 // {ROOT}/test/app/middleware/robot.test.js

const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/app/middleware/robot.test.js', () => {
  it('should block robot', () => {
    return app.httpRequest()
      .get('/')
      .set('User-Agent', "Baiduspider")
      .expect(403);
  });
});