Egg入門學習(二)---理解service作用

龍恩0707發表於2019-01-05

在上一篇文章 Egg入門學習一 中,我們簡單的瞭解了Egg是什麼東西,且能做什麼,這篇文章我們首先來看看官網對Egg的整個框架的約定如下,及約定對應的目錄是做什麼的,來有個簡單的理解,注意:我也是按照官網的來理解的。

egg-project
├── package.json
├── app.js (可選)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可選)
│   |   └── user.js
│   ├── middleware (可選)
│   |   └── xxx.js
│   ├── schedule (可選)
│   |   └── xxx.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

app/router.js 是使用與配置url的路由規則的。
app/controller/** 用於解析使用者的輸入,處理後返回響應的結果。
app/service/** 用於編寫業務邏輯層。
app/middleware/** 用於編寫中介軟體。
app/public/** 用於放置靜態資源。
app/extend/** 用於框架的擴充套件。
config/config.{env}.js 用於編寫配置檔案。
config/plugin.js 用於編寫需要載入的外掛。
test/** 一般用於單元測試。
app.js 一般用於啟動時候的初始化。
app/view/** 用於放置模板檔案,具體是做模板渲染的。
app/model/** 用於放置領域模型,由領域類相關外掛約定。如 egg-sequelize

如上就是官網中對egg目錄的約定,我們只需要在對應目錄中寫對應的程式碼即可,框架內部會自動會幫我們把內部程式碼組織起來,具體怎麼組織的,它的主要邏輯應該在 egg-core 中,在接下來的學習中,我會逐步學習egg-core原始碼來理解egg整個框架的原理的。
現在我們只需要知道就是這樣使用就行了。

下面我們來回過頭來看看理解下我們第一篇文章Egg入門相關的搭建 和渲染整個框架的頁面是怎麼樣的邏輯,上一篇文章我們是使用的是靜態資料來渲染頁面的,這邊文章我們使用 app/service 檔案下來使用ajax介面來獲取資料的demo。因為在專案當中資料不可能是我們寫死的,而是介面動態獲取的。
在上一篇Egg入門學習中,我們專案渲染整個目錄結構如下:

egg-demo2
├── app
│   ├── controller
│   │   └── home.js
|   |   |-- index.js
│   └── router.js
│   ├──public
|   | |---css
|   | | |-- index.css
|   | |---js
|   | | |-- index.js
|   |--- view
|   | |-- index
|   | | |-- list.tpl(模板檔案list)
├── config
│   └── config.default.js
└── package.json 

app/controller/home.js 程式碼如下:

const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'Hello world';
  }
}
module.exports = HomeController;

app/controller/index.js 程式碼如下:

// app/controller/index.js
const Controller = require('egg').Controller;

class IndexController extends Controller {
  async list() {
    const dataList = {
      list: [
        { id: 1, title: '今天是我第一天學習egg了', url: '/index/1' },
        { id: 2, title: '今天是我第一次學習egg了', url: '/index/2' }
      ]
    };
    await this.ctx.render('index/list.tpl', dataList);
  }
}
module.exports = IndexController;

app/controller/** 用於解析使用者的輸入,處理後返回響應的結果。 如上 home.js 和 index.js 使用是Es6的類來編寫程式碼,它都繼承了 egg中的Controller,其中index.js 定義了 dataList 物件資料,然後使用ctx.render把資料渲染到 模板裡面去。
這裡的模板就是 app/view/index/list.tpl的,在上面的目錄中,我們可以看到 view和controller是同級目錄的,在egg內部會直接找到view這個目錄的,然後對模板 index/list.tpl這個目錄進行解析。這就是 app/controller/** 的作用,它用於解析使用者輸入,然後把結果會渲染到模板裡面去,處理模板後就會返回響應的結果。

app/public/** 目錄的的作用是 用於放置靜態資源。比如css和js,然後在 app/view/** 中的模板檔案引入該資原始檔即可
在頁面中呼叫。

app/view/** 檔案的作用是用於放置模板檔案,具體是做模板渲染的。我們在 app/view/index/list.tpl 的程式碼如下:

<!-- app/view/index/list.tpl -->
<html>
  <head>
    <title>第一天學習egg</title>
    <link rel="stylesheet" href="/public/css/index.css" />
  </head>
  <body>
    <ul class="view-list">
      {% for item in list %}
        <li class="item">
          <a href = "{{ item.url }}">{{ item.title }}</a>
        </li>
      {% endfor %}
    </ul>
  </body>
</html> 

如上,在app/controller/index.js 中,我們把 dataList 物件渲染到該模板中,其中 dataList 物件中有一個list陣列。
因此在該模板中,我們直接使用 egg-view-nunjucks 模板引擎的語法來迴圈遍歷即可把資料渲染出來。

app/router.js 的作用是配置url路由規則的,程式碼如下:

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

在如上引數 app 可能會把 router, controller 等等都掛載該物件上面,因此也是使用es6語法把它匯入進來,然後使用router路由get請求,當我們訪問:http://127.0.0.1:7001/ 的時候,我們就會呼叫 controller.home.index 模板,也就是會找到app/controller/home.js 的檔案,然後呼叫裡面的 index()方法。即可執行。

當我們訪問 http://127.0.0.1:7001/index 的時候,我們就會呼叫 app/controller/index.js 的檔案,然後呼叫裡面的list方法,然後執行list方法,就會把資料渲染到對應中的模板裡面去,然後對應的模板就會對資料進行渲染,渲染完成後就會在頁面中返回對應的結果出來。

在專案中 會有一個config配置檔案,所有的配置寫在該 config/config.default.js 中,當然官網還有其他的配置檔案,比如叫:config.prod.js,config.local.js 等等。config/config.default.js 程式碼配置如下:

// 下面是我自己的 Cookie 安全字串金鑰
exports.keys = '123456';

// 新增view配置
exports.view = {
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.tpl': 'nunjucks'
  }
};

比如上面叫 export.view 是對 view下的模板檔案配置預設的模板引擎。其中mapping含義應該是對映的含義吧,應該是把模板引擎對映到有關 .tpl字尾的檔案中。

這就是之前那篇文章的所有的簡單的理解目錄結構。那麼我們知道之前那篇文章是資料是寫死在 app/controller/** 中的,但是在我們專案實際應用中,我們的資料不應該是寫死的,那就可能請求ajax介面,然後把介面的資料返回回來,我們再把對應的資料渲染出來。
從上面我們瞭解到 app/controller/** 用於解析使用者的輸入,處理後返回響應的結果。所以對於ajax介面請求具體的業務邏輯,我們複雜的業務邏輯不應該放在該目錄下,該目錄下只是做一些簡單的使用者輸入,那麼複雜的業務邏輯,我們這邊就應該放到 app/service/** 目錄下。因此我們需要把具體的業務邏輯程式碼寫到 app/service/** 中。

現在我們需要在 app/ 下新建一個 service目錄,在該目錄下新建一個 index.js 來處理具體的業務邏輯程式碼。

業務程式碼如下:

// app/service/index.js

const Service = require('egg').Service;
class IndexService extends Service {
  async list(page = 1) {
    // 讀取config下的預設配置
    const { serverUrl, pageSize } = this.config.index;

    const { data: idList } = await this.ctx.curl(`${serverUrl}/topstories.json`, {
      data: {
        orderBy: '"$key"',
        startAt: `"${pageSize * (page - 1)}"`,
        endAt: `"${pageSize * page - 1}"`
      },
      dataType: 'json',
    });

    const indexList = await Promise.all(
      Object.keys(idList).map(key => {
        const url = `${serverUrl}/item/${idList[key]}.json`;
        return this.ctx.curl(url, { dataType: 'json' });
      })
    );
    return indexList.map(res => res.data);
  }
};

module.exports = IndexService;

我們現在需要把 app/controller/index.js 程式碼改成如下:

// app/controller/index.js
const Controller = require('egg').Controller;

class IndexController extends Controller {
  async list() {
    /*
    const dataList = {
      list: [
        { id: 1, title: '今天是我第一天學習egg了', url: '/index/1' },
        { id: 2, title: '今天是我第一次學習egg了', url: '/index/2' }
      ]
    };
    */
    const ctx = this.ctx;
    const page = ctx.query.page || 1;
    const indexList = await ctx.service.index.list(page);

    await ctx.render('index/list.tpl', { list: indexList });
  }
}

module.exports = IndexController;

然後在 config/config.default.js 配置中新增對應的請求 url 和 頁碼大小配置如下:

// 下面是我自己的 Cookie 安全字串金鑰

exports.keys = '123456';

// 新增view配置
exports.view = {
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.tpl': 'nunjucks'
  }
};

// 新增index 的 配置項
exports.index = {
  pageSize: 10,
  serverUrl: 'https://hacker-news.firebaseio.com/v0'
};

然後我們在 瀏覽器訪問 http://127.0.0.1:7001/index 後,在頁面中返回如下頁面:

因為介面是node伺服器端渲染的,所以在瀏覽器中是看不到請求的。

注意: https://hacker-news.firebaseio.com/v0 這個請求想請求成功 需要chromeFQ下才能請求成功,當然我們也可以換成
自己的請求介面地址的。

app/service/index.js 中,我們繼承了egg中的Service實列,在使用者的每次請求中,框架都會實列化對應的Service實列。因此Service會提供有如下屬性值:

this.ctx: 當前請求的上下文 Context物件的實列,我們就可以拿到該框架封裝好的當前請求的各種屬性和方法。
this.app: 當前應用的Application物件的實列,通過它我們就可以拿到框架提供的全域性物件和方法。
this.servie: 應用定義的Service,通過它可以訪問到其他的業務層。等價於 this.ctx.service.
this.config: 可以拿到應用時的配置項對應的目錄。預設指向與 config.default.js.

Service 提供如下方法:
this.ctx.curl 發起網路呼叫請求。
this.ctx.service.otherService 呼叫其他的Service.
this.ctx.db 發起資料庫呼叫等。db可能是其他外掛提取掛載到app上的模組。

注意:
1. 一個Service檔案只能包含一個類,這個類需要通過 module.exports 的方式返回。
2. Service需要通過Class的方式定義,父類必須是 egg.Service.
3. Service不是單列,是請求級別的物件,框架在每次請求中首次訪問 ctx.service.xx 時延遲例項化,所以我們建議在Service中
可以通過 this.ctx獲取當前請求的上下文。

因此現在專案目錄結構就變成如下了:

egg-demo2
├── app
│   ├── controller
│   │   └── home.js
|   |   |-- index.js
│   └── router.js
│   ├──public
|   | |---css
|   | | |-- index.css
|   | |---js
|   | | |-- index.js
|   |--- view
|   | |-- index
|   | | |-- list.tpl(模板檔案list)
|   |--- service
|   | |--- index.js
├── config
│   └── config.default.js
└── package.json 

其他有關Egg相關的文章下篇待續,繼續來了解下egg相關的知識點。

檢視github上的原始碼

相關文章