Egg.js 基本使用

bluebrid發表於2018-11-21

前言

我們在上一篇文章Egg.js 原始碼分析-專案啟動, 已經簡單的分析了Eggjs 的啟動機制, 以及其相應的實現原理,Eggjs 就是針對一系列的約定俗成的規則,在專案啟動時,自動載入對應資料夾下面的檔案,進行專案的初始化,我們可以參考官網給出的目錄結構,去對我們的專案進行規範,包括檔案結構規範, 程式碼邏輯分層規範,從而達到整個專案的規範。

Egg.js 基本使用
之所以有這樣的一個目錄結構 ,其實還是針對於我們上一篇文章Egg.js 原始碼分析-專案啟動 分析所得,在專案啟動時,會載入如下的配置, 下面程式碼後面加了備註,只標註了我們應用對應的檔名稱(沒有標註eggjs 對應的檔名稱)

  loadConfig() {
    // your-project-name/config/plugin.js
    this.loadPlugin();
    // your-project-name/config/config.default.js 
    // your-project-name/config/`config.${this.serverEnv}`.js 
    super.loadConfig();
  }
複製程式碼
  load() {
    // app > plugin > core
    this.loadApplicationExtend();
    this.loadRequestExtend();
    this.loadResponseExtend();
    this.loadContextExtend();
    this.loadHelperExtend();
    // your_project_name/app.js
    this.loadCustomApp();
    // your_project_name/app/service/**.js
    this.loadService();
    this.loadMiddleware();
    // your_project_name/app/controller/**.js 
    this.loadController();
    // your_project_name/ app/router.js
    this.loadRouter(); // Dependent on controllers
  }
複製程式碼

從上面可以知道我們應用的一個大致的結構,我們下面就來一一從頭開始建立一個專案,深入瞭解Eggjs 的使用方式(規範)

初始化專案

我們利用egg-init 的手腳架egg-init 先初始化一個專案, 我們可以執行如下命令(一直沒怎麼接觸APP 開發, 所以最近想研究下react-native ,所以建立了一個react-native-learning-server專案):

$ npm i egg-init -g
$ egg-init react-native-learning-server --type=simple
$ cd react-native-learning-server
$ npm i
$ npm run dev
複製程式碼

瀏覽器會開啟預設埠:http://localhost:7001, 頁面會顯示hi, egg, 說明我們專案建立成功.

設定路由

我們現在假設我們想做一個類似掘金一樣的APP, 我們可以有四個大選單 Mine , Find , Message, Home

我們簡單的設計幾個API:

Mine:

API Method 描敘
/users GET 獲取所有的使用者
/user/:id GET 獲取指定使用者資訊
/user POST 新增使用者
/user/:id PUT 編輯使用者
/user/:id DELETE 刪除使用者

Message:

API Method 描敘
/messages/:userId GET 獲取使用者所有的資訊
/message POST 傳送資訊
/message/:id DELETE 刪除指定資訊

Find:

API Method 描敘
/search/:keyword GET 根據關鍵字,查詢資訊

Home:

API Method 描敘
/hot GET 查詢最熱資訊
/heart GET 查詢關注的熱點

我們先只設計如上幾個簡單的API(我們這篇文章,只是想通過一個偽業務來實現Egg的一些使用方式, 重點是Eggjs 的使用)

上面我們已經初始化了專案, 我們現在編輯app/router.js去設計路由,其程式碼如下:

module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  // User
  router.get('/users', controller.user.findAll);
  router.get('/user/:id', controller.user.findOne);
  router.post('/user', controller.user.add);
  router.put('/user/:id', controller.user.update);
  router.del('/user/:id', controller.user.delete);
  // Message
  router.get('/messages/:userId', controller.message.findByUserId);
  router.post('/message', controller.message.add);
  router.del('/messages/:id', controller.message.delete);
  // Find
  router.get('/search/:keyword', controller.search.find);
  // Home
  router.get('/hot', controller.home.findHot);
  router.get('/heart', controller.home.findHeart);
};

複製程式碼

我們先不管Controller的實現, 上面我們就已經實現了我們的路由了, 但是我們發現一個問題,就是當專案越來越大,那這個router.js 會越來越大,也會越來越難以維護,所以我們可以做如下的調整:

  1. 在app下面建立一個資料夾routers, 然後建立四個檔案,分別為: user.js, message.js, search.js, home.js,
  2. 然後將router.js 中的路由進行分割,不同的路由都分割到對應的檔案下.
  3. 在router.js 中去引用每個單獨的路由 拆分後的router.js 如下:
'use strict';
const userRouter = require('./routers/user');
const messageRouter = require('./routers/message');
const homeRouter = require('./routers/home');
const searchRouter = require('./routers/search');
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  userRouter(app);
  messageRouter(app);
  homeRouter(app);
  searchRouter(app);
};
複製程式碼

其routers/user.js 程式碼如下:

'use strict';

module.exports = app => {
  const { router, controller } = app;
  router.get('/users', controller.user.findAll);
  router.get('/user/:id', controller.user.findOne);
  router.post('/user', controller.user.add);
  router.put('/user/:id', controller.user.update);
  router.del('/user/:id', controller.user.delete); 
};

複製程式碼

經過如上的拆分, router.js 的程式碼變得整潔, 而且相應的路由變得更加容易維護.

設定控制層(Controller)

上面我們已經開發(配置)好了Router, 但是Router 的回撥函式,都指向的是app 的controller下面的物件, 如:controller.user.findAll 我們在上一章節已經分析了這個路徑怎麼來的: controller 是app(應用)的一個屬性物件, eggjs 會在啟動的時候呼叫this.loadController(); 方法,去載入整個應用app/controller檔案下的所有的js 檔案, 會將檔名作為屬性名稱,掛載在app.controller 物件上, 然後將對應js 檔案export(暴露)出來的所有的方法有掛在在檔名稱為屬性的物件上,然後就可以通過controller.user.findAll這樣的方式來引用Controller 下面的方法了。

有了這個思路,我們就可以很清晰的去維護我們控制層了,下面是我們一個home的範例:

'use strict';

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

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'hi , egg.';
  }
  async findHot() {
    this.ctx.body = this.ctx.request.url;
  }
  async findHeart() {
    this.ctx.body = this.ctx.request.url;
  }
}

module.exports = HomeController;

複製程式碼

設定Service

我們已經開發好了Router 和Controller , 但是在我們的controller 中,都是靜態的內容, 一個專案我們需要跟資料庫互動,我們一般將跟DB 互動的內容,都放在Service 層,下面我們就來開發我們的service.

我們首先在app目錄下面,建立一個service 目錄, 並且建立 user.js, message.js, search.js, home.js,檔案, 我們先不連線真實的資料庫, 建立的service 如下(home.js):

'use strict';

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

class HomeService extends Service {
  async index() {
    return 'hi, egg';
  }
  findHot() {
    const hotArticle = [
      {
        title: 'Title 0001',
        desc: 'This is hot article 0001',
      },
      {
        title: 'Title 0002',
        desc: 'This is hot article 0002',
      },
    ];
    return hotArticle;
  }
  findHeart() {
    const heartArticle = [
      {
        title: 'Title 0001',
        desc: 'This is heart article 0001',
      },
      {
        title: 'Title 0002',
        desc: 'This is heart article 0002',
      },
    ];
    return heartArticle;
  }
}

module.exports = HomeService;
複製程式碼

我們接下來修改Controller檔案:

'use strict';

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

class HomeController extends Controller {
  async index() {
    this.ctx.body = this.service.home.index();
  }
  async findHot() {
    this.ctx.body = this.service.home.findHot();
  }
  async findHeart() {
    this.ctx.body = this.service.home.findHeart();
  }
}

module.exports = HomeController;

複製程式碼

我們呼叫service方法如: this.service.home.index();, 跟controller 原理類似。

到此位置,我們專案的基本框架,已經搭建完成,我們現在可以思考先,我們怎麼連線資料庫。

連線DB

我們的資料庫我們選擇用MongoDB, 所以,我們可以選擇用egg-mongoose 外掛,我們可以按照文件進行操作: 首先安裝外掛:

$ npm i egg-mongoose --save

因為egg-mongoose 是作為Eggjs 的一個外掛,所以我們要配置這個外掛,我們現在app/config/plugin.js中配置外掛:

'use strict';
module.exports = {
  // enable plugins 
  mongoose: {
    enable: true,
    package: 'egg-mongoose',
  },
};
複製程式碼

接下來,我們要配置MongoDB 的連線, 我們修改app/config/config.default.js

'use strict';

module.exports = appInfo => {
  const config = exports = {};
  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1541735701381_1116';
  // add your config here
  config.middleware = [];
  config.cluster = {
    listen: {
      path: '',
      port: 7001,
      hostname: '',
    },
  };
  config.mongoose = {
    client: {
      url: 'mongodb://127.0.0.1/react-native-demo',
      options: {},
    },
  };
  return config;
};

複製程式碼

接下來我們需要給MongoDB 配置Model,我們在app 目錄下面,建立一個model 資料夾, 並且建立user.js, 程式碼如下:

'use strict';
// {app_root}/app/model/user.js
module.exports = app => {
  const mongoose = app.mongoose;
  const Schema = mongoose.Schema;

  const UserSchema = new Schema({
    userName: { type: String },
    password: { type: String },
  });

  return mongoose.model('User', UserSchema);
};
複製程式碼

然後我們接下來,修改Service 來真正的連線資料庫,修改service/user.js 程式碼如下:

'use strict';
const Service = require('egg').Service;

class UserService extends Service {
  async findAll() {
    return await this.ctx.model.User.find();
  }
}

module.exports = UserService;

複製程式碼

連線資料庫的真個流程完成了,我們可以開啟http://localhost:7001/users, 頁面就會顯示從MongoDB 資料庫裡面查詢的所有的資料。

總結

  1. 安裝外掛: $ npm i egg-mongoose --save
  2. 配置外掛: app/config/plugin.js中配置外掛
  3. 配置連線: 我們修改app/config/config.default.js,新增egg-mongoose連線資訊
  4. 建立Model: 我們在{app_root}/app/model/user.js 下建立Model.
  5. 修改Service: 修改Servcie 的程式碼,操作MongoDB, await this.ctx.model.User.find();

問題

  1. 在上一篇文章Egg.js 原始碼分析-專案啟動中,我們並沒有分析到,eggjs 會載入model 資料夾下面的檔案.那這裡的model 目錄下的檔案是什麼時候載入上的?
  2. config.default.js 中,配置mongoose 的連線資訊,掛載在config.mongoose上, mongoose這個名稱是否是固定的, 可以修改成其他的?
  3. app/config/plugin.js中,配置mongoose的外掛資訊, mongoose 的屬性名稱是否是固定的, 可以修改成其他的?

答案

  1. 在eggjs 中,並沒有實現載入model 的功能,但是egg-mongoose這個外掛實現了這個功能,其程式碼如下:
function loadModelToApp(app) {
  const dir = path.join(app.config.baseDir, 'app/model');
  app.loader.loadToApp(dir, 'model', {
    inject: app,
    caseStyle: 'upper',
    filter(model) {
      return typeof model === 'function' && model.prototype instanceof app.mongoose.Model;
    },
  });
}

複製程式碼
  1. 必須叫做mongoose這個名稱,因為在egg-mongoose這個外掛中,會直接去讀取應用中app.config.mongoose 的配置const { client, clients, url, options, defaultDB, customPromise, loadModel } = app.config.mongoose;,所以這個規則是egg-mongoose外掛制定的。
  2. plugin.js的配置名稱, 不是固定的, 可以隨意,因為其真正重要的配置是: package: 'egg-mongoose',指明這個pulgin 用的是那個具體的包。

初始化資料

在一個專案上線的時候,我們經常需要準備一些初始化資料, 比如使用者資料,我們一般會建立一個超級管理員的帳號, 這個帳號,是不需要使用者註冊的,所以我們可以在專案初始化的時候用指令碼生成,我們按照如下步驟進行操作:

  1. 修改app/model/user.js新增isMaster屬性,如下:
'use strict';
// {app_root}/app/model/user.js
module.exports = app => {
  const mongoose = app.mongoose;
  const Schema = mongoose.Schema;

  const UserSchema = new Schema({
    userName: { type: String, required: true },
    password: { type: String, required: true },
    isMaster: { type: Boolean, default: false, required: true },
  });

  return mongoose.model('User', UserSchema);
};

複製程式碼
  1. {app_root}/app目錄下,建立一個data的資料夾,然後建立一個user.json, 其內容如下:
[
    {
        "userName": "admin",
        "password": "admin",
        "isMaster": true
    }
]
複製程式碼
  1. 因為需要在專案啟動的時候,去初始化資料,所以我們在{app_root}目錄下,新增一個app.js(this.loadCustomApp()),程式碼如下:
'use strict';
// app.js
module.exports = app => {
  app.beforeStart(async () => {
    if (app.config.initData) {
      const initUsers = require('./app/data/user.json');
      const ctx = app.createAnonymousContext();
      ctx.model.User.create(initUsers, err => {
        if (err) {
          app.coreLogger.console.warn('[egg-app-beforeStart] init user data fail %s', err);
        }
      });
    }
  });
};
複製程式碼

我們在配置檔案中新增了一個initData 的開關用來表示是否需要初始化資料,因為初始化資料,一般就是第一次需要(這個配置,應該作為執行指令碼命令的引數傳遞,這樣更易於維護,而且不用每次都去該config.default.js 的程式碼)

總結

按照上面的操作,我們基本完成了一個專案的基本骨架,我們只需要在上面搭積木就可以了,而且瞭解了Eggjs 的基本使用。原始碼

相關文章