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 = '