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