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


在上一篇文章 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 這個請求想請求成功 需要chrome翻牆下才能請求成功,當然我們也可以換成
自己的請求接口地址的。

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上的源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM