egg-企業級框架和應用入門

AsGiant發表於2019-03-26

1 環境搭建、建立、執行

1.1 介紹

egg.js是阿里旗下基於node.js和koa是一個node企業級應用開發框架,可以幫助開發團隊,和開發人員減少成本。
基於koa2、es6、es7使得node具有更有規範的開發模式,更低的學習成本、更優雅的程式碼、更少的維護成本。

image.png

image.png


1.2 環境搭建

1、要求nodejs版本必須大於8.0並且要用LTS 版本
2、建立egg的環境   npm i egg-init -g  / cnpm i egg-init -g        (只需要安裝一次)
3、建立專案
cd 到目錄裡面   (注意目錄不要用中文  不要有空格)

1.3 建立

$ npm i egg-init -g
$ egg-init egg-example --type=simple   //例如:egg-init 專案名稱 --type=simple
$ cd egg-example
$ npm i
複製程式碼

1.4 執行專案

npm run dev  
open localhost:7001 //一般性來說預設埠是7001
複製程式碼

2 目錄結構介紹

2.1 目錄結構

egg-project
├── package.json
├── app.js (可選)
├── agent.js (可選)
├── app(專案開發目錄)
|   ├── router.js (用於配置 URL 路由規則)
│   ├── 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
複製程式碼

image.png

3 訪問路由

egg在設計完全符合比較好的mvc的設計模式。

3.1 那麼什麼是mvc呢?

全名是Model View Controller,是模型(model)-檢視(view)-控制器(controller)的縮寫,一種軟體設計典範。

在egg中檢視 (view)、控制器(controller) 和資料模型 Model(Service) 和配置檔案(config)

3.2 控制器(controller)

  • app/controller 目錄下面實現 Controller
// app/controller/home.js

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

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, world';
  }
}

module.exports = HomeController;

複製程式碼

輸入 npm run dev 檢視 http://127.0.0.1:7001 輸出  hi, world

我認為控制器就是一個介面,他管理輸入和輸出

*同樣你可以在app/controller 目錄下 寫很多個這樣個js的,來代表介面

3.3 路由(Router)

主要用來描述請求 URL 和具體承擔執行動作的 Controller 的對應關係, 框架約定了 app/router.js 檔案用於統一所有路由規則。

現在很多單頁面,都是存在相對於的路由,你寫個js,同樣就要寫一個路由

// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}
複製程式碼
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};
複製程式碼

3.4 資料模型 Model(Service)

簡單來說,Service 就是在複雜業務場景下用於做業務邏輯封裝的一個抽象層,提供這個抽象有以下幾個好處:

  • 保持 Controller 中的邏輯更加簡潔。
  • 保持業務邏輯的獨立性,抽象出來的 Service 可以被多個 Controller 重複呼叫。
  • 將邏輯和展現分離,更容易編寫測試用例。
// app/service/user.js
const Service = require('egg').Service;

class UserService extends Service {
  async addName(name) {
    const user = `你好,${name}`;
    return user;
  }
}

module.exports = UserService;
複製程式碼
// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userInfo = await ctx.service.user.addName('wjw');
    ctx.body = userInfo;
  }
}
複製程式碼

3.5 egg中檢視 (view)

egg中的模板渲染,但是我認為前端後端分離的設計,更加有利於作為服務型架構設計,所以這邊不描述view的構造

4 get、post請求

4.1 get 請求

4.1.1 query

在 URL 中 ?後面的部分是一個 Query String,這一部分經常用於 GET 型別的請求中傳遞引數。例如 GET /search?name=egg&age=26name=egg&age=26 就是使用者傳遞過來的引數。我們可以通過 context.query(為一個物件)拿到解析過後的這個引數體

module.exports = app => {

class HomeController extends Controller {
  async getQuery() {
      const queryObj = this.ctx.query;
      console.log(queryObj.age);
      console.log(queryObj);
      //列印結果:{ name: 'egg', age: '26' }
    }
  }
  return SearchController;
};
複製程式碼


當 Query String 中的 key 重複時,context.query只取 key 第一次出現時的值,後面再出現的都會被忽略。GET /posts?category=egg&category=koa 通過 context.query拿到的值是 { category: 'egg' }

4.1.2 queries

有時候我們的系統會設計成讓使用者傳遞相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3。針對此類情況,框架提供了 context.queries 物件,這個物件也解析了 Query String,但是它不會丟棄任何一個重複的資料,而是將他們都放到一個陣列中:

// GET /posts?category=egg&id=1&id=2&id=3
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async getQueries() {
    console.log(this.ctx.queries);
    //result:
    // {
    //   category: [ 'egg' ],
    //   id: [ '1', '2', '3' ],
    // }
  }
};
複製程式碼

context.queries上所有的 key 如果有值,也一定會是陣列型別。

4.2 post 請求

// 獲取引數方法 post 請求


module.exports = app => {
class HomeController extends Controller {
  async postObj() {
      const queryObj = ctx.request.body;
      ctx.body = queryObj;
    }
  }
  return SearchController;
};
複製程式碼

但是我們請求有時是get,有時是post,有時本來應該是post的請求,但是為了測試方便,還是做成get和post請求都支援的請求,於是一個能同時獲取get和post請求引數的中介軟體就很有必要了.

4.3 編寫中間層解決get、post請求

4.3.1 在app目錄下新建middleware資料夾

4.3.2 在middleware裡面新建params.js,內容如下

/**
 * 獲取請求引數中介軟體
 * 可以使用ctx.params獲取get或post請求引數
 */
module.exports = options => {
  return async function params(ctx, next) {
    ctx.params = {
      ...ctx.query,
      ...ctx.request.body
    }
    await next();
  };
};
複製程式碼

本質上就是把get請求的引數和post請求的引數都放到params這個物件裡,所以,不管是get還是post都能獲取到請求引數

4.3.3 在/config/config.default.js裡注入中介軟體

'use strict';
module.exports = appInfo => {
  const config = exports = {};
// 注入中介軟體
  config.middleware = [
    'params',
  ];
  return config;
};
複製程式碼

4.3.4 使用文章獲取

/**
 * 新增文章介面
 */
'use strict';
const Service = require('egg').Service;
class ArticleService extends Service {
  async add() {
    const { ctx } = this;
    // 獲取請求引數
    const {
      userId,
      title,
      content,
    } = ctx.params;
    const result = await ctx.model.Article.create({
      userId,
      title,
      content,
    });
    return result;
  }
}
module.exports = ArticleService;
複製程式碼

4.3.5 允許post請求跨域

// config/plugin.js
exports.cors = {
  enable: true,
  package: 'egg-cors',
};
複製程式碼
 // config/config.default.js
config.security = {
  csrf: {
    enable: false,
    ignoreJSON: true,
  },
  domainWhiteList: [ 'http://www.baidu.com' ], // 配置白名單
};

config.cors = {
  // origin: '*',//允許所有跨域訪問,註釋掉則允許上面 白名單 訪問
  allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
複製程式碼

*一般性最好使用白名單,不要使用全部允許跨域,不安全

5 mysql資料庫

框架提供了 egg-mysql 外掛來訪問 MySQL 資料庫。這個外掛既可以訪問普通的 MySQL 資料庫,也可以訪問基於 MySQL 協議的線上資料庫服務。

5.1 安裝與配置

安裝對應的外掛 egg-mysql :

npm i --save egg-mysql
複製程式碼

開啟外掛:

// config/plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};
複製程式碼

在 config/config.${env}.js 配置各個環境的資料庫連線資訊。

5.1.1 單資料來源

如果我們的應用只需要訪問一個 MySQL 資料庫例項,可以如下配置:
使用方式:

// config/config.${env}.js
exports.mysql = {
  // 單資料庫資訊配置
  client: {
    // host
    host: 'mysql.com',
    // 埠號
    port: '3306',
    // 使用者名稱
    user: 'test_user',
    // 密碼
    password: 'test_password',
    // 資料庫名
    database: 'test',
  },
  // 是否載入到 app 上,預設開啟
  app: true,
  // 是否載入到 agent 上,預設關閉
  agent: false,
};
複製程式碼
await app.mysql.query(sql, values); // 單例項可以直接通過 app.mysql 訪問
複製程式碼

5.1.2 多資料來源

如果我們的應用需要訪問多個 MySQL 資料來源,可以按照如下配置:

exports.mysql = {
  clients: {
    // clientId, 獲取client例項,需要通過 app.mysql.get('clientId') 獲取
    db1: {
      // host
      host: 'mysql.com',
      // 埠號
      port: '3306',
      // 使用者名稱
      user: 'test_user',
      // 密碼
      password: 'test_password',
      // 資料庫名
      database: 'test',
    },
    db2: {
      // host
      host: 'mysql2.com',
      // 埠號
      port: '3307',
      // 使用者名稱
      user: 'test_user',
      // 密碼
      password: 'test_password',
      // 資料庫名
      database: 'test',
    },
    // ...
  },
  // 所有資料庫配置的預設值
  default: {
  },
  // 是否載入到 app 上,預設開啟
  app: true,
  // 是否載入到 agent 上,預設關閉
  agent: false,
};
複製程式碼

5.2 封裝增刪改查

5.2.1、插入,向users表內插入一條資料

const result = await this.app.mysql.insert('users', {
    name: 'wjw',
    age: 18
  })
// 判斷:result.affectedRows === 1
複製程式碼

5.2.2、查詢,查詢users表name=Jack的資料

const result = await this.app.mysql.select('users', {
    columns: ['id', 'name'], //查詢欄位,全部查詢則不寫,相當於查詢*
    where: {
        name: 'wjw'
    }, //查詢條件
    orders: [
        ['id', 'desc'] //降序desc,升序asc
    ],
    limit: 10, //查詢條數
    offset: 0 //資料偏移量(分頁查詢使用)
  })
//判斷:result.length > 0
複製程式碼

5.2.3、修改,修改users表id=1的資料age為20

const result = await this.app.mysql.update('users', {
      age: 20 //需要修改的資料
  }, {
      where: {
        id: 1
      } //修改查詢條件
  });
//判斷:result.affectedRows === 1
複製程式碼

5.2.4、刪除,刪除users表name=wjw的資料

const result = await this.app.mysql.delete('users', {
    name: 'wjw'
})
複製程式碼

6 Cookie 的使用

6.1 Cookie 簡介

  • cookie 是儲存於訪問者的計算機中的變數。可以讓我們用同一個瀏覽器訪問同一個域名的時候共享資料。
  • HTTP 是無狀態協議。簡單地說,當你瀏覽了一個頁面,然後轉到同一個網站的另一個頁面,伺服器無法認識到這是同一個瀏覽器在訪問同一個網站。每一次的訪問,都是沒有任何關係的。

6.2 Cookie 的設定和獲取

6.2.1 Cookie 設定語法

ctx.cookies.set(key, value, options)

this.ctx.cookies.set('name','zhangsan');
複製程式碼

6.2.2 Cookie 獲取語法

ctx.cookies.get(key, options)

this.ctx.cookies.get('name')
複製程式碼

6.2.3 清除 Cookie

this.ctx.cookies.set('name',null);
複製程式碼

或者設定 maxAge 過期時間為 0

6.3 Cookie 引數 options

eggjs.org/en/core/coo…

ctx.cookies.set(key, value, {
  maxAge:24 * 3600 * 1000,
  httpOnly: true, // 預設情況下是正確的
  encrypt: true, // cookie在網路傳輸期間加密
  ctx.cookies.get('frontend-cookie', {
  encrypt: true
});
複製程式碼

6.4 設定中文 Cookie

6.4.1 第一種解決方案

console.log(new Buffer('hello, world!').toString('base64'));
// 轉換成 base64字串:aGVsbG8sIHdvcmxkIQ==
console.log(new Buffer('aGVsbG8sIHdvcmxkIQ==', 'base64').toString()); // 還原 base64字串:hello, world!
複製程式碼

6.4.2 第二種解決方案

ctx.cookies.set(key, value, {
	maxAge:24 * 3600 * 1000,
	httpOnly: true, // 預設情況下是正確的
	encrypt: true, // cookie在網路傳輸期間進行加密
});
複製程式碼

7 Session的使用

7.1 Session 簡單介紹

session 是另一種記錄客戶狀態的機制,不同的是 Cookie 儲存在客戶端瀏覽器中,而session 儲存在伺服器上。

7.2 Session 的工作流程

當瀏覽器訪問伺服器併傳送第一次請求時,伺服器端會建立一個 session 物件,生成一個類似於 key,value 的鍵值對, 然後將 key(cookie)返回到瀏覽器(客戶)端,瀏覽器下次再訪問時,攜帶 key(cookie),找到對應的 session(value)。

7.3 Egg.js 中 session 的使用

egg.js 中 session 基於 egg-session 內建了對 session 的操作

7.3.1 設定

this.ctx.session.userinfo={
	name:'張三', 
  age:'20'
}
複製程式碼

7.3.2 獲取

var userinfo=this.ctx.session
複製程式碼

7.3.3 Session 的預設設定

exports.session = {
  key: 'EGG_SESS',
  maxAge: 24 * 3600 * 1000, // 1 day httpOnly: true,
  encrypt: true
};
複製程式碼

7.4 Session 在 config.default.js 中的配置

config.session={
  key:'SESSION_ID',
  maxAge:864000,
  renew: true //延長會話有效期
}
複製程式碼

7.5 cookie 和session 區別

  • cookie 資料存放在客戶的瀏覽器上,session 資料放在伺服器上。
  • cookie 相比 session 沒有 session 安全,別人可以分析存放在本地的 COOKIE 並進行 COOKIE欺騙。
  • session 會在一定時間內儲存在伺服器上。當訪問增多,會比較佔用你伺服器的效能考慮到減輕伺服器效能方面,應當使用 COOKIE。
  • 單個 cookie 儲存的資料不能超過 4K,很多瀏覽器都限制一個站點最多儲存 20 個 cookie。

8 定時任務&定點任務

egg提供了強大的定時任務系統。通過定時任務,可以系統修改服務的快取資料,以便處理需要定時更新的資料。

在app/schedule目錄下新建一個js檔案,每一個js檔案就是一個定時任務


### 8.1 定時任務
// app/schedule
module.exports = {
  schedule: {
    interval: '1m', // 1 分鐘間隔
    type: 'all', // 指定所有的 worker 都需要執行
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};

/* 註釋:
	1ms -> 1毫秒
	1s -> 1秒
	1m -> 1分鐘
*/
複製程式碼

8.2 定點任務

定點任務(以每週一的5點30分0秒更新排行榜為例)


1、使用cron引數設定時間,cron引數分為6個部分,*表示所有都滿足

*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ 星期 (0 - 7) (0或7都是星期日)
│    │    │    │    └───── 月份 (1 - 12)
│    │    │    └────────── 日期 (1 - 31)
│    │    └─────────────── 小時 (0 - 23)
│    └──────────────────── 分鐘 (0 - 59)
└───────────────────────── 秒 (0 - 59, optional)
複製程式碼
// app/schedule
module.exports = {
  schedule: {
    cron: '0 30 5 * * 1', //每週一的5點30分0秒更新
    type: 'all', // 指定所有的 worker 都需要執行
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};
複製程式碼

8.3 只執行一次定時任務

設定immediate引數為true時,該定時任務會在專案啟動時,立即執行一次定時任務

module.exports = {
  schedule: {
    interval: '1m', // 1 分鐘間隔
    type: 'all', // 指定所有的 worker 都需要執行
    immediate: true, //專案啟動就執行一次定時任務
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};
複製程式碼

8.4 關閉任務

配置disable引數為true時,該定時任務即關閉

8.5 指定定時任務執行環境env

env: ["dev", "debug"] //該定時任務在開發環境和debug模式下才執行
複製程式碼

9 部署

9.1 部署伺服器

首先當然是在你的伺服器上部署好node服務,然後安裝好。

伺服器需要預裝 Node.js,框架支援的 Node 版本為 >= 8.0.0。
框架內建了 egg-cluster 來啟動 Master 程式,Master 有足夠的穩定性,不再需要使用 pm2 等程式守護模組。
同時,框架也提供了 egg-scripts 來支援線上環境的執行和停止。

egg-scripts start --port=7001 --daemon --title=egg-server-showcase
複製程式碼
  • --port=7001 埠號,預設會讀取環境變數 process.env.PORT,如未傳遞將使用框架內建埠 7001
  • --daemon 是否允許在後臺模式,無需 nohup。若使用 Docker 建議直接前臺執行。
  • --env=prod 框架執行環境,預設會讀取環境變數 process.env.EGG_SERVER_ENV, 如未傳遞將使用框架內建環境 prod
  • --workers=2 框架 worker 執行緒數,預設會建立和 CPU 核數相當的 app worker 數,可以充分的利用 CPU 資源。
  • --title=egg-server-showcase 用於方便 ps 程式時 grep 用,預設為 egg-server-${appname}
  • --framework=yadan 如果應用使用了可以配置 package.json 的 egg.framework 或指定該引數。
  • --ignore-stderr 忽略啟動期的報錯。

9.1.1 啟動配置項

你也可以在 config.{env}.js 中配置指定啟動配置。

// config/config.default.js

exports.cluster = {
  listen: {
    port: 7001,
    hostname: '127.0.0.1',
    // path: '/var/run/egg.sock',
  }
}
複製程式碼

pathporthostname 均為 server.listen 的引數,egg-scripts 和 egg.startCluster 方法傳入的 port 優先順序高於此配置。
s

9.1.2 停止命令

該命令將殺死 master 程式,並通知 worker 和 agent 優雅退出。
支援以下引數:

  • --title=egg-server 用於殺死指定的 egg 應用,未傳遞則會終止所有的 Egg 應用。
"start": "egg-scripts start --daemon --title=${程式名稱}",
"stop": "egg-scripts stop --title=${程式名稱}"
複製程式碼
  • 你也可以直接通過
ps -eo "pid,command" | grep -- "--title=egg-server"
複製程式碼

來找到 master 程式,並 kill 掉,無需 kill -9

9.2 監控

9.2.1 Node.js 效能平臺(alinode)

是面向所有 Node.js 應用提供 效能監控、安全提醒、故障排查、效能優化 等服務的整體性解決方案,提供完善的工具鏈和服務,協助開發者快速發現和定位線上問題。

npm i nodeinstall -g
複製程式碼

提供了egg-alinode  來快速接入,無需安裝 agenthub 等額外的常駐服務。

npm i egg-alinode --save
複製程式碼
// /config/plugin.js

exports.alinode = {
    enable:true,
    package:'egg-alinode',
},
複製程式碼

申請一下服務

訪問控制檯

控制檯地址:node.console.aliyun.com

image.png

image.png

// config/config.default.js
exports.alinode = {
  enable: true,
  appid: '***',  // Node.js 效能平臺給您的專案生成的 appid
  secret: '***',  // Node.js 效能平臺給您的專案生成的 secret
  logdir: '***',  //可選,Node.js 效能平臺日誌輸出地址絕對路徑,與 NODE_LOG_DIR 保持一致。如:/tmp/,也可以不寫
  error_log: [
    // '您的應用在業務層面產生的異常日誌的路徑,陣列,可選,可配置多個',
    // '例如:/root/.logs/error.#YYYY#-#MM#-#DD#.log',
    // '不更改 Egg 預設日誌輸出路徑可不配置本專案',
  ],// 可選
  agentidMode:'IP',  // 可選,如果設定,則在例項ID中新增部分IP資訊,用於多個例項 hostname 相同的場景(以容器為主)
};
複製程式碼

然後你就能愉快針對你的egg,進行監控了

image.png

相關文章