diff --git a/.gitignore b/.gitignore index 74a6311..ecdd3d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ checklist node_modules/ yarn.lock +.DS_Store diff --git a/docs/guide/README.md b/docs/guide/README.md index b5edd8f..cde5051 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -1,7 +1,53 @@ --- title: Guide -navTitle: Egg Guide +navTitle: Guide toc: false --- -guide \ No newline at end of file +In this guide, we'd thoroughly introduce you every bit of Egg. You can also find guides about the applicable situations of Egg, components how-to, common functions and properties, extendability and testing. + +## MVC + +Egg is a web application framework written in Node.js. It is designed based on the MVC principle, which is commonly seen among other web application frameworks written in Node.js, like Express.js. + +Before you step into the details, we strongly recommend you to learn [the structure of an Egg application]. + +There're some basic concepts that you need to know: + +- [Middleware]: Same as `Koa`'s middleware and similar to `Filter` in Java's world. +- [Controller]: Response to the incoming requests and performs interactions on the data models or calling the services. +- [Router]: Dispatches incoming requests to designated controllers. +- [Service]: A bunch of magic functions that contain your business logics. +- [Application]: A handy object includes the [configurations] of the application. It can be accessed globally. +- [Context]: Every user request has a context. It describes the request and defines the response. +- Besides, there're [Cookie], [Session], [Helper], etc. + +## Features + +Egg provides a range of utilities you can use in your day to day development. + +- [Plugins]: The foundation of Egg's eco-system. You can extend your applications using plugins just in one minute. +- [Lifecycle]: Allow you to run your own code at different stages. +- [Logs]: Logging anything to anywhere. It is crucial for monitoring and debugging. +- [Error handling]: Make the application robust. +- [Security]: Be safe. +- Last but not least, [file uploading], [i18n]. + +[the structure of an Egg application]: ./directory.md +[Controller]: ./controller.md +[Service]: ./service.md +[Router]: ./router.md +[Cookie]: ./cookie.md +[Session]: ./session.md +[Application]: ./application.md +[Context]: ./context.md +[Middleware]: ./middleware.md +[Plugins]: ./plugin.md +[Lifecycle]: ./lifecycle.md +[configurations]: ./config.md +[Logs]: ./logger.md +[Error handling]: ./error_handler.md +[Helper]: ./helper.md +[Security]: ../ecosystem/security/ +[file uploading]: ./upload.md +[i18n]: ./i18n.md \ No newline at end of file diff --git a/docs/guide/application.md b/docs/guide/application.md new file mode 100644 index 0000000..2eb3ca8 --- /dev/null +++ b/docs/guide/application.md @@ -0,0 +1,178 @@ +--- +title: Application +--- + +## Use Cases + +`Application` is an global object which inherits from [Koa.Application]. It can be used for extending functionalities which are meant to be shared across the application. + +The Egg application would only instantiate the `Application` once in a process. + +:::warning +For processes started by Egg, they all have an instance of `Application`. +::: + +## How To Access + +`Application` probably is the most common seen object in developing Egg applications. + +```js +// app/controller/home.js +class HomeController extends Controller { + async index() { + // 从 `Controller/Service` 基类继承的属性: `this.app` + console.log(this.app.config.name); + // 从 ctx 对象上获取 + console.log(this.ctx.app.config.name); + } +} +``` + +[Routers][Router] and [Middlewares][Middleware] and other files that loaded through the `Loader` can export a function and take the `app` as an argument. + +*Router* + +```js +// app/router.js +module.exports = app => { + const { router, controller } = app; + router.get('/', controller.home.index); +}; +``` + +*Middleware* + +```js +// app/middleware/response_time.js +module.exports = (options, app) => { + // 加载期传递 app 实例 + console.log(app); + + return async function responseTime(ctx, next) {}; +}; +``` + +## Common API + +### app.config + +[Configurations][Configuration] for the application. + +### app.router + +The [`Router`][Router] instance. + +### app.controller + +All the [`Controller`][Controller] instances. + +### app.logger + +The global logger for logging business unrelated information. + +Go to [Logger] to see more. + +### app.middleware + +All [Middlewares][Middleware]. + +### app.server + +An instance of [HTTP Server](https://nodejs.org/api/http.html#http_class_http_server) or [HTTPS Server](https://nodejs.org/api/https.html#https_class_https_server). + +It can be accessed after the [lifecycle](./lifecycle.md) event `serverDidReady`. + +### app.curl() + +An instance of [HttpClient](./httpclient.md). + +### app.createAnonymousContext() + +You can create an anonymous request by calling this function if you need to access the [Context] instance. + +```js +const ctx = app.createAnonymousContext(); +await ctx.service.user.list(); +``` + +## Extending + +Egg supports extending `Application` using `app/extend/application.js`. + +### Extend a Function + +```js +// app/extend/application.js +module.exports = { + foo(param) { + // `this` points to the `app` + }, +}; +``` + +### Extend a Property + +It is recommended to use *Symbol* and *Getter* to save properties that need to be instantiate. + +```js +// app/extend/application.js +const NUNJUCKS = Symbol('Application#nunjucks'); +const nunjuck = require('nunjuck'); + +module.exports = { + get nunjucks() { + if (!this[NUNJUCKS]) { + // `this` points to the `app` + this[NUNJUCKS] = new nunjucks.Environment(this.config.nunjucks); + } + return this[NUNJUCKS]; + }, + + foo: 'bar', +}; +``` + +### Writing Tests + +We recommend you to keep your plugins high quality using [unit tests](../workflow/development/unittest.md). + +```js +// test/app/extend/application.js +const { app, assert } = require('egg-mock'); + +describe('test/app/extend/application.js', () => { + it('should export nunjucks', () => { + assert(app.nunjucks); + assert(app.nunjucks.renderString('{{ name }}', { name: 'TZ' }) === 'TZ'); + }); +}); +``` + +Find more on this topic in [Development - Unit Test](../workflow/development/unittest.md). + +### Extend In Different Environment + +You can choose when to activate certain plugins or extend files. + +For example, `app/extend/application.unittest.js` will only be activated on `unittest` environment. + +```js +// app/extend/application.unittest.js +module.exports = { + mockXX(k, v) { + }, +}; +``` + +Meanwhile, other objects like `Application`, `Context`, `Request`, `Response`, `Helper` can also be extended using this technique. + +[Koa]: http://koajs.com +[Koa.Application]: http://koajs.com/#application +[Middleware]: ./middleware.md +[Context]: ./context.md +[Controller]: ./controller.md +[Service]: ./service.md +[Router]: ./router.md +[路由]: ./router.md +[Configuration]: ./config.md +[Logger]: ./logger.md \ No newline at end of file diff --git a/docs/guide/config.md b/docs/guide/config.md new file mode 100644 index 0000000..bdc8efd --- /dev/null +++ b/docs/guide/config.md @@ -0,0 +1,193 @@ +--- +title: Configuration +--- + +## Managing Configurations + +There are various ways to manage configurations in production: + +- Keeping environment-specific configurations on different environments/servers. Config files get defined on preparation process, so the config files cannot be edited once the preparation is done. +- Environment-specific configurations get passed through environment variables. This approach is more elegant but need supports of the running environment. Local development is not that friendly. +- Keeping every configuration as a part of the project. Let the framework choose which configuration to use according to the environment. No global configurations. Config changes mean code changes. + +Egg chooses the *last* one, which is **configurations are code**. All changes to the configuration should be **reviewed** before shipping. A ship-able application can run on different environment since all configurations have been defined in the project. + +## Environment + +A ship-able Egg application is able to run on any environment as long as certain `NODE_ENV` is presented. + +### env + +`app.config.env` shows which environment the application is currently running on. + +These are all the valid values: + +`env` | `NODE_ENV` | Description +--- | --- | --- +`local` | - | Local development +`unittest` | `test` | The application is being tested +`prod` | `production` | Production + +`env` defines which plugins and configurations are in use. + +## Configuration Files + +Egg uses certain configuration files regarding the environment. + +``` +showcase +├── app +└── config + ├── config.default.js + ├── config.prod.js + ├── config.unittest.js + ├── config.default.js + └── config.local.js +``` + +- `config.default.js` - Default configurations. This file is loaded in all conditions. **Most of your configurations should be placed here**. +- `config.${env}.js` - File defined by environment is loaded afterwards. + +*For example* + +Application running in production mode: + +- Egg loads `config.default.js` first, then `config.prod.js`. +- Content in `config.default.js` with the same name as in `config.prod.js` will be merged. + +You can find more detail in [Deployment](../workflow/deployment/README.md). + +## Define Configurations + +There are three forms to define configurations. + +*I* + +```js +// config/config.default.js +module.exports = { + logger: { + dir: '/home/admin/logs/demoapp', + }, +}; +``` + +*II* + +```js +// config/config.default.js +exports.keys = 'my-cookie-secret-key'; +exports.logger = { + level: 'DEBUG', +}; +``` + +*III* + +```js +// config/config.default.js +const path = require('path'); + +module.exports = appInfo => { + const config = {}; + + config.logger = { + dir: path.join(appInfo.root, 'logs', appInfo.name), + }; + + return config; +}; +``` + +:::tip Tips +You might find `exports.pluginName = {}` in some documents. Please check after copying and pasting. Don't mix up with the third scenario. +::: + +## `AppInfo` + +`appInfo` | Definition +--- | --- +`pkg` | package.json +`name` | Application name, same as `pkg.name` +`baseDir` | Absolute path of the application +`HOME` | User directory +`root` | Root directory, `local` and `unittest` is same as `baseDir`, otherwise is same as `HOME` + +:::warning Warning +**We intend to differentiate `appInfo.root` in different environments.** + +For example, we use `/home/admin/logs` as the logging directory but don't want to mess up with the user directory while developing, so different values are used. +::: + +## Loading Sequence + +Framework, Plugin and Application are capable of defining the configurations, but the priority of them is different -- `Application > Framework > Plugin`. + +The merging procedure will overwrite existing keys with newly loaded keys. + +*For example* + +When the application is running on `prod` mode. + +```txt +-> Plugin config.default.js +-> Framework config.default.js +-> Application config.default.js +-> Plugin config.prod.js +-> Framework config.prod.js +-> Application config.prod.js +``` + +:::tip Tips +Merging two arrays is kind of different from others -- always overwriting instead of concacting. + +```js +const a = { + arr: [ 1, 2 ], +}; +const b = { + arr: [ 3 ], +}; +extend(true, a, b); +// => { arr: [ 3 ] } +``` +::: + +## Most Asked Questions + +### Why aren't my configurations effective? + +Look into your config files, making sure they don't like this. + + +```js +// config/config.default.js +exports.someKeys = 'abc'; + +module.exports = appInfo => { + const config = {}; + config.keys = '123456'; + return config; +}; +``` + +### Is it possible to see the whole config after merging? + +YES. + +There're two types of file showing the final version of configurations. + +- `run/application_config.json` - Final version +- `run/application_config_meta.json` - Where do they come from + +Additionally, some properties' value would be redacted for security reasons. For example: + +- Passwords, private keys. This can be configured by [config.dump.ignore](https://github.com/eggjs/egg/blob/master/config/config.default.js). +- `Function`s, `Buffer`s. + +:::tip Tips +Files in `run` directory are generated after the initiation automatically, so: + +1. Modifications are not effective. +2. This directory should be ignored by your CVS. +::: diff --git a/docs/guide/context.md b/docs/guide/context.md new file mode 100644 index 0000000..acfc20c --- /dev/null +++ b/docs/guide/context.md @@ -0,0 +1,517 @@ +--- +title: Context +--- + +## What Is `Context` + +`Context` is created after receiving an incoming request and destroyed after sending the response. `Context` is inherited from [Koa.Context]. + +`Context` consists of every detail of the request, as well as many handy functions that enquire information from the request or modify the response. + +Egg would attach all the [Service] to the `Context` instance. Other plugins would also attach objects and functions to the `Context` instance. + +## Usage + +You are able to get access to the `Context` instance from within [Middleware], [Controller] and [Service]. + +*[Controller]* and *[Service]* + +```js +// app/controller/home.js +class HomeController extends Controller { + async index() { + const { ctx } = this; + ctx.body = ctx.query('name'); + } +} +``` + +*[Middleware]* (Same as Koa) + +```js +// app/middleware/response_time.js +module.exports = () => { + return async function responseTime(ctx, next) { + const start = Date.now(); + await next(); + const cost = Date.now() - start; + ctx.set('X-Response-Time', `${cost}ms`); + } +}; +``` + +If you need a `Context` instance without actual requests, you can use `createAnonymousContext()` in [Application](./application.md). + +```js +const ctx = app.createAnonymousContext(); +await ctx.service.user.list(); +``` + +[Timed Task](../ecosystem/schedule/timer.md) also has `Context` instance as its argument. + +```js +// app/schedule/refresh.js +exports.task = async ctx => { + await ctx.service.posts.refresh(); +}; +``` + +## Common API + +### `ctx.app` + +[Application](./application.md) instance. + +### `ctx.service` + +[Service] instance. + +### `ctx.logger` + +`ContextLogger` instance. This logger is dedicated to write logs which are specific to requests. Egg has predefined the format for developers: + +``` +[$userId/$ip/$traceId/${cost}ms $method $url] +``` + +This's useful for debugging or locating logs related to certain requests. + +You can find more in [Logger]. + +### `ctx.curl()` + +[HttpClient](./httpclient.md) instance. + +### `ctx.runInBackground()` + +You can run asynchronous code in the callback without interfering the responding process. + +```js +// app/controller/trade.js +class TradeController extends Controller { + async buy () { + const goods = {}; + const result = await ctx.service.trade.buy(goods); + + // Non-blocking trade checking + ctx.runInBackground(async () => { + // Errors will be captured and logged + await ctx.service.trade.check(result); + }); + + ctx.body = { msg: 'ordered' }; + } +} +``` + +### `ctx.query` + +Get parsed query-string, returning an empty object when no query-string is present. Note that this getter does not support nested parsing. + +```js +// GET /api/user/list?limit=10&sort=name +class UserController extends Controller { + async list() { + console.log(this.ctx.query); + // { limit: '10', sort: 'name' } + ctx.body = 'hi, egg'; + } +} +``` + +:::tip Tips +All values parsed from URL are string. +::: + +:::warning Warning +Only the first key is effective when duplicate keys are presented. For example: + +``` +// GET /api/user?sort=name&id=2&id=3 + +console.log(this.ctx.query.id); // => '2' +``` +::: + +### `ctx.queries` + +Different from `ctx.query`, this getter returns parsed query-string which support duplicate keys. + +```js +// GET /api/user?sort=name&id=2&id=3 +class UserController extends Controller { + async list() { + console.log(this.ctx.queries); + // { sort: [ 'name' ], id: [ '2', '3' ] } + } +} +``` + +- Values are always array, even if the key just shows once. Like `queries.name === [ 'sort' ]`. +- You should use `ctx.query` if you are confident that the key just shows once. + +### `ctx.params` + +Get params defined in [Router](./router.md#获取命名参数). + +### `ctx.routerPath` + +Get current [route](./router.md#路由路径) path. + +### `ctx.routerName` + +Get the name of current [route](./router.md#路由别名). + +### `ctx.request.body` + +Egg has a built-in [bodyparser](https://github.com/koajs/bodyparser). + +```js +class UserController extends Controller { + async create() { + // Form, application/json... + console.log(this.ctx.request.body); + // ... + } +} +``` + +Corrosponding test cases: + +```js +// test/controller/home.test.js +it('should POST form', () => { + + // bypass `CSRF` validation + app.mockCsrf(); + + return app.httpRequest() + .post('/user/create') + .type('form') + .send({ name: 'TZ' }) + .expect(200); +}); + +it('should POST JSON', () => { + app.mockCsrf(); + return app.httpRequest() + .post('/user/create') + .type('json') + .send({ name: 'TZ' }) + .expect(200); +}); +``` + +### `ctx.request.files` + +Get files uploaded from browsers. You can find more in [Uploading](./upload.md). + +### `ctx.get(name)` + +Get the value of a specific *Header*. + +All keys in `ctx.headers` are lowercase, no matter how they are presented in requests. To minimize confusion, we recommend using `ctx.get(name)` which is case insensitive. + +```js +ctx.get('User-Agent'); // Recommended + +ctx.headers['user-agent']; // OK + +ctx.headers['User-Agent']; // undefined +``` + +### `ctx.cookies` + +Interact with cookies. You can find more on [Cookie](./cookie.md). + +### `ctx.status =` + +This setter is used to set the response [status code]((https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)). + +```js +class UserController extends Controller { + async create() { + // 设置状态码为 201 + this.ctx.status = 201; + } +}; +``` + +Corresponding test cases: + +```js +it('should POST /user', () => { + return app.httpRequest() + .post('/user') + .expect(201); +}); +``` + +### `ctx.body =` + +This setter is used to set the response body. It takes following value: + +- Object +- String +- Stream + +```js +// app/controller/home.js +class HomeController extends Controller { + // GET / + async index() { + this.ctx.type = 'html'; + this.ctx.body = '

Hello

'; + } + + // GET /api/info + async info() { + this.ctx.body = { + name: 'egg', + category: 'framework', + language: 'Node.js', + }; + } + + // GET /api/proxy + async proxy() { + const { ctx } = this; + const result = await ctx.curl(url, { + streaming: true, + }); + ctx.set(result.header); + // result.res 是一个 stream + ctx.body = result.res; + } +} +``` + +Corosponding test cases: + +```js +it('should response html', () => { + return app.httpRequest() + .get('/') + .expect('

Hello

') + .expect(/Hello/); +}); + +it('should response json', () => { + return app.httpRequest() + .get('/api/info') + .expect({ + name: 'egg', + category: 'framework', + language: 'Node.js', + }) + .expect(res => { + assert(res.body.name === 'egg'); + }); +}); +``` + +### `ctx.set(name, value)` + +Response header can also be modified through this function. + +- `ctx.set(key, value)`: Modify one item at a time +- `ctx.set(headers)`: Batch modify + +```js +// app/controller/proxy.js +class ProxyController extends Controller { + async show() { + const { ctx } = this; + const start = Date.now(); + ctx.body = await ctx.service.post.get(); + const cost = Date.now() - start; + + ctx.set('x-response-time', `${cost}ms`); + } +}; +``` + +Corosponding test cases: + +```js +it('should send response header', () => { + return app.httpRequest() + .post('/api/post') + .expect('X-Response-Time', /\d+ms/); +}); +``` + +### `ctx.type =` + +This setter is used to modify `Content-Type` of the response. It's equal to `ctx.set('Content-Type', mime)`. It takes following values: + +- `json`: Equal to `application/json`. +- `html`: Equal to `text/html`. +- More valid values see [mime-types](https://github.com/jshttp/mime-types). + +:::tip Tips +Normally, `Content-Type` is set automatically by Egg. +::: + +```js +// app/controller/user.js +class UserController extends Controller { + async list() { + this.ctx.body = { name: 'egg' }; + } +}; +``` + +Corosponding test cases: + +```js +it('should response json', () => { + return app.httpRequest() + .get('/api/user') + .expect('Content-Type', /json/); +}); +``` + +### `ctx.render()` + +Call render function provided by template engines. + +```js +class HomeController extends Controller { + async index() { + const ctx = this.ctx; + await ctx.render('home.tpl', { name: 'egg' }); + // ctx.body = await ctx.renderString('hi, {{ name }}', { name: 'egg' }); + } +}; +``` + +You can find more on [Template Engine](../ecosystem/frontend/template.md). + +### `ctx.redirect()` + +Send a redirect response to browsers. The status code is `302` by default, but can be modified through `ctx.status = 301`. + +```js +class UserController extends Controller { + async logout() { + const { ctx } = this; + + ctx.logout(); + ctx.redirect(ctx.get('referer') || '/'); + } +} +``` + +Corosponding test cases: + +```js +it('should logout', () => { + return app.httpRequest() + .get('/user/logout') + .expect('Location', '/') + .expect(302); +}); +``` + +:::warning Warning +**Only URLs under selected domains can redirect to by default for security reasons.** + +You can find more on [Security URL](../ecosystem/security/security_url.md). +::: + +### `ctx.request` + +Same as [Koa.Request]. It provides many helpers and properties to the request, many of those are delegated to `Context`. This object is not exactly is [HTTP Request](https://nodejs.org/api/http.html#http_class_http_clientrequest). + +Be aware that `ctx.body` is a setter for `ctx.response.body`. If you need the request body, use `ctx.request.body`. + +```js +// app/controller/user.js +class UserController extends Controller { + async update() { + const { app, ctx } = this; + // Equal to ctx.query + const id = ctx.request.query.id; + + // Request body + const postBody = ctx.request.body; + + // Equal to ctx.body + ctx.response.body = await app.service.update(id, postBody); + } +} +``` + +### `ctx.response` + +Same as [Koa.Response]. It provides many helpers and properties to the response. This object is not exactly is [HTTP Response](https://nodejs.org/api/http.html#http_class_http_serverresponse). + +### More + +You can find more helpers and properties on [Koa Aliases]. + +## Extend + +Egg supports extending `Context`, `Request` and `Response` with custom functions and properties. + +- `app/extend/context.js`: Extend `Context`。 +- `app/extend/request.js`: Extend `Request`。 +- `app/extend/response.js`: Extend `Response`。 +- `app/extend/context.unittest.js`: Extend `Context` while testing. + +### Extending Properties + +You can use the following method to minimize the performance overhead of getters. + +```js +// app/extend/context.js +const UA = Symbol('Context#ua'); +const useragent = require('useragent'); + +module.exports = { + get ua() { + if (!this[UA]) { + const uaString = this.get('user-agent'); + this[UA] = useragent.parse(uaString); + } + return this[UA]; + }, +}; +``` + +Corosponding test cases: + +```js +// test/app/extend/context.js +const { app, assert } = require('egg-mock'); + +describe('test/app/extend/contex.js', () => { + it('should parse ua', () => { + // Mocking context + const ctx = app.mockContext({ + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) Chrome/15.0.874.24', + }, + }); + + assert(ctx.ua.chrome); + }); +}); +``` + +You can find more on [Development - Unit Test](../workflow/development/unittest.md). + +[Koa]: http://koajs.com +[Koa.Application]: http://koajs.com/#application +[Koa.Context]: http://koajs.com/#context +[Koa.Request]: http://koajs.com/#request +[Koa.Response]: http://koajs.com/#response +[Koa Aliases]: https://koajs.com/#request-aliases +[Middleware]: ./middleware.md +[Controller]: ./controller.md +[Service]: ./service.md +[Router]: ./router.md +[Router]: ./router.md +[Config]: ./config.md +[Logger]: ./logger.md diff --git a/docs/quickstart/README.md b/docs/quickstart/README.md deleted file mode 100644 index a4512ed..0000000 --- a/docs/quickstart/README.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: QuickStart ---- - -QuickStart \ No newline at end of file diff --git a/docs/quickstart/egg.md b/docs/quickstart/egg.md new file mode 100644 index 0000000..f2a7fd3 --- /dev/null +++ b/docs/quickstart/egg.md @@ -0,0 +1,306 @@ +--- +title: Your first Egg application +--- + +It's time to get your hands dirty. In this guide, we'll walk you through every step of building a (really) simple Egg application. + +:::tip Tips +- This guide is for beginners who want to get started with an Egg application from scratch. +- Most of the plugins are included in Egg by default. You can switch them on/off in the configurations. +- More information can be found in [Guide](../guide/README.md). +::: + +## TodoMVC + +We'll build a [TodoMVC](http://todomvc.com/) application from scratch, using Egg. + +The complete version can be found at [eggjs/examples/todomvc](https://github.com/eggjs/examples/tree/master/todomvc). + + + +## Creating an Egg aplication + +### Preparation + +- **OS**: All operating systems are supported, but `macOS` is recommended. +- **Node.js**: If you are new to Node.js, there's an [installation guide here](./prepare.md). + +### Scaffolding + +There's a [tool](../workflow/development/init.md) for scaffolding Egg applications. + +:::tip Tips +The examples below use $ to represent the terminal prompt in a UNIX-like OS, it might be different in some occasions. +::: + +```bash +$ mkdir demo && cd demo +$ npm init egg --type=simple +$ npm install +``` + +### Directory + +The `demo` directory will be looking like this. They are generated by following [the directory rules](../guide/directory.md) of an Egg application. We strongly recommend you to follow it when creating new folders. + +```bash +demo +├── app +│   ├── controller # Controllers +│   │   └── home.js +│   └── router.js # Routers +├── config # Configurations +│   ├── config.default.js +│   └── plugin.js +├── test # Testing files +├── README.md +└── package.json +``` + +### `Controller` + +[Controller](../guide/controller.md) is responsible for **parsing user requests**, **handling them** and **response back**. + +```js +// app/controller/home.js +const { Controller } = require('egg'); + +class HomeController extends Controller { + async index() { + const { ctx } = this; + ctx.body = 'hi, egg'; + } +} + +module.exports = HomeController; +``` + +Every controller needs to mount to a certain `URL` which tells Egg when to handle incoming requests. In the case below, `/` would be handled with the controller you just wrote. + +```js +// app/router.js +/** + * @param {Egg.Application} app - egg application + */ +module.exports = app => { + const { router, controller } = app; + router.get('/', controller.home.index); +}; +``` + +### Local Development + +Egg comes with a [tool](../workflow/development/development.md) for local development. + +- Getting your application up; +- Watching file changes; +- Generating `d.ts` files if you're using TypeScript; + +To start your application: + +```bash +$ npm run dev +``` + +Next, go to `http://127.0.0.1:7001` in your browser. + +### Template Rendering + +In most cases, we want to present web pages to users, using template rendering. But Egg doesn't come with this feature enabled by default. You have to choose a *template engine plugin* and enable it by yourself. + +:::tip What is plugin? +The plugin system is a special feature of Egg. It makes the core application simple, yet stable and highly efficient. You can use plugins for business logic reusing and building an eco-system. + +Go to [Development - Plugins](../guide/plugin.md) to see more. +::: + +In this guide, we'll be using [Nunjucks](https://mozilla.github.io/nunjucks/) to render our pages. + +To start, you need to install [egg-view-nunjucks] first. + +```bash +$ npm i egg-view-nunjucks --save +``` + +Then, enable it. + +```js +// config/plugin.js +exports.nunjucks = { + enable: true, + package: 'egg-view-nunjucks' +}; +``` + +Every template file should be located in `app/view` and its enclosing folders. + +```html + + + ... + + +``` + +Lastly, change your `Controller` into this. + +```js +class HomeController extends Controller { + async index() { + const { ctx } = this; + // 渲染模板 `app/view/home.tpl` + await ctx.render('home.tpl'); + } +} +``` + +### Static Files + +Normally, static files are served through: + +- Content Deliver Network (recommend); +- Current application; + +Egg comes with a handy plugin [egg-static] for serving static files in current application. + +egg-static would mount the folder `app/public` to the route `/public` by default. + +:::warning Warning +- Files served by `egg-static` will be responsed with a `maxAge` header which indicates the browsers to cache for a year by default. +- [CSRF mechanism](../ecosystem/security/csrf.md) is open by default. `AJAX` requests need to have a valid `token` so that they can be handled properly. If you are using axios, here's an example: + +```js +// app/public/main.js +axios.defaults.headers.common['x-csrf-token'] = Cookies.get('csrfToken'); +``` +::: + +### Configurations + +It's quite common for developers to manage a bunch of [configurations](../guide/config.md). Egg's configuration feature would definately be helpful. + +For example, to use [egg-view-nunjucks], you will have to add a few lines to `config/config.default.js`. + +```js +// config/config.default.js +config.view = { + defaultViewEngine: 'nunjucks', + mapping: { + '.tpl': 'nunjucks', + '.html': 'nunjucks', + }, +}; +``` + +:::warning Warning +It's `./config`, not `./app/config`! +::: + +### `Service` + +Normally, business logics are in the [Service](../guide/service.md). They can be called by the `Controller`. + +Here is an example for creating a todo item. + +```js +// app/service/todo.js +const { Service } = require('egg'); + +class TodoService extends Service { + /** + * create todo + * @param {Todo} todo - todo info without `id`, but `title` required + */ + async create(todo) { + // validate + if (!todo.title) this.ctx.throw(422, 'task title required'); + + // normalize + todo.id = Date.now().toString(); + todo.completed = false; + + this.store.push(todo); + return todo; + } +} +``` + +Then, call it in `Controller`. + +```js +// app/controller/todo.js +class TodoController extends Controller { + async create() { + const { ctx, service } = this; + + // params validate, need `egg-validate` plugin + // ctx.validate({ title: { type: 'string' } }); + + ctx.status = 201; + ctx.body = await service.todo.create(ctx.request.body); + } +} +``` + +### RESTful + +Egg has [built-in support](../guide/router.md#RESTful-风格的-URL-定义) for RESTful routing. + +```js +// app/router.js +module.exports = app => { + const { router, controller } = app; + + // RESTful mapping + router.resources('/api/todo', controller.todo); +}; +``` + +The controller `controller.todo` must implement these functions: + +```js +// app/controller/todo.js +class TodoController extends Controller { + // `GET /api/todo` + async index() {} + + // `POST /api/todo` + async create() {} + + // `PUT /api/todo` + async update() {} + + // `DELETE /api/todo` + async destroy() {} +} +``` + +### Unit Testing + +It is highly recommended to do the unit testing. Egg comes with [a tool](../workflow/development/unittest.md) for you to test your application. + +```js +// test/app/controller/todo.test.js +const { app, mock, assert } = require('egg-mock/bootstrap'); + +describe('test/app/controller/todo.test.js', () => { + it('should add todo', () => { + return app.httpRequest() + .post('/api/todo') + .send({ title: 'Add one' }) + .expect('Content-Type', /json/) + .expect('X-Response-Time', /\d+ms/) + .expect(201) + .expect(res => { + assert(res.body.id); + assert(res.body.title === 'Add one'); + assert(res.body.completed === false); + }); + }); +}); +``` + +[Node.js]: http://nodejs.org +[egg-static]: https://github.com/eggjs/egg-static +[egg-view-nunjucks]: https://github.com/eggjs/egg-view-nunjucks +[Nunjucks]: https://mozilla.github.io/nunjucks/ \ No newline at end of file diff --git a/docs/zh/guide/application.md b/docs/zh/guide/application.md index d6ad8c4..8e81906 100644 --- a/docs/zh/guide/application.md +++ b/docs/zh/guide/application.md @@ -18,6 +18,8 @@ Node.js 进程间是无法共享对象的,因此每个进程都会有一个 `A 在 [Controller]、[Service] 等可以通过 `this.app`,或者所有 [Context] 对象上的 `ctx.app`: +TODO: 需要更准确的描述 + ```js // app/controller/home.js class HomeController extends Controller { @@ -32,6 +34,8 @@ class HomeController extends Controller { 几乎所有被框架 `Loader` 加载的文件,都可以 export 一个函数,并接收 `app` 作为参数: +TODO: 什么是 Loader + [Router]: ```js diff --git a/docs/zh/quickstart/egg.md b/docs/zh/quickstart/egg.md index 6ba6c1c..30e0c5a 100644 --- a/docs/zh/quickstart/egg.md +++ b/docs/zh/quickstart/egg.md @@ -163,7 +163,7 @@ class HomeController extends Controller { 在本例中,我们使用 `Vue` 来写对应的前端逻辑,可以直接参见示例代码。 :::warning 注意事项 -- `static` 插件,线上会默认设置一年的 `magAge`。 +- `static` 插件,线上会默认设置一年的 `maxAge`。 - 框架默认开启了 [CSRF 防护](../ecosystem/security/csrf.md),故 `AJAX` 请求需要带上对应的 `token`: ```js