egg學習筆記(摘抄官方文檔)


eggjs學習路線

快速建立egg項目

$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i

啟動項目

npm run dev

基礎功能

內置對象

包括從 Koa 繼承而來的 4 個對象(Application, Context, Request, Response) 以及框架擴展的一些對象(Controller, Service, Helper, Config, Logger)

Application

Application 是全局應用對象,在一個應用中,只會實例化一個,它繼承自 Koa.Application,在它上面我們可以掛載一些全局的方法和對象。我們可以輕松的在插件或者應用中擴展 Application 對象。

server

該事件一個 worker 進程只會觸發一次,在 HTTP 服務完成啟動后,會將 HTTP server 通過這個事件暴露出來給開發者。

error

運行時有任何的異常被 onerror 插件捕獲后,都會觸發 error 事件,將錯誤對象和關聯的上下文(如果有)暴露給開發者,可以進行自定義的日志記錄上報等處理。

request & response

應用收到請求和響應請求時,分別會觸發 request 和 response 事件,並將當前請求上下文暴露出來,開發者可以監聽這兩個事件來進行日志記錄。

獲取方式

Application 對象幾乎可以在編寫應用時的任何一個地方獲取到,下面介紹幾個經常用到的獲取方式:

幾乎所有被框架 Loader 加載的文件(Controller,Service,Schedule 等),都可以 export 一個函數,這個函數會被 Loader 調用,並使用 app 作為參數:
啟動自定義腳本

module.exports = app => {
  app.cache = new Cache();
};

Controller 文件

class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.app.cache.get(this.ctx.query.id);
  }
}

在context對象上,可以通過ctx.app訪問到Application對象.

class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
  }
}

在繼承於 Controller, Service 基類的實例中,可以通過 this.app 訪問到 Application 對象。

class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.app.cache.get(this.ctx.query.id);
  }
};

Context

Context 是一個請求級別的對象,繼承自 Koa.Context。在每一次收到用戶請求時,框架會實例化一個 Context 對象,這個對象封裝了這次用戶請求的信息,並提供了許多便捷的方法來獲取請求參數或者設置響應信息。框架會將所有的 Service 掛載到 Context 實例上,一些插件也會將一些其他的方法和對象掛載到它上面(egg-sequelize 會將所有的 model 掛載在 Context 上)。

獲取方式

最常見的 Context 實例獲取方式是在 Middleware, Controller 以及 Service 中。Controller 中的獲取方式在上面的例子中已經展示過了,在 Service 中獲取和 Controller 中獲取的方式一樣,在 Middleware 中獲取 Context 實例則和 Koa 框架在中間件中獲取 Context 對象的方式一致。

框架的 Middleware 同時支持 Koa v1 和 Koa v2 兩種不同的中間件寫法,根據不同的寫法,獲取 Context 實例的方式也稍有不同:

// Koa v1
function* middleware(next) {
  // this is instance of Context
  console.log(this.query);
  yield next;
}

// Koa v2
async function middleware(ctx, next) {
  // ctx is instance of Context
  console.log(ctx.query);
}

除了在請求時可以獲取 Context 實例之外, 在有些非用戶請求的場景下我們需要訪問 service / model 等 Context 實例上的對象,我們可以通過 Application.createAnonymousContext() 方法創建一個匿名 Context 實例:

module.exports = app => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
    // preload before app start
    await ctx.service.posts.load();
  });
}

在定時任務中的每一個 task 都接受一個 Context 實例作為參數,以便我們更方便的執行一些定時的業務邏輯:

在定時任務中的每一個 task 都接受一個 Context 實例作為參數,以便我們更方便的執行一些定時的業務邏輯:

Request & Response

Request 是一個請求級別的對象,繼承自 Koa.Request。封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求常用參數。

Response 是一個請求級別的對象,繼承自 Koa.Response。封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。

獲取方式

class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.request.query.id;
    ctx.response.body = app.cache.get(id);
  }
}

Controller

框架提供了一個 Controller 基類,並推薦所有的 Controller 都繼承於該基類實現。這個 Controller 基類有下列屬性:

ctx

當前請求的 Context 實例。

app

應用的 Application 實例

config

應用的配置

service

應用所有的service

logger

為當前controller封裝的logger對象

在 Controller 文件中,可以通過兩種方式來引用 Controller 基類:

// 從 egg 上獲取(推薦)
const Controller = require('egg').Controller;
class UserController extends Controller {
  // implement
}
module.exports = UserController;

// 從 app 實例上獲取
module.exports = app => {
  return class UserController extends app.Controller {
    // implement
  };
};

service

框架提供了一個 Service 基類,並推薦所有的 Service 都繼承於該基類實現。

Service 基類的屬性和 Controller 基類屬性一致,訪問方式也類似:

// 從 egg 上獲取(推薦)
const Service = require('egg').Service;
class UserService extends Service {
  // implement
}
module.exports = UserService;

// 從 app 實例上獲取
module.exports = app => {
  return class UserService extends app.Service {
    // implement
  };
};

Helper

Helper 用來提供一些實用的 utility 函數。它的作用在於我們可以將一些常用的動作抽離在 helper.js 里面成為一個獨立的函數,這樣可以用 JavaScript 來寫復雜的邏輯,避免邏輯分散各處,同時可以更好的編寫測試用例。

Helper 自身是一個類,有和 Controller 基類一樣的屬性,它也會在每次請求時進行實例化,因此 Helper 上的所有函數也能獲取到當前請求相關的上下文信息。

獲得方式

可以在 Context 的實例上獲取到當前請求的 Helper(ctx.helper) 實例。

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.query.id;
    const user = app.cache.get(id);
    ctx.body = ctx.helper.formatUser(user);
  }
}

除此之外,Helper 的實例還可以在模板中獲取到,例如可以在模板中獲取到 security 插件提供的 shtml 方法。

// app/view/home.nj
{{ helper.shtml(value) }}

自定義 helper 方法

應用開發中,我們可能經常要自定義一些 helper 方法,例如上面例子中的 formatUser,我們可以通過框架擴展的形式來自定義 helper 方法。

module.exports = {
  formatUser(user) {
    return only(user, [ 'name', 'phone' ]);
  }
};

config

我們推薦應用開發遵循配置和代碼分離的原則,將一些需要硬編碼的業務配置都放到配置文件中,同時配置文件支持各個不同的運行環境使用不同的配置,使用起來也非常方便,所有框架、插件和應用級別的配置都可以通過 Config 對象獲取到

獲取方式

我們可以通過 app.config 從 Application 實例上獲取到 config 對象,也可以在 Controller, Service, Helper 的實例上通過 this.config 獲取到 config 對象。

logger

框架內置了功能強大的日志功能,可以非常方便的打印各種級別的日志到對應的日志文件中,每一個 logger 對象都提供了 4 個級別的方法

logger.debug()

logger.info()

logger.warn()

logger.error()

運行環境

一個 Web 應用本身應該是無狀態的,並擁有根據運行環境設置自身的能力。

指定運行環境

框架有兩種指定運行環境

1、 通過 config/env 文件指定,該文件的內容就是運行環境,如 prod。一般通過構建工具來生成這個文件。

// config/env
prod

2、通過EGG_SERVER_ENV環境變量指定運行環境更加方便,比如在生產環境啟動應用:

EGG_SERVER_ENV=prod npm start

應用內獲得運行環境

框架提供了變量 app.config.env 來表示應用當前的運行環境。

與 NODE_ENV 的區別

很多 Node.js 應用會使用 NODE_ENV 來區分運行環境,但 EGG_SERVER_ENV 區分得更加精細。一般的項目開發流程包括本地開發環境、測試環境、生產環境等,除了本地開發環境和測試環境外,其他環境可統稱為服務器環境,服務器環境的 NODE_ENV 應該為 production。而且 npm 也會使用這個變量,在應用部署的時候一般不會安裝 devDependencies,所以這個值也應該為 production。

自定義環境

常規開發流程可能不僅僅只有以上幾種環境,Egg 支持自定義環境來適應自己的開發流程。

比如,要為開發流程增加集成測試環境 SIT。將 EGG_SERVER_ENV 設置成 sit(並建議設置 NODE_ENV = production),啟動時會加載 config/config.sit.js,運行環境變量 app.config.env 會被設置成 sit。

config配置

框架提供了強大且可擴展的配置功能,可以自動合並應用、插件、框架的配置,按順序覆蓋,且可以根據環境維護不同的配置。合並后的配置可直接從 app.config 獲取。

egg.js使用代碼管理配的方法,配置即代碼,配置的變更也應該經過 review 后才能發布。應用包本身是可以部署在多個環境的,只需要指定運行環境即可。

多環境配置

框架支持根據環境來加載配置,定義多個環境的配置文件

config
|- config.default.js
|- config.prod.js
|- config.unittest.js
`- config.local.js

config.default.js 為默認的配置文件,所有環境都會加載這個配置文件,一般也會作為開發環境的默認配置文件。

當指定 env 時會同時加載對應的配置文件,並覆蓋默認配置文件的同名配置。如 prod 環境會加載 config.prod.js 和 config.default.js 文件,config.prod.js 會覆蓋 config.default.js 的同名配置。

配置寫法

配置文件返回的是一個 object 對象,可以覆蓋框架的一些配置,應用也可以將自己業務的配置放到這里方便管理。

// 配置 logger 文件的目錄,logger 默認配置由框架提供
module.exports = {
  logger: {
    dir: '/home/admin/logs/demoapp',
  },
};

配置文件也可以簡化的寫成 exports.key = value 形式

exports.keys = 'my-cookie-secret-key';
exports.logger = {
  level: 'DEBUG',
};

配置文件也可以返回一個 function,可以接受 appInfo 參數

// 將 logger 目錄放到代碼目錄下
const path = require('path');
module.exports = appInfo => {
  return {
    logger: {
      dir: path.join(appInfo.baseDir, 'logs'),
    },
  };
};

內置的appInfo有

pkg:package.json
name:應用名,同pkg.name
baseDir:應用代碼目錄
HOME:用戶目錄,如admin賬戶為/home/admin
root:應用根目錄,只有在local和unittset環境下為baseDir,其他都為HOME

appInfo.root 是一個優雅的適配,比如在服務器環境我們會使用 /home/admin/logs 作為日志目錄,而本地開發時又不想污染用戶目錄,這樣的適配就很好解決這個問題

配置加載順序

應用、插件、框架都可以定義這些配置,而且目錄結構都是一致的,但存在優先級(應用 > 框架 > 插件),相對於此運行環境的優先級會更高。

比如在 prod 環境加載一個配置的加載順序如下,后加載的會覆蓋前面的同名配置。

-> 插件 config.default.js
-> 框架 config.default.js
-> 應用 config.default.js
-> 插件 config.prod.js
-> 框架 config.prod.js
-> 應用 config.prod.js

合並規則

配置的合並使用 extend2 模塊進行深度拷貝,extend2 fork 自 extend,處理數組時會存在差異。


const a = {
  arr: [ 1, 2 ],
};
const b = {
  arr: [ 3 ],
};
extend(true, a, b);
// => { arr: [ 3 ] }

根據上面的例子,框架直接覆蓋數組而不是進行合並。

中間件(Middleware)

編寫中間件

寫法

我們先來通過編寫一個簡單的 gzip 中間件,來看看中間件的寫法。

// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');

async function gzip(ctx, next) {
  await next();

  // 后續中間件執行完成后將響應體轉換成 gzip
  let body = ctx.body;
  if (!body) return;
  if (isJSON(body)) body = JSON.stringify(body);

  // 設置 gzip body,修正響應頭
  const stream = zlib.createGzip();
  stream.end(body);
  ctx.body = stream;
  ctx.set('Content-Encoding', 'gzip');
}

可以看到,框架的中間件和 Koa 的中間件寫法是一模一樣的,所以任何 Koa 的中間件都可以直接被框架使用。

配置

一般來說中間件也會有自己的配置。在框架中,一個完整的中間件是包含了配置處理的。我們約定一個中間件是一個放置在 app/middleware 目錄下的單獨文件,它需要 exports 一個普通的 function,接受兩個參數:

options: 中間件的配置項,框架會將 app.config[${middlewareName}] 傳遞進來。
app: 當前應用 Application 的實例。

我們將上面的 gzip 中間件做一個簡單的優化,讓它支持指定只有當 body 大於配置的 threshold 時才進行 gzip 壓縮,我們要在 app/middleware 目錄下新建一個文件 gzip.j

const isJSON = require('koa-is-json');
const zlib = require('zlib');

module.exports = options => {
  return async function gzip(ctx, next) {
    await next();

    // 后續中間件執行完成后將響應體轉換成 gzip
    let body = ctx.body;
    if (!body) return;

    // 支持 options.threshold
    if (options.threshold && ctx.length < options.threshold) return;

    if (isJSON(body)) body = JSON.stringify(body);

    // 設置 gzip body,修正響應頭
    const stream = zlib.createGzip();
    stream.end(body);
    ctx.body = stream;
    ctx.set('Content-Encoding', 'gzip');
  };
};

使用中間件

中間件編寫完成后,我們還需要手動掛載,支持以下方式:

在應用中使用中間件

在應用中,我們可以完全通過配置來加載自定義的中間件,並決定它們的順序。

如果我們需要加載上面的 gzip 中間件,在 config.default.js 中加入下面的配置就完成了中間件的開啟和配置:

module.exports = {
  // 配置需要的中間件,數組順序即為中間件的加載順序
  middleware: [ 'gzip' ],

  // 配置 gzip 中間件的配置
  gzip: {
    threshold: 1024, // 小於 1k 的響應體不壓縮
  },
};

該配置最終將在啟動時合並到 app.config.appMiddleware。

在框架和插件中使用中間件

框架和插件不支持在 config.default.js 中匹配 middleware,需要通過以下方式:

// app.js
module.exports = app => {
  // 在中間件最前面統計請求時間
  app.config.coreMiddleware.unshift('report');
};

// app/middleware/report.js
module.exports = () => {
  return async function (ctx, next) {
    const startTime = Date.now();
    await next();
    // 上報請求時間
    reportTime(Date.now() - startTime);
  }
};

應用層定義的中間件(app.config.appMiddleware)和框架默認中間件(app.config.coreMiddleware)都會被加載器加載,並掛載到 app.middleware 上。

router中使用中間件

以上兩種方式配置的中間件是全局的,會處理每一次請求。 如果你只想針對單個路由生效,可以直接在 app/router.js 中實例化和掛載,如下:

module.exports = app => {
  const gzip = app.middleware.gzip({ threshold: 1024 });
  app.router.get('/needgzip', gzip, app.controller.handler);
};

框架默認中間件

除了應用層加載中間件之外,框架自身和其他的插件也會加載許多中間件。所有的這些自帶中間件的配置項都通過在配置中修改中間件同名配置項進行修改,例如框架自帶的中間件中有一個 bodyParser 中間件(框架的加載器會將文件名中的各種分隔符都修改成駝峰形式的變量名),我們想要修改 bodyParser 的配置,只需要在 config/config.default.js 中編寫

module.exports = {
  bodyParser: {
    jsonLimit: '10mb',
  },
};

通用配置

無論是應用層加載的中間件還是框架自帶中間件,都支持幾個通用的配置項:
1、enable:控制中間件是否開啟
2、match:設置只有符合某些規則的請求才會經過這個中間件
3、ignore:設置符合某些規則的請求不經過這個中間件

enable

如果我們的應用並不需要默認的 bodyParser 中間件來進行請求體的解析,此時我們可以通過配置 enable 為 false 來關閉它

module.exports = {
  bodyParser: {
    enable: false,
  },
};

match和ignore

match 和 ignore 支持的參數都一樣,只是作用完全相反,match 和 ignore 不允許同時配置。

如果我們想讓 gzip 只針對 /static 前綴開頭的 url 請求開啟,我們可以配置 match 選項

module.exports = {
  gzip: {
    match: '/static',
  },
};

match 和 ignore支持多種類型的配置方式
1、字符串:當參數為字符串類型時,配置的是一個url的路徑前綴,所有以配置的字符串作為前綴的 url 都會匹配上。 當然,你也可以直接使用字符串數組。
2、當參數為正則時,直接匹配滿足正則驗證的 url 的路徑。
3、函數:當參數為一個函數時,會將請求上下文傳遞給這個函數,最終取函數返回的結果(true/false)來判斷是否匹配。

module.exports = {
  gzip: {
    match(ctx) {
      // 只有 ios 設備才開啟
      const reg = /iphone|ipad|ipod/i;
      return reg.test(ctx.get('user-agent'));
    },
  },
};

路由(Router)

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

通過統一的配置,我們可以避免路由規則邏輯散落在多個地方,從而出現未知的沖突,集中在一起我們可以更方便的來查看全局的路由規則。

如何定義Router

app/rounter.js里面定義URL路由規則

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};

app/controller目錄下面實現Controller

// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}

這樣就完成了一個最簡單的 Router 定義,當用戶執行 GET /user/123,user.js 這個里面的 info 方法就會執行。

Router 詳細定義說明

下面是路由的完整定義,參數可以根據場景的不同,自由選擇:

router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);

verb - 用戶觸發動作,支持 get,post 等所有 HTTP 方法

router-name 給路由設定一個別名,可以通過 Helper 提供的輔助函數 pathFor 和 urlFor 來生成 URL。(可選)

path-match - 路由 URL 路徑。

middleware1 - 在 Router 里面可以配置多個 Middleware。(可選)

controller - 指定路由映射到的具體的 controller 上,controller 可以有兩種寫法:
app.controller.user.fetch - 直接指定一個具體的 controller
'user.fetch' - 可以簡寫為字符串形式

注意事項

1、 Router 定義中, 可以支持多個 Middleware 串聯執行

2、Controller 必須定義在 app/controller 目錄中。

3、一個文件里面也可以包含多個 Controller 定義,在定義路由的時候,可以通過 \({fileName}.\){functionName} 的方式指定對應的 Controller。

4、Controller 支持子目錄,在定義路由的時候,可以通過 \({directoryName}.\){fileName}.${functionName} 的方式制定對應的 Controller。

RESTful 風格的 URL 定義

如果想通過 RESTful 的方式來定義路由, 我們提供了 app.router.resources('routerName', 'pathMatch', controller) 快速在一個路徑上生成 CRUD 路由結構。

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.resources('posts', '/api/posts', controller.posts);
  router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};

上面代碼就在 /posts 路徑上部署了一組 CRUD 路徑結構,對應的 Controller 為 app/controller/posts.js 接下來, 你只需要在 posts.js 里面實現對應的函數就可以了。

router實戰

參數獲取

Query String 方式
// app/router.js
module.exports = app => {
  app.router.get('/search', app.controller.search.index);
};

// app/controller/search.js
exports.index = async ctx => {
  ctx.body = `search: ${ctx.query.name}`;
};

// curl http://127.0.0.1:7001/search?name=egg
參數命名方式
// app/router.js
module.exports = app => {
  app.router.get('/user/:id/:name', app.controller.user.info);
};

// app/controller/user.js
exports.info = async ctx => {
  ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};

// curl http://127.0.0.1:7001/user/123/xiaoming
復雜參數的獲取

路由里面也支持定義正則,可以更加靈活的獲取參數:

// app/router.js
module.exports = app => {
  app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};

// app/controller/package.js
exports.detail = async ctx => {
  // 如果請求 URL 被正則匹配, 可以按照捕獲分組的順序,從 ctx.params 中獲取。
  // 按照下面的用戶請求,`ctx.params[0]` 的 內容就是 `egg/1.0.0`
  ctx.body = `package:${ctx.params[0]}`;
};

// curl http://127.0.0.1:7001/package/egg/1.0.0

表單內容的獲取

// app/router.js
module.exports = app => {
  app.router.post('/form', app.controller.form.post);
};

// app/controller/form.js
exports.post = async ctx => {
  ctx.body = `body: ${JSON.stringify(ctx.request.body)}`;
};

// 模擬發起 post 請求。
// curl -X POST http://127.0.0.1:7001/form --data '{"name":"controller"}' --header 'Content-Type:application/json'

這里直接發起 POST 請求會報錯:'secret is missing'。錯誤信息來自 koa-csrf/index.js#L69 。

原因:框架內部針對表單 POST 請求均會驗證 CSRF 的值,因此我們在表單提交時,請帶上 CSRF key 進行提交,可參考安全威脅csrf的防范

注意:上面的校驗是因為框架中內置了安全插件 egg-security,提供了一些默認的安全實踐,並且框架的安全插件是默認開啟的,如果需要關閉其中一些安全防范,直接設置該項的 enable 屬性為 false 即可。

「除非清楚的確認后果,否則不建議擅自關閉安全插件提供的功能。」

這里在寫例子的話可臨時在 config/config.default.js 中設置

exports.security = {
  csrf: false
};

表單校驗

// app/router.js
module.exports = app => {
  app.router.post('/user', app.controller.user);
};

// app/controller/user.js
const createRule = {
  username: {
    type: 'email',
  },
  password: {
    type: 'password',
    compare: 're-password',
  },
};

exports.create = async ctx => {
  // 如果校驗報錯,會拋出異常
  ctx.validate(createRule);
  ctx.body = ctx.request.body;
};

// curl -X POST http://127.0.0.1:7001/user --data 'username=abc@abc.com&password=111111&re-password=111111'

重定向

內部重定向

// app/router.js
module.exports = app => {
  app.router.get('index', '/home/index', app.controller.home.index);
  app.router.redirect('/', '/home/index', 302);
};

// app/controller/home.js
exports.index = async ctx => {
  ctx.body = 'hello controller';
};

// curl -L http://localhost:7001

外部重定向

// app/router.js
module.exports = app => {
  app.router.get('/search', app.controller.search.index);
};

// app/controller/search.js
exports.index = async ctx => {
  const type = ctx.query.type;
  const q = ctx.query.q || 'nodejs';

  if (type === 'bing') {
    ctx.redirect(`http://cn.bing.com/search?q=${q}`);
  } else {
    ctx.redirect(`https://www.google.co.kr/search?q=${q}`);
  }
};

// curl http://localhost:7001/search?type=bing&q=node.js
// curl http://localhost:7001/search?q=node.js

中間件的使用

如果我們想把用戶某一類請求的參數都大寫,可以通過中間件來實現。

// app/controller/search.js
exports.index = async ctx => {
  ctx.body = `search: ${ctx.query.name}`;
};

// app/middleware/uppercase.js
module.exports = () => {
  return async function uppercase(ctx, next) {
    ctx.query.name = ctx.query.name && ctx.query.name.toUpperCase();
    await next();
  };
};

// app/router.js
module.exports = app => {
  app.router.get('s', '/search', app.middleware.uppercase(), app.controller.search)
};

// curl http://localhost:7001/search?name=egg

太多路由映射

如上所述,我們並不建議把路由規則邏輯散落在多個地方,會給排查問題帶來困擾。

若確實有需求,可以如下拆分:

// app/router.js
module.exports = app => {
  require('./router/news')(app);
  require('./router/admin')(app);
};

// app/router/news.js
module.exports = app => {
  app.router.get('/news/list', app.controller.news.list);
  app.router.get('/news/detail', app.controller.news.detail);
};

// app/router/admin.js
module.exports = app => {
  app.router.get('/admin/user', app.controller.admin.user);
  app.router.get('/admin/log', app.controller.admin.log);
};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM