在上一篇文章 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相關的知識點。