前言
我们在上一篇文章 Egg.JS 源码分析 - 项目启动, 已经简单的分析了 Eggjs 的启动机制, 以及其相应的实现原理, Eggjs 就是针对一系列的约定俗成的规则, 在项目启动时, 自动加载对应文件夹下面的文件, 进行项目的初始化, 我们可以参考官网给出的目录结构 https://eggjs.org/zh-cn/basics/structure.html , 去对我们的项目进行规范, 包括文件结构规范, 代码逻辑分层规范, 从而达到整个项目的规范.
之所以有这样的一个目录结构 , 其实还是针对于我们上一篇文章 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 会越来越大, 也会越来越难以维护, 所以我们可以做如下的调整:
在 App 下面创建一个文件夹 routers, 然后创建四个文件, 分别为: user.JS, message.JS, search.JS, home.JS,
然后将 router.JS 中的路由进行分割, 不同的路由都分割到对应的文件下.
在 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, 所以, 我们可以选择用 https://github.com/eggjs/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 数据库里面查询的所有的数据.
总结
安装插件:
$ NPM i egg-mongoose --save
配置插件:
App/config/plugin.JS
中配置插件
配置连接: 我们修改
App/config/config.default.JS
, 添加 egg-mongoose 连接信息
创建 Model: 我们在
{app_root}/App/model/user.JS
下创建 Model.
修改 Service: 修改 Servcie 的代码, 操作 MongoDB,
await this.ctx.model.User.find();
问题
在上一篇文章 Egg.JS 源码分析 - 项目启动中, 我们并没有分析到, eggjs 会加载 model 文件夹下面的文件. 那这里的 model 目录下的文件是什么时候加载上的?
在 config.default.JS 中, 配置 mongoose 的连接信息, 挂载在 config.mongoose 上, mongoose 这个名称是否是固定的, 可以修改成其他的?
在
App/config/plugin.JS
中, 配置 mongoose 的插件信息, mongoose 的属性名称是否是固定的, 可以修改成其他的?
答案
在 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;
- },
- });
- }
必须叫做 mongoose 这个名称, 因为在 egg-mongoose 这个插件中, 会直接去读取应用中
App.config.mongoose
的配置
const { client, clients, url, options, defaultDB, customPromise, loadModel } = App.config.mongoose;
, 所以这个规则是 egg-mongoose 插件制定的.
在 plugin.JS 的配置名称, 不是固定的, 可以随意, 因为其真正重要的配置是:
package: 'egg-mongoose'
, 指明这个 pulgin 用的是那个具体的包.
初始化数据
在一个项目上线的时候, 我们经常需要准备一些初始化数据, 比如用户数据, 我们一般会创建一个超级管理员的帐号, 这个帐号, 是不需要用户注册的, 所以我们可以在项目初始化的时候用脚本生成, 我们按照如下步骤进行操作:
修改 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);
- };
在{app_root}/App 目录下, 创建一个 data 的文件夹, 然后创建一个 user.JSON, 其内容如下:
- [
- {
- "userName": "admin",
- "password": "admin",
- "isMaster": true
- }
- ]
因为需要在项目启动的时候, 去初始化数据, 所以我们在 {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 的基本使用. 源码
来源: https://juejin.im/post/5bf362f0e51d4543850ff46c