diff --git a/.autod.conf.js b/.autod.conf.js deleted file mode 100644 index 9787ec2cba..0000000000 --- a/.autod.conf.js +++ /dev/null @@ -1,28 +0,0 @@ -'ues strict'; - -module.exports = { - write: true, - plugin: 'autod-egg', - prefix: '^', - devprefix: '^', - exclude: [ - 'test/fixtures', - 'examples', - 'benchmarks', - "docs", - ], - devdep: [ - 'autod', - 'autod-egg', - 'nunjucks', - 'koa', - 'koa-router', - 'toa', - 'toa-router', - ], - keep: [ - ], - semver: [ - 'koa@1', - ], -}; diff --git a/.eslintignore b/.eslintignore index eb984b683c..dacb55c46c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ test/fixtures examples/**/app/public logs run +docs/node_modules diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..5f59807ac1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +open_collective: eggjs # Replace with a single Open Collective username + +# github: [ fengmk2, popomore, atian25, dead_horse ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# custom: # Replace with a single custom sponsorship URL diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b021f100e8..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ - - -* **Node Version**: -* **Egg Version**: -* **Plugin Name**: -* **Plugin Version**: -* **Platform**: - - diff --git a/.github/ISSUE_TEMPLATE/bug-report-cn.yml b/.github/ISSUE_TEMPLATE/bug-report-cn.yml new file mode 100644 index 0000000000..84f3755a33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-cn.yml @@ -0,0 +1,65 @@ +name: 🐛 Egg Bug 反馈 +description: 如发现 Egg 框架中的 Bug,请及时在此汇报。 +labels: [bug] +body: + - type: textarea + attributes: + label: | + 在此输入你需要反馈的 Bug 具体信息(Bug in Detail): + placeholder: | + 1. 我做了什么。 + + 2. 我的预期值。 + + 3. 我实际得到的结果。 + + 4. 可以的话,请提供一些截图、视频作为附件以复现症状。 + validations: + required: true + - type: textarea + attributes: + label: 可复现问题的仓库地址(Reproduction Repo) + description: | + 1. 请使用 `npm init egg --type=simple bug` 创建最小可复现问题的代码。 + + 2. 在 GitHub 中上传该代码项目,并在此处粘贴地址。你也可以直接将你的仓库压缩为 zip 文件直接以附件形式提交。 + placeholder: | + https://github.com/YOUR_REPOSITORY_URL + validations: + required: true + - type: input + attributes: + label: Node 版本号: + description: | + 你的当前复现问题的 Node 版本号: + placeholder: | + 使用 “node -v” 命令,在控制台得到版本号(例如:v8.5.0)。 + validations: + required: true + - type: input + attributes: + label: Eggjs 版本号: + description: | + 你的当前复现问题 Eggjs 版本号: + placeholder: | + 请直接在“package.json”中查阅(例如:3.1.0)。 + validations: + required: true + - type: input + attributes: + label: "相关插件名称与版本号(PlugIn and Name):" + description: | + 插件名称以及版本号: + placeholder: | + 请直接在“package.json”中查阅(例如:egg-mysql,3.1.1)。 + validations: + required: true + - type: input + attributes: + label: "操作平台与版本号(Platform and Version):" + description: | + 你的操作平台与版本号: + placeholder: | + Windows 10 专业版(21H2) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000000..11bd06379e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,65 @@ +name: 🐛 Bug Report For Eggjs +description: Report an issue if something isn't working as expected 🤔. +labels: [bug] +body: + - type: textarea + attributes: + label: | + Your detail info about the Bug: + placeholder: | + 1. What I did. + + 2. What I expected to happen. + + 3. What I actually got. + + 4. If possible, images/videos as attachments are welcomed to show the bug. + validations: + required: true + - type: textarea + attributes: + label: Reproduction Repo + description: | + 1. Please use `npm init egg --type=simple bug` to create your smallest repo. + + 2. Submit it in the GitHub and paste your URL here. You can also attach your zip file directly. + placeholder: | + https://github.com/YOUR_REPOSITORY_URL or your zip file + validations: + required: true + - type: input + attributes: + label: Node Version + description: | + What's your Node's version? + placeholder: | + Use "node -v" in your console to get it (e.g: v8.5.0). + validations: + required: true + - type: input + attributes: + label: Eggjs Version + description: | + What's your Eggjs version? + placeholder: | + See it directly in your "package.json" file (e.g: 3.1.0) + validations: + required: true + - type: input + attributes: + label: Plugin Name and its version + description: | + What's your plugin's name and version? + placeholder: | + See them directly in your "package.json" file (e.g: egg-mysql, 3.1.1) + validations: + required: true + - type: input + attributes: + label: Platform and its version + description: | + What's your platform and its version? + placeholder: | + Windows 10 Professional(21H2) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request-cn.yml b/.github/ISSUE_TEMPLATE/feature-request-cn.yml new file mode 100644 index 0000000000..1dc6f7bb04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request-cn.yml @@ -0,0 +1,21 @@ +name: 💡 我有一个新点子 +description: 我对 Eggjs 框架有一个新的想法(或许我想来实现他)…… +labels: [feature request] +body: + - type: markdown + attributes: + value: | + 对于 Egg.js 框架,你有一个新的想法? + 不过在提交你的新点子之前,麻烦请检阅一下是否之前的帖子中有类似重复的内容。 + - type: textarea + attributes: + label: 请详细告知你的新点子(Nice Ideas): + placeholder: | + 1. 您期望能够实现什么功能。 + + 2. 您的理由(如:我一直被什么问题困扰……)。 + 如果方便的话,请提供截屏或者视频等详细信息。 + + 3. 我能够做一些什么(最好是能提供一些伪代码帮助实现)。 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000000..0e10efe551 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,23 @@ +name: 💡 Feature Request For Eggjs +description: I have a suggestion (and may want to implement it)! +labels: [feature request] +body: + - type: markdown + attributes: + value: | + You have an idea how to improve the Eggjs? + + Before submitting, please have a look at the existing issues if there's already + something related to your suggestion. + - type: textarea + attributes: + label: "Enter your suggestions in details:" + placeholder: | + 1. What I expected to happen? + + 2. Your reason (e.g: I'm always frustrated with...). + If possible, images or videos are welcome. + + 3. What I plan to do (Optional but better in pseudo codes). + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/rfc-cn.yml b/.github/ISSUE_TEMPLATE/rfc-cn.yml new file mode 100644 index 0000000000..6de5aec4bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc-cn.yml @@ -0,0 +1,27 @@ +name: 🚀 RFC 提案 +description: 我对 Eggjs 框架技术架构功能层面上有重大新增、改进等。 +labels: [RFC proposal] +body: + - type: markdown + attributes: + value: | + 对于 Eggjs 框架功能你是否有重大的新增或改进之类的想法? + + 不过在提交你的新想法或方案之前,麻烦请检阅一下是否之前的帖子中有类似重复的内容。 + - type: textarea + attributes: + label: 请详细告知你的新解决思路(Your new RFCs): + placeholder: | + 1. 描述你希望解决的问题的现状,附上相关的 issue 地址。 + 如果方便的话,请提供截屏或者视频等详细信息。 + + 2. 我能够做一些什么(譬如具体相关的的 API,描述思路,最好是能提供一些伪代码帮助实现)。 + validations: + required: true + - type: checkboxes + attributes: + label: "跟进类型(Follow-up Types):" + description: 此议案跟进类型情况: + options: + - label: 这是某个任务 + - label: 这是一个具体的 PR 的地址(URL) diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml new file mode 100644 index 0000000000..10d4a469f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -0,0 +1,31 @@ +name: 🚀 RFC Proposals +description: I've got a major improvement (or idea) on the technical architecture of the Eggjs framework. +labels: [RFC proposal] +body: + - type: markdown + attributes: + value: | + Any better new/changable functions for the core technical architecture of the Egg.js framework? + + But please make sure there's no duplicated issues related to your idea before submitting. + - type: textarea + attributes: + label: "Please describe your idea in detail:" + placeholder: | + 1. Describe the current situation of the problem you want to solve, + and attach the related issue address. + + If it is possible, please provide screenshots or videos in detail. + + 2. What can I do (related APIs, Your ideas, better to provide some pseudo code to help implementations). + validations: + required: true + - type: checkboxes + attributes: + label: Follow-up type + description: The type of the RFC proposals. + options: + - label: Some Task + - label: PR URL(s) + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 48f9944f27..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ - - -##### Checklist - - -- [ ] `npm test` passes -- [ ] tests and/or benchmarks are included -- [ ] documentation is changed or added -- [ ] commit message follows commit guidelines - -##### Affected core subsystem(s) - - - -##### Description of change - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..444235616c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 1 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/chainalert.yml b/.github/workflows/chainalert.yml new file mode 100644 index 0000000000..17b0f978fb --- /dev/null +++ b/.github/workflows/chainalert.yml @@ -0,0 +1,12 @@ +name: ChainAlert +on: + schedule: + - cron: '0 0 * * *' + push: + branches: [ master ] +jobs: + chainalert: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: checkmarx/chainalert-github-action@v1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..e0fdf68971 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master, 1.x, 3.x ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000000..559003fa3a --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,34 @@ +name: Github Pages + +on: + push: + branches: [ master ] + +jobs: + Runner: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + node-version: [ 18 ] + steps: + - name: Checkout Git Source + uses: actions/checkout@master + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm i -g npminstall && npminstall + + - name: Build Documents + run: npm run site:build && cp vercel.json ./site/dist/vercel.json + + - name: Deploy Documents + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site/dist diff --git a/.github/workflows/nodejs-3.x.yml b/.github/workflows/nodejs-3.x.yml new file mode 100644 index 0000000000..512b93f4a8 --- /dev/null +++ b/.github/workflows/nodejs-3.x.yml @@ -0,0 +1,18 @@ +name: CI for 3.x + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, macos-latest, windows-latest' + version: '14, 16, 18, 20, 22, 23' + install: 'npm i -g npminstall && npminstall' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pr-contributor-welcome.yml b/.github/workflows/pr-contributor-welcome.yml new file mode 100644 index 0000000000..ce5382bee9 --- /dev/null +++ b/.github/workflows/pr-contributor-welcome.yml @@ -0,0 +1,34 @@ +# 当 PR 被合并时,留言欢迎加入共建群 +name: PullRequest Contributor Welcome + +on: + pull_request_target: + types: + - closed + paths: + - 'app/**' + - 'site/**' + - 'config/**' + - 'lib/**' + - 'test/**' + - '*.js' + - '*.ts' + +jobs: + check-merged: + runs-on: ubuntu-latest + needs: read-file + if: github.event.pull_request.merged == true + steps: + - uses: actions-cool/maintain-one-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + body: | + 🎉 Thanks for contribution. Please feel free to join DingTalk Social Community (Provide the PR link please). + + 🎉 感谢参与贡献,欢迎扫码(或搜索群号 21751340)加入钉钉社区(进群后请提供 PR 地址)。 + + + + + body-include: '' diff --git a/.github/workflows/release-3.x.yml b/.github/workflows/release-3.x.yml new file mode 100644 index 0000000000..5d9aed5d12 --- /dev/null +++ b/.github/workflows/release-3.x.yml @@ -0,0 +1,13 @@ +name: Release for 3.x + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: eggjs/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.github/workflows/vercel-preview.yml b/.github/workflows/vercel-preview.yml new file mode 100644 index 0000000000..359c81b2c8 --- /dev/null +++ b/.github/workflows/vercel-preview.yml @@ -0,0 +1,23 @@ +name: GitHub Actions Vercel Preview Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches-ignore: + - master + - gh-pages + +jobs: + Deploy-Preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Vercel CLI + run: npm install --global vercel@canary + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/vercel-production.yml b/.github/workflows/vercel-production.yml new file mode 100644 index 0000000000..2e5535c21e --- /dev/null +++ b/.github/workflows/vercel-production.yml @@ -0,0 +1,21 @@ +name: GitHub Actions Vercel Production Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: + - master +jobs: + Deploy-Production: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Vercel CLI + run: npm install --global vercel@canary + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.gitignore b/.gitignore index 4a25a09c51..cd6dbe2e05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -node_modules +/node_modules coverage *.log npm-debug.log @@ -10,12 +10,30 @@ run .idea .DS_Store .tmp -docs/CONTRIBUTING.md -docs/README.md -docs/db.json -docs/source/release/index.md -docs/source/member_guide.md -docs/**/contributing.md -docs/public +*-lock.json +*-lock.yaml +yarn.lock !test/fixtures/apps/loader-plugin/node_modules +.editorconfig +*clinic-flame* +*clinic-doctor* +.nyc_output/ +test/fixtures/apps/app-ts/**/*.js +!test/fixtures/apps/app-ts/node_modules +!test/fixtures/apps/app-ts/node_modules/**/*.js +test/fixtures/apps/app-ts-esm/**/*.js +test/fixtures/apps/app-ts-type-check/**/*.js +test/fixtures/apps/app-ts-type-check/**/*.d.ts +test/fixtures/apps/app-ts/**/*.d.ts +test/fixtures/apps/app-ts-esm/**/*.d.ts + +# site +site/dist +!site/public + +.umi +.umi-production +.vercel +package-lock.json +.tshy* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 03736ab24c..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -sudo: false -language: node_js -node_js: - - '4' - - '6' -install: - - npm i npminstall && npminstall -script: - - npm run ci -after_script: - - npminstall codecov && codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..c2660febef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2409 @@ +# Changelog + +## [3.30.1](https://github.com/eggjs/egg/compare/v3.30.0...v3.30.1) (2025-01-19) + + +### Bug Fixes + +* redefine urllib export types ([#5386](https://github.com/eggjs/egg/issues/5386)) ([02d9fdb](https://github.com/eggjs/egg/commit/02d9fdb7c127f9e8150bdb8f23977a2effeaf6ae)) + +## [3.30.0](https://github.com/eggjs/egg/compare/v3.29.0...v3.30.0) (2025-01-09) + + +### Features + +* dump ignore support key path ([#5380](https://github.com/eggjs/egg/issues/5380)) ([74346c2](https://github.com/eggjs/egg/commit/74346c2d056cab63c3c3a905c1e8afbf857451b4)) + +## [3.24.1](https://github.com/eggjs/egg/compare/v3.24.0...v3.24.1) (2024-06-07) + + +### Bug Fixes + +* serverTimeout default to 0 (no timeout) ([#5325](https://github.com/eggjs/egg/issues/5325)) ([44ab507](https://github.com/eggjs/egg/commit/44ab507b6299c849a2fe31bee54f3a1909aa9d53)) + +## [3.24.0](https://github.com/eggjs/egg/compare/v3.23.0...v3.24.0) (2024-06-07) + + +### Features + +* add bodyParser.onProtoPoisoning type define ([#5324](https://github.com/eggjs/egg/issues/5324)) ([b3582e0](https://github.com/eggjs/egg/commit/b3582e02d0f5d85edbc03f3f20c4cdcc65619dc1)) + +## [3.23.0](https://github.com/eggjs/egg/compare/v3.22.0...v3.23.0) (2024-05-08) + + +### Features + +* use utility@2 ([#5312](https://github.com/eggjs/egg/issues/5312)) ([9bf5f22](https://github.com/eggjs/egg/commit/9bf5f22bfae44a1f44651efba7b3e167f9040714)) + +## [3.22.0](https://github.com/eggjs/egg/compare/v3.21.0...v3.22.0) (2024-04-12) + + +### Features + +* app.httpClient alias to app.httpclient ([#5304](https://github.com/eggjs/egg/issues/5304)) ([a6ebe0f](https://github.com/eggjs/egg/commit/a6ebe0f49a9e1a8506c26a0bb4e89a32528aa727)) + +## [3.21.0](https://github.com/eggjs/egg/compare/v3.20.0...v3.21.0) (2024-03-31) + + +### Features + +* tiny improvements for "convertValue" ([#5302](https://github.com/eggjs/egg/issues/5302)) ([794d7f3](https://github.com/eggjs/egg/commit/794d7f3e89c2a283e38d2082b407b79e480f0b50)) + +## [3.20.0](https://github.com/eggjs/egg/compare/v3.19.0...v3.20.0) (2024-02-22) + + +### Features + +* urllib-next alias to npm:urllib ([#5299](https://github.com/eggjs/egg/issues/5299)) ([61cd51d](https://github.com/eggjs/egg/commit/61cd51d02a86cb6ca8d510fb3ea3a1ed73f7beec)) + +## [3.19.0](https://github.com/eggjs/egg/compare/v3.18.0...v3.19.0) (2024-02-08) + + +### Features + +* 优化中文文档表达 ([#5290](https://github.com/eggjs/egg/issues/5290)) ([d73046b](https://github.com/eggjs/egg/commit/d73046bb9165c74473b6f28842de23c880e78a87)) + +## [3.18.0](https://github.com/eggjs/egg/compare/v3.17.7...v3.18.0) (2024-01-21) + + +### Features + +* auto set custom logger with onelogger ([#5287](https://github.com/eggjs/egg/issues/5287)) ([1fd79a2](https://github.com/eggjs/egg/commit/1fd79a2715b4eded47ae77d955de5fa50efa573b)) + +## [3.17.7](https://github.com/eggjs/egg/compare/v3.17.6...v3.17.7) (2024-01-11) + + +### Bug Fixes + +* omit koa application ctxStorage and currentContext define ([#5285](https://github.com/eggjs/egg/issues/5285)) ([4c24dac](https://github.com/eggjs/egg/commit/4c24dac1e9ec86051d806dd6940c1d5095723b4d)) + +## [3.17.6](https://github.com/eggjs/egg/compare/v3.17.5...v3.17.6) (2024-01-10) + + +### Bug Fixes + +* typo on index.d.ts ([#5284](https://github.com/eggjs/egg/issues/5284)) ([17ee60b](https://github.com/eggjs/egg/commit/17ee60b35a48c22eb90f392b688b4347c44b490d)) + +## [3.17.5](https://github.com/eggjs/egg/compare/v3.17.4...v3.17.5) (2023-10-12) + + +### Bug Fixes + +* set body parser error to status 400 by default ([#5262](https://github.com/eggjs/egg/issues/5262)) ([5ac26a3](https://github.com/eggjs/egg/commit/5ac26a39b4256b6a3fcd55da947a47a82811c7c1)) + +## [3.17.4](https://github.com/eggjs/egg/compare/v3.17.3...v3.17.4) (2023-08-01) + + +### Bug Fixes + +* use app.logger instead of ctx.logger ([#5246](https://github.com/eggjs/egg/issues/5246)) ([b700fb9](https://github.com/eggjs/egg/commit/b700fb962a866c6e699f5c88b342960cc5ee0b78)), closes [/github.com/eggjs/egg/issues/5213#issuecomment-1657771583](https://github.com/eggjs//github.com/eggjs/egg/issues/5213/issues/issuecomment-1657771583) + +## [3.17.3](https://github.com/eggjs/egg/compare/v3.17.2...v3.17.3) (2023-06-29) + + +### Bug Fixes + +* add missing args definition on runSchedule ([#5232](https://github.com/eggjs/egg/issues/5232)) ([f90763b](https://github.com/eggjs/egg/commit/f90763b164897bf4992b6ec58c4eae20775c0006)) + +## [3.17.2](https://github.com/eggjs/egg/compare/v3.17.1...v3.17.2) (2023-06-25) + + +### Bug Fixes + +* don't require inspector module on production env ([#5228](https://github.com/eggjs/egg/issues/5228)) ([398fe15](https://github.com/eggjs/egg/commit/398fe15eb28c3bbe8d79a0c2b129d55922f45a9a)) + +## [3.17.1](https://github.com/eggjs/egg/compare/v3.17.0...v3.17.1) (2023-06-22) + + +### Bug Fixes + +* compatible with content-type extra semicolon ([#5217](https://github.com/eggjs/egg/issues/5217)) ([cfdca36](https://github.com/eggjs/egg/commit/cfdca36b4ee84397ed2cb1987982d502a3c8af0a)) + +## [3.17.0](https://github.com/eggjs/egg/compare/v3.16.1...v3.17.0) (2023-06-19) + + +### Features + +* add getSingletonInstance alias to singleton.get(id) ([#5216](https://github.com/eggjs/egg/issues/5216)) ([9868768](https://github.com/eggjs/egg/commit/98687685bb095d37166d3a66890f0164428a8e53)) + +## [3.16.1](https://github.com/eggjs/egg/compare/v3.16.0...v3.16.1) (2023-06-15) + + +### Bug Fixes + +* ipc not work with worker_threads mode ([#5210](https://github.com/eggjs/egg/issues/5210)) ([03c8cf7](https://github.com/eggjs/egg/commit/03c8cf743d1fb56a55dbc633f088b08410423c5a)) + +## [3.16.0](https://github.com/eggjs/egg/compare/v3.15.0...v3.16.0) (2023-05-10) + + +### Features + +* use egg-security@3.0.0 ([#5182](https://github.com/eggjs/egg/issues/5182)) ([a13b35e](https://github.com/eggjs/egg/commit/a13b35e05ca660fee3663db9381cdf44d63e44a0)) + +## [3.15.0](https://github.com/eggjs/egg/compare/v3.14.2...v3.15.0) (2023-01-28) + + +### Features + +* runInAnonymousContextScope support req ([#5134](https://github.com/eggjs/egg/issues/5134)) ([615d660](https://github.com/eggjs/egg/commit/615d6608ab2fc66af848fb82ce41ed359f41bfb0)) + +## [3.14.2](https://github.com/eggjs/egg/compare/v3.14.1...v3.14.2) (2023-01-20) + + +### Bug Fixes + +* **types:** app.router.url params should be optional ([#5132](https://github.com/eggjs/egg/issues/5132)) ([dda6bb3](https://github.com/eggjs/egg/commit/dda6bb3674af4acbdd7d5eb2f2ca373c714c7d2d)) + +## [3.14.1](https://github.com/eggjs/egg/compare/v3.14.0...v3.14.1) (2023-01-17) + + +### Bug Fixes + +* export urllib types directly ([#5128](https://github.com/eggjs/egg/issues/5128)) ([483bf1d](https://github.com/eggjs/egg/commit/483bf1d12bee5157f9a95e0a5b7403fc7562900e)) + +## [3.14.0](https://github.com/eggjs/egg/compare/v3.13.0...v3.14.0) (2023-01-17) + + +### Features + +* export urllib types ([#5127](https://github.com/eggjs/egg/issues/5127)) ([1f7b082](https://github.com/eggjs/egg/commit/1f7b08298ff4c6f118b93d3b3bf8e1fb3ac37db1)) + +## [3.13.0](https://github.com/eggjs/egg/compare/v3.12.0...v3.13.0) (2023-01-13) + + +### Features + +* log app start timeline on coreLogger ([#5122](https://github.com/eggjs/egg/issues/5122)) ([6c4e8bc](https://github.com/eggjs/egg/commit/6c4e8bca1b829ecd47e98cc9b0544c7aa874e755)) + +## [3.12.0](https://github.com/eggjs/egg/compare/v3.11.1...v3.12.0) (2023-01-04) + + +### Features + +* siteFile favicon config support async function type ([#5114](https://github.com/eggjs/egg/issues/5114)) ([667684f](https://github.com/eggjs/egg/commit/667684f79d8485ee9d9a03bf99077fa2dfef5507)) + +## [3.11.1](https://github.com/eggjs/egg/compare/v3.11.0...v3.11.1) (2023-01-03) + + +### Bug Fixes + +* remove duplicate identifier ssrf ([#5113](https://github.com/eggjs/egg/issues/5113)) ([2b407eb](https://github.com/eggjs/egg/commit/2b407ebce9112d96e5e8a452eef09bcc70496e92)) + +## [3.11.0](https://github.com/eggjs/egg/compare/v3.10.0...v3.11.0) (2023-01-02) + + +### Features + +* add ssrf declaration ([#4687](https://github.com/eggjs/egg/issues/4687)) ([b1414f2](https://github.com/eggjs/egg/commit/b1414f2c749da5ab9bf07abf26ff75eac0b9cb73)) + +## [3.10.0](https://github.com/eggjs/egg/compare/v3.9.2...v3.10.0) (2023-01-02) + + +### Features + +* use egg-core@5 ([#5111](https://github.com/eggjs/egg/issues/5111)) ([7b8edbf](https://github.com/eggjs/egg/commit/7b8edbf322ed59c76ce3a85cd4595605d743fb80)) + +## [3.9.2](https://github.com/eggjs/egg/compare/v3.9.1...v3.9.2) (2022-12-21) + + +### Bug Fixes + +* currentContext typo ([#5107](https://github.com/eggjs/egg/issues/5107)) ([713a081](https://github.com/eggjs/egg/commit/713a081475189ef6d00c85a559849ff97f824d11)) + +## [3.9.1](https://github.com/eggjs/egg/compare/v3.9.0...v3.9.1) (2022-12-18) + + +### Bug Fixes + +* Enable auto npm release workflow ([#5102](https://github.com/eggjs/egg/issues/5102)) ([13bbe6c](https://github.com/eggjs/egg/commit/13bbe6c24e1c8160ae629e12c81e30e27b6c3dba)) + +## [3.9.1](https://github.com/eggjs/egg/compare/v3.9.0...v3.9.1) (2022-12-18) + + +### Bug Fixes + +* Enable auto npm release workflow ([#5102](https://github.com/eggjs/egg/issues/5102)) ([13bbe6c](https://github.com/eggjs/egg/commit/13bbe6c24e1c8160ae629e12c81e30e27b6c3dba)) + +--- + +# History + +## 2022-12-16, Version 3.9.0 @fengmk2 + +### Notable Changes + +* **features** + * 📦 NEW: Run async function in the anonymous context scope + + ```js + await app.runInAnonymousContextScope(async ctx => { + // run with anonymous ctx here + }); + ``` + +### Commits + + * [[`af1206904`](http://github.com/eggjs/egg/commit/af12069041c1ea11217688c9c17d3712a44d3422)] - chore: update workflow for gh-pages (#5098) (Suyi <>) + * [[`344139e47`](http://github.com/eggjs/egg/commit/344139e4759f56ab2beca2e2a5c2783160396ba9)] - 🐛 FIX: Typo on HttpClient request (#5097) (fengmk2 <>) + * [[`1021faf78`](http://github.com/eggjs/egg/commit/1021faf78e5f23fa366c0034a38f81b0f361e9ec)] - 👌 IMPROVE: Keep more compatible d.ts on httpclient request (#5092) (fengmk2 <>) + * [[`9d6acfd7c`](http://github.com/eggjs/egg/commit/9d6acfd7c3266ae6a56e45cb7a72473d628f6e16)] - 📦 NEW: Run async function in the anonymous context scope (#5094) (fengmk2 <>) + +## 2022-12-12, Version 3.8.0 @fengmk2 + +### Notable Changes + +* **features** + * Upgrade egg-schedule@4 to support `app.currentContext` on scheduler + +### Commits + + * [[`75d025b24`](http://github.com/eggjs/egg/commit/75d025b24e5e3016f2df84e2ba1901f42156c0b7)] - 👌 IMPROVE: Upgrade egg-schedule to v4 (#5088) (fengmk2 <>) + +## 2022-12-11, Version 3.7.0 @fengmk2 + +### Notable Changes + +* **features** + * 📦 NEW: Set `config.logger.enableFastContextLogger = true` to enable faster context logger + +### Commits + + * [[`e94c7df63`](http://github.com/eggjs/egg/commit/e94c7df63e1812da672dbaf7200e652cc4537c7b)] - 📦 NEW: Upgrade egg-logger v3 to enable localStorage (#5085) (fengmk2 <>) + * [[`c76e16cf7`](http://github.com/eggjs/egg/commit/c76e16cf7fb67d5f2c1b19252e01a5e3fed9cf96)] - 📖 DOC: Use @eggjs/tsconfig for tsconfig.json (#5066) (fengmk2 <>) + +## 2022-12-09, Version 3.6.0 @fengmk2 + +### Notable Changes + +* **features** + * 🚀🚀🚀 Support `app.ctxStorage` and `app.currentContext` to get current execute ctx, see [koa#1455](https://github.com/koajs/koa/pull/1455) + +### Commits + + * [[`bf36904e0`](http://github.com/eggjs/egg/commit/bf36904e0fb1d4477ebb7068dd8ad6726d29182f)] - 📦 NEW: Add ctxStorage and currentContext d.ts (#5079) (fengmk2 <>) + * [[`c68992ab7`](http://github.com/eggjs/egg/commit/c68992ab71b854f825df0ff3ea4b82e7666ec828)] - chore: ignore gp-pages branch while deploying preview (#5077) (Suyi <>) + * [[`13906825b`](http://github.com/eggjs/egg/commit/13906825bc3fab260aa0dd8888ce9fd19f2f70c5)] - chore: use actions to deploy vercel project (#5076) (Suyi <>) + * [[`5d825bb59`](http://github.com/eggjs/egg/commit/5d825bb59ed691bd45c3a8b2f6c222496910e250)] - docs: update communite links (#5073) (Suyi <>) + +## 2022-11-28, Version 3.5.1 @killagu + +### Notable Changes + +* **fixes** + * Dump `config/timing` when app start timeout + +### Commits + + * [[`c859506a0`](http://github.com/eggjs/egg/commit/c859506a094181f5f45db16a8501daaaea56b3d3)] - fix: dump config/timing when timeout (#5069) (killa <>) + +## 2022-11-15, Version 3.5.0 @fengmk2 + +### Notable Changes + +* **features** + * Auto disable cluster-client heartbeat checker on debug mode + +### Commits + + * [[`6de5cba5d`](http://github.com/eggjs/egg/commit/6de5cba5d0d02d09e9e6ee71f9e7b1cb3d65c24e)] - feat: disable cluster-client heartbeat on debug mode (#5059) (sinkhaha <<1468709106@qq.com>>) + +## 2022-11-07, Version 3.4.0 @fengmk2 + +### Notable Changes + +* **features** + * Upgrade egg-cluster v2 to support worker_threads start mode + * Drop httpclient callback and thunk style, a breaking change to egg@2 + * Print warnning log when boot action takes more than 5000ms + * Don't need to patch keep-alive header on Node.js >= 14.20.0 + +### Commits + + * [[`2b5f289bb`](http://github.com/eggjs/egg/commit/2b5f289bba3bd14c2867136b5dcbf3bed5cfdf9e)] - 📦 NEW: Use egg-cluster v2 (#5055) (fengmk2 <>) + * [[`610a39e7f`](http://github.com/eggjs/egg/commit/610a39e7f41a17a2123705691d6c1bfdc3e12f88)] - 👌 IMPROVE: Drop httpclient callback and thunk style (#5052) (fengmk2 <>) + * [[`3a941d669`](http://github.com/eggjs/egg/commit/3a941d669cc1d2c12a2caad4dd24492e98444348)] - 👌 IMPROVE: Print warnning log when boot action takes more than 5000ms (#5049) (fengmk2 <>) + * [[`d820b739b`](http://github.com/eggjs/egg/commit/d820b739b95207bdea8c9b4c3da0f5059bc0113c)] - 👌 IMPROVE: Don't need to patch keep-alive header on Node.js >= 14.20.0 (#5051) (fengmk2 <>) + * [[`6ac4cdbfb`](http://github.com/eggjs/egg/commit/6ac4cdbfbb35905f6f315f51122c1badcb913b5c)] - 🤖 TEST: Add Node.js 19 ci runner (#5050) (fengmk2 <>) + * [[`d05cc015e`](http://github.com/eggjs/egg/commit/d05cc015e4a748bf41a4dbf46e978d1f4ad44954)] - docs: fix Application description (#5044) (ldc-37 <<34739463+ldc-37@users.noreply.github.com>>) + +## 2022-09-28, Version 3.3.3 @fengmk2 + +### Notable Changes + +* **fixes** + * Allow override HttpClientNext + +### Commits + * [[`7ee19e840`](http://github.com/eggjs/egg/commit/7ee19e8402b1d23ecdc1791e044a1902049e14dd)] - 🐛 FIX: Allow override HttpClientNext (#5037) (fengmk2 <>) + +## 2022-09-27, Version 3.3.2 @atian25 + +### Notable Changes + +* **fixes** + * update multipart 3.1.0, https://github.com/eggjs/egg-multipart/pull/56 + +### Commits + * [[`201bfa749`](http://github.com/eggjs/egg/commit/201bfa7492920aafad71b7845e5cc6eaef69f8bc)] - fix: update multipart 3.1.0 (#5034) (TZ | 天猪 <>) + + +## 2022-09-26, Version 3.3.1 @fengmk2 + +### Notable Changes + +* **fixes** + * fallback egg-multipart@2 to support filename with non-ASCII characters +### Commits + + * [[`acadb28e2`](http://github.com/eggjs/egg/commit/acadb28e2814b0b91828e0766673f199d7767f3a)] - fix: fallback egg-multipart to v2 (#5032) (fengmk2 <>) + + +## 2022-09-23, Version 3.3.0 @fengmk2 + +### Notable Changes + +* **features** + * Support config `serverGracefulIgnoreCode` to ignore error avoid process exit when uncatch error emit + See https://github.com/node-modules/graceful/pull/13 +### Commits + + * [[`a0761d65f`](http://github.com/eggjs/egg/commit/a0761d65f5df1002853c169efedab969636247d3)] - feat(graceful): support serverGracefulIgnoreCode (#5027) (hyj1991 <>) + * [[`8b8dd3be9`](http://github.com/eggjs/egg/commit/8b8dd3be95bb53ad3c732b8bc9c20566021e955f)] - chore: remove jsdoc and disable vercel comment (#5026) (Suyi <>) + * [[`f4225339f`](http://github.com/eggjs/egg/commit/f4225339f6235f78fe53d34d1eb0993faa410b36)] - test: fix ci (#5025) (TZ | 天猪 <>) + * [[`5de994b9c`](http://github.com/eggjs/egg/commit/5de994b9c4cd17f9ecd4d4083c20b29f399a9e40)] - chore: fix action for gh-pages (#5024) (Suyi <>) + +## 2022-09-21, Version 3.2.0 @fengmk2 + +### Notable Changes + +**features** + * [[`733d66989`](http://github.com/eggjs/egg/commit/733d66989d1f8657ce55b6032944188da635b8f0)] - feat: update egg-multipart 2.x -> 3.x (#5023) (TZ | 天猪 <>) + * [[`2ffb37ab5`](http://github.com/eggjs/egg/commit/2ffb37ab59395c9b14f153f91abb9f816a5e98ea)] - feat: Support urllib@3 (#5000) (fengmk2 <>) + +**others** + * [[`485781389`](http://github.com/eggjs/egg/commit/485781389e548ff0cf1eb107fea93c1bb01170d7)] - docs: update the version of the required Node (#5021) (Maledong <>) + * [[`bbd0e432e`](http://github.com/eggjs/egg/commit/bbd0e432e52832cc7a3d4b26a0141d7eb02e3793)] - chore: change the templates of bug/suggestion report (#5019) (Maledong <>) + * [[`2c5ba484a`](http://github.com/eggjs/egg/commit/2c5ba484a2dd8f214b9cdb53aa952688bc54cb2b)] - 🐛 FIX: Add config.httpclient.useHttpClientNext defined (#5001) (fengmk2 <>) + +## 2022-08-28, Version 3.1.0 @fengmk2 + +### Notable Changes + +* **features** + * Support urllib@3 by `config.httpclient.useHttpClientNext = true`, see [#4847](https://github.com/eggjs/egg/issues/4847) + +### Commits + * [[`2c5ba484a`](http://github.com/eggjs/egg/commit/2c5ba484a2dd8f214b9cdb53aa952688bc54cb2b)] - 🐛 FIX: Add config.httpclient.useHttpClientNext defined (#5001) (fengmk2 <>) + * [[`2ffb37ab5`](http://github.com/eggjs/egg/commit/2ffb37ab59395c9b14f153f91abb9f816a5e98ea)] - feat: Support urllib@3 (#5000) (fengmk2 <>) + +## 2022-08-21, Version 3.0.0 @fengmk2 + +**features** + * Drop Node.js 8, 10, 12 supports, this release is a LTS version for egg@2, see https://github.com/eggjs/egg/issues/3644#issuecomment-1221460692 + +## 2022-06-17, Version 2.36.0 @atian25 + +**features** + * [[`e0b93e023`](http://github.com/eggjs/egg/commit/e0b93e023e1258c4037c68dacfc41fc304602bbc)] - feat: should log unfinished timing item (#4968) (TZ | 天猪 <>) + +**others** + * [[`7f1689f9f`](http://github.com/eggjs/egg/commit/7f1689f9fbd286bde3b8b5aebf86af09a599359c)] - chore: typo CSRF on router.md (#4962) (Homyee King <>) + * [[`e31c09c20`](http://github.com/eggjs/egg/commit/e31c09c2001b15fbc2431f4c36f6e59da5e3ebca)] - chore: fix some comments (#4937) (Maledong <>) + * [[`b0c17fdd0`](http://github.com/eggjs/egg/commit/b0c17fdd02512f743786203c326dc86be636f9a6)] - chore: remove git.io (#4940) (Baoshuo Ren <>) + * [[`12755e275`](http://github.com/eggjs/egg/commit/12755e27555b8f84a745c319c89b1c4d75ae3f78)] - test: Create codeql-analysis.yml (#4935) (fengmk2 <>) + * [[`8078917fd`](http://github.com/eggjs/egg/commit/8078917fd66c41d21b0f2c738f77cc7916edfaca)] - chore: package upgrade and unittest fixture (#4933) (Maledong <>) + * [[`a5a358ceb`](http://github.com/eggjs/egg/commit/a5a358cebc78734d45a450a641913ae242c5dc70)] - chore: fix contributors badges on README.md (#4930) (XiaoRui <>) + +## 2022-04-01, Version 2.35.0 @mansonchor + +**features** + * [[`c1313f5ef`](http://github.com/eggjs/egg/commit/c1313f5ef960e5aaad7f04adb6665679f2ec10e2)] - feat: dumpConfig add appInfo (#4917) (mansonchor.github.com <>) + +**others** + * [[`4e5309188`](http://github.com/eggjs/egg/commit/4e5309188a60393435d5ab2df65ca67186f31035)] - test: add ChainAlert action (#4908) (fengmk2 <>) + +## 2022-03-16, Version 2.34.0 + +**features** + * [[`caacd09c3`](http://github.com/eggjs/egg/commit/caacd09c38aae03fc291febbb97a43c8ecbdc221)] - feat: siteFile support custom control-cache (#4902) (binginsist <>) + +**others** + * [[`f97fe4a8c`](http://github.com/eggjs/egg/commit/f97fe4a8c8c0b5f8c097055213f9e7177b9ab2dd)] - test: change error code assert (#4907) (fengmk2 <>) + * [[`a7aa7f37d`](http://github.com/eggjs/egg/commit/a7aa7f37d901afd4c26a2a9aa57fe938b2109e94)] - docs: typo fix on deployment.zh-CN.md (#4906) (Krryxa <>) + * [[`d3fe13aa2`](http://github.com/eggjs/egg/commit/d3fe13aa25065995cfa9d78461be80194176f183)] - docs: typo fix on security.zh-CN.md (#4905) (Krryxa <>) + * [[`2dc723129`](http://github.com/eggjs/egg/commit/2dc723129614bd727e86871ae3e7cfc60166dd81)] - docs: use egg brand color for site (#4900) (Peach <>) + * [[`11bbd8527`](http://github.com/eggjs/egg/commit/11bbd852731b1583cae5a5d519baa20960c0521d)] - docs: enhance (#4884) (Suyi <>) + * [[`76d014bd5`](http://github.com/eggjs/egg/commit/76d014bd583c6d0b9b9f40ac2e6e7ba9dd66e8ed)] - docs: update node version (#4886) (lxinr <<33972246+lxinr@users.noreply.github.com>>) + * [[`9003cb5ad`](http://github.com/eggjs/egg/commit/9003cb5ad370d0c07b2baee88f3b25c597ac3929)] - docs: update https://registry.npm.taobao.org to https://registry.npmmirror.com (#4881) (Non-Official NPM Mirror Bot <<99484857+npmmirror@users.noreply.github.com>>) + * [[`b47409770`](http://github.com/eggjs/egg/commit/b47409770d76f03eb1ea0476be9e00207186c42e)] - docs: dumi (#4879) (Suyi <>) + * [[`56816dbc5`](http://github.com/eggjs/egg/commit/56816dbc59dbb1a4973ca60130c9ff3f5be8b2da)] - docs (sequelize): Changed `config.sequelize` to `exports.sequelize` in configuration part (#4873) (Aelita <<45784210+xsjcTony@users.noreply.github.com>>) + * [[`20842f9c2`](http://github.com/eggjs/egg/commit/20842f9c216ed538924936163b7ed18437c54cd7)] - docs: Add license scan report and status (#4880) (fossabot <>) + +## 2021-12-07, Version 2.33.1 + +**features** + * [[`18dcadc1c`](http://github.com/eggjs/egg/commit/18dcadc1cf6c9837de605916a0d8b161a63e7218)] - feat: meta middleware x-readtime support performanceStarttime (#4827) (fengmk2 <>) + +**others** + * [[`8659d4bc3`](http://github.com/eggjs/egg/commit/8659d4bc37e0652d66d04d2e5504fdc0ef2f7f7d)] - docs: update contributors (#4826) (Suyi <>) + * [[`4d18732c7`](http://github.com/eggjs/egg/commit/4d18732c79e44a84140df05e879b8b5f569c2b4b)] - chore: remove @types/urllib from autod (fengmk2 <>) + +## 2021-12-06, Version 2.33.0 + +**features** + * [[`0f6589e1d`](http://github.com/eggjs/egg/commit/0f6589e1dc9e538434eb1580327556d5aa264822)] - feat: support better logger timer in precise milliseconds (#4806) (fengmk2 <>) + +## 2021-11-15, Version 2.32.0 @atian25 + +### Notable Changes + +* **features** + * handle ENETUNREACH error on httpclient + +### Commits + + * [[`189c47804`](http://github.com/eggjs/egg/commit/189c478048d820b7b1a6ba6e8bce3444604876ff)] - feat: handle ENETUNREACH error on httpclient (#4792) (fengmk2 <>) + + +## 2021-10-18, Version 2.31.0 @killagu + +### Notable Changes + +* **typing** + * support ssrf typing in config + +### Commits + +* [[`debfda7ab`](https://github.com/eggjs/egg/commit/debfda7ab38f4893b6f122abfbf3e5288af1441e)] - feat(config): support ssrf field in security config. (#4778) (Jasin Yip <>) + +## 2021-08-09, Version 2.30.0 @mansonchor + +### Notable Changes + +* **features** + * support disableDNSCache in one request even though config set to `enableDNSCache: true` + +* **docs** + * update ts docs, add missing zh-CN doc + * typo fix + +### Commits + + * [[`13dd55076`](https://github.com/eggjs/egg.git/commit/13dd5507694a57a11e12d1ac6f71ba4a562d88c0)] - feat: support disableDNSCache by request args handle (#4728) (mansonchor.github.com <>) + * [[`1b4fde454`](https://github.com/eggjs/egg.git/commit/1b4fde454d2d61200a8b066ba841ad6d81b5b69d)] - unittest: rename and remove some useless tests (#4705) (Maledong <>) + * [[`156980d36`](https://github.com/eggjs/egg.git/commit/156980d369570531c1ef9cf842f02f513b56fe4a)] - doc: Typo fixture (#4707) (Maledong <>) + * [[`27aa49b59`](https://github.com/eggjs/egg.git/commit/27aa49b5945f08fa6b636479cf4cba7822e3af2d)] - doc: Typo fixture:「,」->「,」 (#4708) (陈煮酒 <<501205587@qq.com>>) + * [[`1f02a8d45`](https://github.com/eggjs/egg.git/commit/1f02a8d4560ca3334425e646c1ac87226aba3a63)] - doc: Add the missing zh-CN trans for typescript (#4703) (Maledong <<52018749+MaledongGit@users.noreply.github.com>>) + * [[`f5cf0d965`](https://github.com/eggjs/egg.git/commit/f5cf0d965fa4077da10e87f070e113496077872c)] - doc: "登陆" should be "登录" (#4697) (HOU Ce <<594965698@qq.com>>) + * [[`750558400`](https://github.com/eggjs/egg.git/commit/750558400e3bd5f39658dfcbedd4af7bc0bdda2a)] - docs: update ts docs (#4666) (吖猩 <>) + * [[`93d2b04b9`](https://github.com/eggjs/egg.git/commit/93d2b04b985145f27a39335300a78002a61da2a8)] - docs: fix loaderUpdate.md didReady example (#4652) (shadyzoz <>) + +## 2021-04-13, Version 2.29.4 @popomore + +### Notable Changes + +* **fixes** + * remove internal interval handler when close agent +* **docs** + * Added english doc to how-to-migrate-from-1.x, Thx @ZixiaoWang + * typo improvement + +### Commits + + * [[`ce234226b`](http://github.com/eggjs/egg/commit/ce234226bf0c43f03aedf727a7d195ae4175c01f)] - fix: remove internal interval handler when close agent (#4654) (Harry Chen <>) + * [[`6a6d68fb2`](http://github.com/eggjs/egg/commit/6a6d68fb228a3eae9b5cab3ba7afe0892d5e08ea)] - Typo fixture:制定 -> 指定 (#4639) (华晨 <>) + * [[`603c74b58`](http://github.com/eggjs/egg/commit/603c74b581d6b3a9ad80170335425b0876ffcb1f)] - docs: Added english doc to how-to-migrate-from-1.x (#4630) (Jacky Wang <>) + * [[`693df6066`](http://github.com/eggjs/egg/commit/693df60661735008e8a758258fc2df0bb9783f04)] - docs: fix schedule reference (#4556) (xuxu <>) + * [[`4ebbe8143`](http://github.com/eggjs/egg/commit/4ebbe814386102377870959129e831a3b20133c1)] - docs: fix typo (#4596) (suinia <>) + +## 2021-02-19, Version 2.29.3 @killa + +### Notable Changes + +* **fixes** + * fix ctx body typing + +### Commits + * [[`e9fba1b7b`](http://github.com/eggjs/egg/commit/e9fba1b7bbe3f54b023262aeb5487b31047e119e)] - fix: fix ctx body as any (#4613) (killa <>) + +## 2021-02-18, Version 2.29.2 @killa + +### Notable Changes + +* **fixes** + * fix query typing + * add overrideIgnore define + +### Commits + + * [[`99682e4bd`](http://github.com/eggjs/egg/commit/99682e4bd5afe1697cab02001e919b967c264869)] - fix: fix query typing (#4611) (killa <>) + * [[`26017ee2e`](http://github.com/eggjs/egg/commit/26017ee2e49bd8a42b660baabe31284e00f81bcb)] - chore: fix comment typo at request.js (#4513) (Albert 理斯特 <>) + * [[`34048c275`](http://github.com/eggjs/egg/commit/34048c275afe1126716ef4265b667169e9866e53)] - fix: add overrideIgnore define (#4490) (kotot <<317643941@qq.com>>) + * [[`d652658d1`](http://github.com/eggjs/egg/commit/d652658d1205d0fbc73b4417c2ae54ee0a24f395)] - docs: d2 ads (#4508) (Suyi <>) + +## 2020-10-19, Version 2.29.1 @atian25 + +### Notable Changes + +* **fixes** + * revert clear timing after ready, only disable + * won't set keep-alive header at Node.js ^12.19.0 || >=14.8.0 + +### Commits + + * [[`9f653afe7`](http://github.com/eggjs/egg/commit/9f653afe790e0ead44109c68dfffb8353fdca56c)] - fix: remove clear timing && skip keep-alive after 12.19.0 (#4497) (TZ | 天猪 <>) + + +## 2020-09-23, Version 2.29.0 @atian25 + +### Notable Changes + +* **features** + * dumpconfig also dump disabled plugin + +* **docs** + * csrf double cookie defense should enabled on all method + * test case for env.EGG_APP_CONFIG + * optimize multiple env configuration description + +### Commits + + * [[`cc80c6ab8`](http://github.com/eggjs/egg/commit/cc80c6ab86c71b1c9ea244065d4766297bfb6c17)] - feat: dumpconfig also dump disabled plugin (#4480) (TZ | 天猪 <>) + * [[`1d32771e5`](http://github.com/eggjs/egg/commit/1d32771e5aeb8fa8546ad8bfacf2f438973afae0)] - doc: csrf double cookie defense should enabled on all method (#3881) (Hongcai Deng <>) + * [[`504e4bebc`](http://github.com/eggjs/egg/commit/504e4bebcef03830be7c7432210b5b6b1de9d06b)] - test: for env.EGG_APP_CONFIG (#4468) (TZ | 天猪 <>) + * [[`ff4dfaa09`](http://github.com/eggjs/egg/commit/ff4dfaa098ad40ab7a0773c44d409829fcbb0e41)] - docs(config): optimize multiple env configuration description (#4406) (Andy <>) + * [[`b283791da`](http://github.com/eggjs/egg/commit/b283791dab3c86beb14506668e2aa3ef21cb78e6)] - docs: update badge to github action (#4463) (TZ | 天猪 <>) + + +## 2020-09-08, Version 2.28.0 @atian25 + +### Notable Changes + +* **features** + * clear & disable timing after ready + +* **fixes** + * only set keep-alive header before Node.js 14.8.0 + +* **typings** + * Added missing types in HttpClientConfig + * export EggLogger/EggHttpClient/EggContextHttpClient + +* **docs** + * fixed grammatical and spelling errors + * update compress url + +### Commits + + * [[`b31b47df1`](http://github.com/eggjs/egg/commit/b31b47df10722bfac7ac13771db534f0200fc6ce)] - feat: clear & disable timing after ready (#4421) (TZ | 天猪 <>) + * [[`d25d32e58`](http://github.com/eggjs/egg/commit/d25d32e584b0bfd80f21cc522b91ac465f2852ac)] - fix: only set keep-alive header before Node.js 14.8.0 (#4457) (TZ | 天猪 <>) + * [[`a7ae46c84`](http://github.com/eggjs/egg/commit/a7ae46c847db07c0c4af1a85173bc4009d1219d9)] - type: Added missing types in HttpClientConfig (#4388) (Gcaufy <>) + * [[`064cc7a91`](http://github.com/eggjs/egg/commit/064cc7a91a2be89e39d72a833a1cb35cdcd8f201)] - docs: fixed grammatical and spelling errors (#4424) (Hridayesh Sharma <>) + * [[`95776d646`](http://github.com/eggjs/egg/commit/95776d6462080448e946c049f9ad4da4e9fc065e)] - docs: fix spelling mistakes and grammatical errors (#4423) (Hridayesh Sharma <>) + * [[`50976280f`](http://github.com/eggjs/egg/commit/50976280fcb19ce556ddc46f76da1f5fab46fa4a)] - docs: update compress url (#4415) (忽如寄 <<594613537@qq.com>>) + +## 2020-06-29, Version 2.27.0 @killa + +### Notable Changes + +* **typings** + * fix curl type + * export EggLogger/EggHttpClient/EggContextHttpClient + +* **docs** + * update docs about how to extends ctx.helper + +### Commits + + * [[`b5cc8b6e3`](http://github.com/eggjs/egg/commit/b5cc8b6e361b1ac2e7b4eb509f1e6486fe1dab13)] - fix(dts): fix curl type (#4312) (胡宇航 <<591765099@qq.com>>) + * [[`432128a80`](http://github.com/eggjs/egg/commit/432128a80aefdc8d11a6571da1ff35550e85fd66)] - type: export EggLogger/EggHttpClient/EggContextHttpClient (#4280) (killa <>) + * [[`eca6b04c1`](http://github.com/eggjs/egg/commit/eca6b04c1c50bc69c53f9910cc35bb7441c7cb02)] - docs:update docs about how to extends ctx.helper (#4362) (EasonQwQ <<750225883@qq.com>>) + + +## 2020-05-13, Version 2.26.1 @dead-horse + +### Notable Changes + +* **fixes** + * runInBackground always run after setImmediate + +* **docs** + * imporve docs + * imporve typings + +### Commits + + * [[`9c67298d6`](http://github.com/eggjs/egg/commit/9c67298d69946d4ba0887c3648d3404e835c6902)] - test: run test on node 14 (#4272) (fengmk2 <>) + * [[`427a30a07`](http://github.com/eggjs/egg/commit/427a30a071d248cc2e5e15bb4bad35f3058e867f)] - test: make dnscache test case more stable (#4297) (Yiyu He <>) + * [[`64efd076b`](http://github.com/eggjs/egg/commit/64efd076bf9d937e36748f89bada6fce186913cd)] - fix: runInBackground always run after setImmediate (#4296) (Yiyu He <>) + * [[`69923977a`](http://github.com/eggjs/egg/commit/69923977a7e5c0825f2fc5c616852b0721411a0c)] - docs: Update doc for config of logger.consoleLevel (#4276) (xuxu <>) + * [[`7b6e4371c`](http://github.com/eggjs/egg/commit/7b6e4371c7367583627977a0ca4aa2ff66e28429)] - docs(typescript): Add --noEmit to unittest example code (#4250) (Ink <>) + * [[`3413e35fd`](http://github.com/eggjs/egg/commit/3413e35fd86408896e5c0b7e77ec2528ac08c9f9)] - chore: fix typo (#4234) (zoomdong <<1344492820@qq.com>>) + * [[`b4b9b50af`](http://github.com/eggjs/egg/commit/b4b9b50af1bb842e0138fc236d45882188698123)] - doc (socketio): fix packet middleware bug (#4204) (zfx <<502545703@qq.com>>) + * [[`2fcd605c6`](http://github.com/eggjs/egg/commit/2fcd605c6397480b0a5add9c11f7c7d071393de5)] - docs: update bodyParser types and doc (#4192) (sexy pig <<353071655@qq.com>>) + * [[`5e2bad0c4`](http://github.com/eggjs/egg/commit/5e2bad0c421952b7c84471df06d2ca80c20fa14c)] - docs: fix typo in router (#4203) (Xuemuyang <>) + * [[`1181a675c`](http://github.com/eggjs/egg/commit/1181a675cae08c2d6952b8c15a3a9375947e220b)] - docs(plugin): format en/basics/plugin.md (#4168) (chs97 <<623528324@qq.com>>) + * [[`e9011e8f3`](http://github.com/eggjs/egg/commit/e9011e8f332a97da7e6e112d0f1ded42f0b5db42)] - feat: add http method patch to typings (#4125) (xiaoxu <>) + * [[`2109505b4`](http://github.com/eggjs/egg/commit/2109505b40a159cd047075e566e9465b1ad5a365)] - test: fix doctools path on windows (#4090) (fengmk2 <>) + + +## 2019-12-07, Version 2.26.0 @fengmk2 + +### Notable changes + +* **features** + * add application level Cookie options, can fix [Cookie SameSite warning on Chrome](https://support.google.com/chrome/thread/16654793?hl=en) now. + * use new URL instead of url.parse, avoiding potential security issues. + +### Commits + + * [[`b28134e77`](http://github.com/eggjs/egg/commit/b28134e7709c803eb7a7ed071a25bac8a28e3d1f)] - feat: add application level Cookie options (#4086) (fengmk2 <>) + * [[`b7718c1cc`](http://github.com/eggjs/egg/commit/b7718c1cc2b94b02ee728088060fdbc85e462b6d)] - fix: use new URL instead of url.parse (#4048) (Yiyu He <>) + * [[`afed9105d`](http://github.com/eggjs/egg/commit/afed9105df34aad60c40ecba0e44ddaad1a605dc)] - fix: index.d.ts (#4012) (dxd <>) + * [[`690711bab`](http://github.com/eggjs/egg/commit/690711bab8bd8c838b4dd651baea3bc49a5fa1f1)] - test: fix the testcase error of load_boot.test.js (#4041) (Xin Wang <>) + * [[`6c55a436b`](http://github.com/eggjs/egg/commit/6c55a436bf2afb7bb99810401a6f92b3a58471ff)] - docs: fix typo (#4028) (Xuehua Cai <>) + +## 2019-10-28, Version 2.25.0 @dead-horse + +### Notable changes + +* **features** + * support config.maxIpsCount, deprecate config.maxProxyCount + * singleton returns client name when create client + +### Commits + + * [[`b3479e8e2`](http://github.com/eggjs/egg/commit/b3479e8e2b6cc74c95eef4334bd6b054fddb6158)] - feat: support config.maxIpsCount (#4014) (Yiyu He <>) + * [[`380e7d634`](http://github.com/eggjs/egg/commit/380e7d6344b4f608397fed5dea4f19918ca347f5)] - docs(env): cleanup the document (#3988) (Alpha <>) + * [[`2c5e64a50`](http://github.com/eggjs/egg/commit/2c5e64a50edd78522aa5bf83e4ea8ef4d72f5b40)] - docs: add promo link (#3995) (Haoliang Gao <>) + * [[`adca16637`](http://github.com/eggjs/egg/commit/adca1663712acd361dddd1b390ca48dda5a3608e)] - docs(deployment): update the description for hostname (#3994) (Suyi <>) + * [[`27dacb7c9`](http://github.com/eggjs/egg/commit/27dacb7c9dae4b65c1b973d39aae1f8c9a4cd7b1)] - feat:Singleton returns client name when create client (#3905) (暗色调 <<41288382+dark-tone@users.noreply.github.com>>) + * [[`d3f68c371`](http://github.com/eggjs/egg/commit/d3f68c3710946a63f640b1885e9c7120db935d8e)] - docs(deployment): modify hostname (#3987) (zfx <<502545703@qq.com>>) + * [[`73ad6c086`](http://github.com/eggjs/egg/commit/73ad6c0861d5d6f8235fde2a5ba985ad9f53fcfe)] - chore: use github actions to run CI (#3974) (Suyi <>) + + +## 2019-10-11, Version 2.24.0 @thonatos + +### Notable changes + +* **features** + * feat: set default body-parser limitation to 1mb + +* **fixes** + * app.keys getter must have a setter either + * more log for bodyParser + +* **docs** + * add opencollective to sponsors list + * update lf url + * fix hsts docs error + * fix typo of socket.io + * modify invalid links + +### Commits + + * [[`bddf1e183`](http://github.com/eggjs/egg/commit/bddf1e183b5b6fc0f1414f81948ffdedd71e16a9)] - feat: set default body-parser limitation to 1mb (#3903) (Suyi <>) + * [[`5ddf07c43`](http://github.com/eggjs/egg/commit/5ddf07c435ad81142a6e995583849e20c7348dda)] - docs: update readme (#3968) (Suyi <>) + * [[`be1b72606`](http://github.com/eggjs/egg/commit/be1b72606a62a8efe88849976126cc8ca61b8d7e)] - docs(security): hsts is disabled by default (#3972) (Suyi <>) + * [[`e5e948783`](http://github.com/eggjs/egg/commit/e5e948783a45ea138592a33e9ae7faa20a85af26)] - docs: add opencollective to sponsors list (#3971) (Suyi <>) + * [[`cde921456`](http://github.com/eggjs/egg/commit/cde921456657c16dba81c6fb9c991b62a826d120)] - docs: update lf url (#3922) (Suyi <>) + * [[`17b22c86b`](http://github.com/eggjs/egg/commit/17b22c86b7f83bc168d9c7176191618e19add1dd)] - docs: fix typo of socket.io (#3895) (lqzhgood <<9134671+lqzhgood@users.noreply.github.com>>) + * [[`a9d0cf5c0`](http://github.com/eggjs/egg/commit/a9d0cf5c0d5aadc957e12854f7a3ab6469f83f75)] - fix: app.keys getter must have a setter either (#3891) (Yiyu He <>) + * [[`f1707410b`](http://github.com/eggjs/egg/commit/f1707410bc3f703d69aae27965cc622b9a768107)] - docs(deployment): add logs that the egg app has been successfully connected to alinode (#3868) (hyj1991 <>) + * [[`24c388b4a`](http://github.com/eggjs/egg/commit/24c388b4a5ec4c717875a1510accbd5882800ad0)] - docs(egg-and-koa): modify invalid links (#3851) (sdjdd <<352207572@qq.com>>) + * [[`84894e871`](http://github.com/eggjs/egg/commit/84894e8714747876821447faeaada0dabc2a7147)] - chore: add sponsor config (#3751) (TZ | 天猪 <>) + * [[`064616934`](http://github.com/eggjs/egg/commit/064616934b4ff2c044395baae60bd33d3d7dc5ff)] - chore: add node12 for ci (#3196) (Haoliang Gao <>) + * [[`45a966621`](http://github.com/eggjs/egg/commit/45a966621746f9d2c9e8a91fc1b17f75d97033de)] - docs(quickstart): npm version for npm init command (#3836) (QingDeng <>) + * [[`341beda59`](http://github.com/eggjs/egg/commit/341beda59ee99867998e28603f9ed623e49c33aa)] - test: mv assert to fixtures (#3829) (Suyi <>) + * [[`79dbb14a5`](http://github.com/eggjs/egg/commit/79dbb14a535c27a55daf83b441b47aabff472b06)] - docs(logger): formatter (#3835) (TZ | 天猪 <>) + +## 2019-07-17, Version 2.23.0 @atian25 + +### Notable changes + +* **features** + * error message rewrite when it has only a getter + +* **fixes** + * handleRequest method should return a promise + * more log for bodyParser + +* **docs** + * httpclient upload files + * typings improve + +### Commits + + * [[`6bfc0eb5b`](http://github.com/eggjs/egg/commit/6bfc0eb5b9a6d38c73d46bf641ece6adda3481a1)] - feat: error message rewrite when it has only a getter (#3796) (TZ | 天猪 <>) + * [[`489f52b5c`](http://github.com/eggjs/egg/commit/489f52b5ce4078efccefc8837729b42c15828722)] - fix: handleRequest method should return a promise (#3820) (引证 <>) + * [[`29a2f2fd9`](http://github.com/eggjs/egg/commit/29a2f2fd92e4d3e3cf0ee9ff034d8cdce07ee693)] - fix: more log for bodyParser (#3809) (TZ | 天猪 <>) + * [[`6dc8a2d14`](http://github.com/eggjs/egg/commit/6dc8a2d14582c774479593f002af2f2b96e0ce96)] - chore: fix ci (#3825) (Suyi <>) + * [[`e30511eff`](http://github.com/eggjs/egg/commit/e30511effeb77d954e2c15b684c274b85da2c69b)] - docs: add alinode supported platforms (#3821) (hyj1991 <>) + * [[`c67ca2059`](http://github.com/eggjs/egg/commit/c67ca2059f2c44140e3a5bc46c38c87f52a08172)] - docs: open should come with protocol (#3787) (zhennann <>) + * [[`9adcd40f8`](http://github.com/eggjs/egg/commit/9adcd40f81a22670abf2f5f9167d9c8de438ad34)] - docs(lifecyle): add class export in sample code (#3758) (Kermit Xuan <<33770367+Kermit-Xuan@users.noreply.github.com>>) + * [[`4ca62734d`](http://github.com/eggjs/egg/commit/4ca62734db829cad7e3ea35bc6394de98c9ad160)] - fix: typos (#3768) (Jeff <>) + * [[`b1cb5332d`](http://github.com/eggjs/egg/commit/b1cb5332d433c158f59ab4877f3f6ab07bf9fe79)] - chore: remove @types/urllib (#3732) (TZ | 天猪 <>) + * [[`3de31f541`](http://github.com/eggjs/egg/commit/3de31f5418503f02afde9e92409d5f40e664c46c)] - fix(typings): add custom logger typings (#3697) (吖猩 <>) + * [[`35af6331c`](http://github.com/eggjs/egg/commit/35af6331c97b2e9c9f831ff193709f8fc984f3a9)] - docs: https options en version (#3702) (liulun <>) + * [[`9c23232a4`](http://github.com/eggjs/egg/commit/9c23232a47679bdcb8f071a5cc01f013f443aa05)] - docs(sequelize): replace findById with findByPk (#3700) (Zhao zuoqi <<30346283+Mavericker-1996@users.noreply.github.com>>) + * [[`3fccb4f27`](http://github.com/eggjs/egg/commit/3fccb4f275b2982b974a1a1d99cec32795f4efd3)] - docs: https options (#3701) (liulun <>) + * [[`5b2dbd5b0`](http://github.com/eggjs/egg/commit/5b2dbd5b097d80b0f9150d1a03ec1d9c73af8dec)] - test: fix some test methods failed on windows platform (#3686) (QingDeng <>) + * [[`409990299`](http://github.com/eggjs/egg/commit/409990299fd3afeb35968bc06b02f4b0137718ba)] - fix:add the doc test on windows (#3654) (Maledong <>) + * [[`17fab1c1d`](http://github.com/eggjs/egg/commit/17fab1c1d645076bda76be351fcb3c6f86cea4ca)] - docs: httpclient upload files (#3682) (TZ | 天猪 <>) + * [[`da2d439d6`](http://github.com/eggjs/egg/commit/da2d439d6f79f767055021bf96f7ef73207a751a)] - docs(lifecyle): fix typo (#3681) (+v <>) + +## 2019-04-30, Version 2.22.2 @atian25 + +### Notable changes + +* **fixes** + * optimize declaration of httpclient + +### Commits + + * [[`670ba3475`](http://github.com/eggjs/egg/commit/670ba34751af0b3869dd656064b4587affb888ec)] - fix(typings): optimize declaration of httpclient (#3665) (吖猩 <>) + +## 2019-04-29, Version 2.22.1 @atian25 + +### Notable changes + +* **fixes** + * should restore agent messenger first + +### Commits + + * [[`04adcf93b`](http://github.com/eggjs/egg/commit/04adcf93b8f0a8c48c35015e8d2a279fc7d06b24)] - fix: should restore agent messenger first (#3658) (TZ | 天猪 <>) + * [[`99eb75398`](http://github.com/eggjs/egg/commit/99eb7539850c117d3d8b05f669cae5a9e9269be8)] - docs: fix history time (#3655) (TZ | 天猪 <>) + +## 2019-04-29, Version 2.22.0 @atian25 + +### Notable changes + +* **features** + * switch httpclient to httpclient2 for retry feature + * add BaseHookClass + +* **fixes** + * loadCustomLoader should be run before loadCustomApp + +* **docs** + * d.ts for single mode + +### Commits + + * [[`d3b1cb5d9`](http://github.com/eggjs/egg/commit/d3b1cb5d9d2dd91330778966ba9813f56476a47b)] - fix: loadCustomLoader should be run before loadCustomApp (#3652) (Haoliang Gao <>) + * [[`7cc8aab02`](http://github.com/eggjs/egg/commit/7cc8aab02d89869b4ce460b2fa186aedcf00b64b)] - chore: update packages,remove 'plugin' and validations of doc generation (#3643) (Maledong <>) + * [[`bffb6448f`](http://github.com/eggjs/egg/commit/bffb6448f201ce0d61bd3a32b91f673cf5c074f4)] - docs: fix httpclient proxy (#3638) (TZ | 天猪 <>) + * [[`e7fbd68f3`](http://github.com/eggjs/egg/commit/e7fbd68f32054041b74bc860f11aca05c025c0a9)] - feat: switch httpclient to httpclient2 for retry feature (#3626) (TZ | 天猪 <>) + * [[`8bb7c7e7d`](http://github.com/eggjs/egg/commit/8bb7c7e7d59d6aeca4b2ed1eb580368dcb731a4d)] - feat: add BaseHookClass (#3581) (killa <>) + * [[`459454354`](http://github.com/eggjs/egg/commit/4594543543290a8c714fe3b9047c84578bf2f9a6)] - feat: index.d.ts添加单进程模式 (#3628) (jasine <>) + * [[`4b13a1ffb`](http://github.com/eggjs/egg/commit/4b13a1ffbed0895731bf38f72d5786d4b15f263f)] - chore: fix jsdocs (#3627) (TZ | 天猪 <>) + +## 2019-04-12, Version 2.21.1 @dead-horse + +### Notable changes + +* **fixes** + * Revert "feat: switch httpclient to httpclient2 for retry feature(which is a breaking change) + +### Commits + + * [[`89872a76f`](http://github.com/eggjs/egg/commit/89872a76fc09cefb9ff92221a5c3b9977d206f7c)] - Revert "feat: switch httpclient to httpclient2 for retry feature (#36… (#3622) (Yiyu He <>) + +## 2019-04-11, Version 2.21.0 @dead-horse + +### Notable changes + +* **features** + * support config.maxProxyCount to help get the real client ip + * switch httpclient to httpclient2 for retry feature + +* **docs** + * add how to config egg behind a proxy + * update http_proxy usage + * change `egg-init` to `npm init egg` + +### Commits + + * [[`01b9588a3`](http://github.com/eggjs/egg/commit/01b9588a35ba33a7088e79f6d3e08c713c4de963)] - feat: support config.maxProxyCount to help get the real client ip (#3612) (Yiyu He <>) + * [[`eead31862`](http://github.com/eggjs/egg/commit/eead318625347bb9de8f9d7ffc6fae5ae1b33901)] - feat: switch httpclient to httpclient2 for retry feature (#3606) (TZ | 天猪 <>) + * [[`879fe93a6`](http://github.com/eggjs/egg/commit/879fe93a6dde156101318c766a3c29ca07f1e18d)] - docs: add how to config egg behind a proxy (#3614) (Yiyu He <>) + * [[`2357fbc1e`](http://github.com/eggjs/egg/commit/2357fbc1ee18cf0a8ee8692ed2d62d2224acfe3b)] - docs: remove egg-ts-helper && inspect-brk (#3603) (TZ | 天猪 <>) + * [[`e0a1d8fc6`](http://github.com/eggjs/egg/commit/e0a1d8fc6806acc0a4141bc4cf67149069bfbdf0)] - docs: change egg-init to `npm init egg` (#3588) (TZ | 天猪 <>) + * [[`763923cd7`](http://github.com/eggjs/egg/commit/763923cd76be30496fee9f733db9500c1d8188f2)] - chore: remove unused plugins.puml link (#3579) (TZ | 天猪 <>) + * [[`b1746468d`](http://github.com/eggjs/egg/commit/b1746468dae2d02aeef37f6e8d85414624c79880)] - docs(httpclient): update http_proxy usage (#3569) (TZ | 天猪 <>) + + +## 2019-03-25, Version 2.20.2 @whxaxes + +### Notable changes + +* **fixes** + * onClientError remove content-length header + +* **types** + * add custom loader typing + * import types from egg-core + +### Commits + + * [[`f31cd38aa`](http://github.com/eggjs/egg/commit/f31cd38aa1c1cb58f4fb6b08020b0b49a9b5c1a8)] - fix(types): add custom loader typing (#3533) (吖猩 <>) + * [[`a73cfd067`](http://github.com/eggjs/egg/commit/a73cfd067b48b2c2301e50d5ab431dfecebddef4)] - fix(types): import types from egg-core (#3545) (吖猩 <>) + * [[`04adb930d`](http://github.com/eggjs/egg/commit/04adb930de61f6c3d1b7b9b4e7f49800e3b49602)] - fix: onClientError remove content-length header (#3544) (Yiyu He <>) + +## 2019-03-12, Version 2.20.1 @dead-horse + +### Notable changes + +* **fixes** + * empty querystring must be cached + * add Singleton class declare typings + +### Commits + + * [[`2fc241a86`](http://github.com/eggjs/egg/commit/2fc241a8648d64faab78196ccd0377c781287e5e)] - fix: add Singleton class declare typings (#3522) (mars <>) + * [[`981bad58b`](http://github.com/eggjs/egg/commit/981bad58ba6c4644b8bbbd818a43bf0dd62e206f)] - fix: empty querystring must be cached (#3535) (Yiyu He <>) + + +## 2019-03-07, Version 2.20.0 @popomore + +### Notable changes + +* **features** + * support customLoader + +* **chore** + * fix typo + * fix testcase + +### Commits + + * [[`4cf06da27`](http://github.com/eggjs/egg/commit/4cf06da272a3f71b864efb6780ddfe2e6c1ad37c)] - feat: support customLoader (#3484) (Haoliang Gao <>) + * [[`2f2bd69bb`](http://github.com/eggjs/egg/commit/2f2bd69bb5a5ef5f9d45514c0640f3849bc64293)] - chore:Fix some typos in Chinese and English (#3514) (Maledong <>) + * [[`65bdd158c`](http://github.com/eggjs/egg/commit/65bdd158caf38abfc945de9aad8367a8567b1a18)] - Fix(cluster-client.test.js):Rollback to previous (#3507) (Maledong <>) + +## 2019-02-28, Version 2.19.0 @dead-horse + +### Notable changes + +* **features** + * single mode support ignore warning + +* **fixes** + * fix type defined + +### Commits + + * [[`18efac152`](http://github.com/eggjs/egg/commit/18efac152dd5cf789d1e79b1c1fb1fb4ec2013a1)] - feat: single mode support ignore warning (#3501) (Yiyu He <>) + * [[`f9eea2a4d`](http://github.com/eggjs/egg/commit/f9eea2a4da805a1b2f0e8883860266d68eb432ff)] - fix(types): getFileStream options types (#3500) (kayikay <<469797590@qq.com>>) + +## 2019-02-26, Version 2.18.0 @dead-horse + +### Notable changes + +* **features** + * cluster-client support single process mode + +* **fixes** + * fix type defined + +### Commits + + * [[`db1093128`](http://github.com/eggjs/egg/commit/db10931281dd39106e5c657e358117abd39b2103)] - feat: cluster-client support single cpu mode (#3497) (zōng yǔ <>) + * [[`f7e6ab535`](http://github.com/eggjs/egg/commit/f7e6ab535df378b35dfe6b6b49d7e009dc2bcf3f)] - doc (typescript.md): Chinese translation for the beginning of TypeScript's Introduction (#3488) (Maledong <>) + * [[`ac7e9a6b6`](http://github.com/eggjs/egg/commit/ac7e9a6b6d732d946dc238d9bad3eaabb81a1b70)] - fix: helper type (#3483) (吖猩 <>) + + +## 2019-02-21, Version 2.17.0 @dead-horse + +### Notable changes + +* **features** + * agent context can be extended + +* **fixes** + * createAnonymousContext add host in headers + +### Commits +* [[`7147b23cf`](http://github.com/eggjs/egg/commit/7147b23cf7edaa98a8f009d98de7ef2aaa5303a0)] - feat: agent context can be extended (#3478) (Hongcai Deng <>) +* [[`a2f0d9620`](http://github.com/eggjs/egg/commit/a2f0d96204e05f11c5586ff0fa9441f4e3ab5dff)] - fix: createAnonymousContext add host in headers (#3477) (Yiyu He <>) +* [[`5952d1240`](http://github.com/eggjs/egg/commit/5952d12404ae896a2338ee4ee79d68876ffbb205)] - docs(typescript): fix wrong path of LifeCycle (#3475) (CHANG, TZU-YEN <>) + +## 2019-02-18, Version 2.16.2 @dead-horse + +### Notable changes + +* **fixes** + * fix: messenger in single process mode support send without `to` + +### Commits + + * [[`eac494184`](http://github.com/eggjs/egg/commit/eac4941846948ca6bb8a357d525ad82737425005)] - fix: support send without to argument (#3472) (Yiyu He <>) + + +## 2019-02-15, Version 2.16.1 @atian25 + +### Notable changes + +* **docs** + * remove declaration of view + +* **others** + * update dependencies + +### Commits + + * [[`1e859f2e2`](http://github.com/eggjs/egg/commit/1e859f2e200260cab95ac0b860d85609eb3eec06)] - feat(types): remove declaration of view (#3466) (吖猩 <>) + * [[`4a3ab5ac0`](http://github.com/eggjs/egg/commit/4a3ab5ac0324537fc3cdbcc0e84e3085b8a34586)] - deps: update dependencies (#3464) (Yiyu He <>) + +## 2019-02-14, Version 2.16.0 @dead-horse + +### Notable changes + +* **features** + * allow ctx.router setter + +* **others** + * more document improvement + +### Commits + + * [[`0b67c85f6`](http://github.com/eggjs/egg/commit/0b67c85f6f1798b2d3f377fb5ea336c96b60b6e3)] - feat: allow ctx.router setter (#3460) (fengmk2 <>) + * [[`ae5f56f3e`](http://github.com/eggjs/egg/commit/ae5f56f3e9b60eaa3507db44736020f3a13ec6f1)] - chore: Add principles for English titles and change all English titles (#3444) (Maledong <>) + * [[`a9bee07da`](http://github.com/eggjs/egg/commit/a9bee07daff1530da7350f9ad1ea56e21aa3eead)] - docs(sequelize): fix init doc (#3456) (Yiyu He <>) + * [[`f76c23052`](http://github.com/eggjs/egg/commit/f76c23052c86afcf158087f8b13a7e47ef76f67c)] - docs(logger): add logger.outputJSON to docs (#3425) (FX <>) + + +## 2019-02-04, Version 2.15.1 @dead-horse + +### Notable changes + +* **fixes** + * add missing framework support for single process mode + +### Commits + + * [[`277c024cf`](http://github.com/eggjs/egg/commit/277c024cf565948547dbc7a518d39d7f55318f58)] - fix: add missing framework support for single process mode (#3445) (Yiyu He <>) + +## 2019-02-03, Version 2.15.0 @dead-horse + +### Notable changes + +* **features** + * [EXPERIMENT FEATURE] support single process mode + +* **fixes** + * [TYPE] array supporting for config.static.dir + * [TYPE] fix IMiddleware type is incompatible + * [TYPE] fix type error while esModuleInterop is true + +* **others** + * more document improvement + +### Commits + + * [[`83c423a0a`](http://github.com/eggjs/egg/commit/83c423a0a985e701bfaef7f10372268b4ce8cef5)] - docs(development.md): Add English translation (Jennie <>) + * [[`d79da17bd`](http://github.com/eggjs/egg/commit/d79da17bdbe94f7b78d923caa10abf21e6c5f752)] - fix: type error while esModuleInterop is true (#3436) (吖猩 <>) + * [[`20ba4632b`](http://github.com/eggjs/egg/commit/20ba4632ba32e3b81e760678b4bbe00cdf05388e)] - feat: support single process mode (#3430) (Yiyu He <>) + * [[`133616961`](http://github.com/eggjs/egg/commit/133616961e5a2e95d5e2cd1254d7304c846b859c)] - docs: fix typo in socketio.md (#3431) (kilmas <>) + * [[`e899630e9`](http://github.com/eggjs/egg/commit/e899630e97865701b81d428686a19288b1c87b98)] - fix: array supporting for config.static.dir (#3421) (Gray <>) + * [[`43f2e3c44`](http://github.com/eggjs/egg/commit/43f2e3c449a7b448506d2484ed729618b06bffec)] - fix: IMiddleware type is incompatible (#3419) (吖猩 <>) + * [[`b3256b54e`](http://github.com/eggjs/egg/commit/b3256b54eeb09b2cee3cfdb75e98d6c090844a10)] - doc:Add new loaderUpdate.md (#3395) (Maledong <>) + * [[`71768002a`](http://github.com/eggjs/egg/commit/71768002a468a8fd30b9516c0bed85fa27b99b0a)] - docs: Wrong words are corrected (#3418) (巧克力冰激凌 <<121017405@qq.com>>) + * [[`20d56c7a8`](http://github.com/eggjs/egg/commit/20d56c7a83a74bb81ee47b0b8c2785db15519996)] - fix: fix ts ci (#3416) (吖猩 <>) + * [[`8beacd13e`](http://github.com/eggjs/egg/commit/8beacd13e3bcfff6b6a1e02eea72cafdd343858c)] - docs(logger): add logger.disableConsoleAfterReady to docs (#3384) (吖猩 <>) + * [[`271bc6372`](http://github.com/eggjs/egg/commit/271bc63722723531556bbec06d621f964ad1db33)] - chore: typo "submit an PR" should be "submit a PR" (#3408) (DAI JIE <>) + * [[`688f67c9f`](http://github.com/eggjs/egg/commit/688f67c9f329c71ea4469b9d28d5ee41815831ed)] - Chore: Fix some chore issues (#3400) (Maledong <>) + * [[`cfcebc623`](http://github.com/eggjs/egg/commit/cfcebc6234c62780c6aecf84db3862efd74e430c)] - doc (typescript.md): Sync the English translation (#3397) (Maledong <>) + * [[`7e5ef2181`](http://github.com/eggjs/egg/commit/7e5ef21811f98e5d55884f2574092e6f2e7b619b)] - docs(typescript): optimize docs of typescript (#3374) (吖猩 <>) + * [[`2a801f789`](http://github.com/eggjs/egg/commit/2a801f789f6e60427657b507951de8fc8e4a830f)] - chore: comments typo fix (#3392) (Jeff <>) + * [[`9a4b72062`](http://github.com/eggjs/egg/commit/9a4b7206212a27d37a37a1d68b4739be306b1a7a)] - chore: fix issue template (#3369) (Suyi <>) + * [[`ef73396a5`](http://github.com/eggjs/egg/commit/ef73396a5828c6b4e55c85cd2b27c3830bd306e5)] - docs: improve debug docs (#3370) (TZ | 天猪 <>) + * [[`874e57fda`](http://github.com/eggjs/egg/commit/874e57fda480d3295c1b1b30198ca3493f57814d)] - docs(sequelize): fix init (#3372) (Yiyu He <>) + * [[`b2152c56f`](http://github.com/eggjs/egg/commit/b2152c56f525a87fe0c1cfe66949005f308e1569)] - Chore: Fix some typo translations (#3361) (Maledong <>) + * [[`d275929d1`](http://github.com/eggjs/egg/commit/d275929d17830c95a2c828611b5ca54ffb747270)] - docs(boot): update app start document (#3348) (Yiyu He <>) + * [[`9a8652beb`](http://github.com/eggjs/egg/commit/9a8652bebc29f3d097039196b10daa2575f49695)] - Fix: Change the diagram of "starting process" (#3358) (Maledong <>) + * [[`ac0f13bc6`](http://github.com/eggjs/egg/commit/ac0f13bc604ebfc08185b336a106ec7e5d1bc98f)] - Chore: Add missing links for "Sails" and union the spellings of "Plugin" (#3356) (Maledong <>) + * [[`cd52b063b`](http://github.com/eggjs/egg/commit/cd52b063b60e15bc78c440fdebc00ddd3dca9909)] - docs(cluster-and-ipc.md): fix typos and formatting errors (#3357) (Darren Poon <>) + * [[`37e3c1aba`](http://github.com/eggjs/egg/commit/37e3c1abab31f47fc492574464657a57ff686b2b)] - Chroe: Fix something in articles (#3349) (Maledong <>) + +## 2018-12-20, Version 2.14.2 @atian25 + +### Notable changes + +* **fixes** + * fix d.ts context declaration not works + +* **docs** + * more document improvement + +### Commits + * [[`edfe66093`](http://github.com/eggjs/egg/commit/edfe66093c9ffe730ffd9804da1e2b264a48c38e)] - fix: Add comments for re-writing properties from Koa (#3332) (Maledong <>) + * [[`f312db78f`](http://github.com/eggjs/egg/commit/f312db78fc330da2bfe6efdb0f095d7b3b363beb)] - fix: fix context declaration not works (#3329) (Axes <>) + * [[`ef47a2746`](http://github.com/eggjs/egg/commit/ef47a274625a6ae8696857bef01b2c679dd65395)] - docs: fix config heading level (#3327) (Suyi <>) + * [[`cddd91ded`](http://github.com/eggjs/egg/commit/cddd91ded2fac1395487e2847caaf92afaafcf8e)] - chore: adjust template (TZ <>) + * [[`7319727a0`](http://github.com/eggjs/egg/commit/7319727a0b8a4fa210746ac201631a4b7db4359b)] - chore: Update issue templates (#3326) (TZ | 天猪 <>) + * [[`0cb246e26`](http://github.com/eggjs/egg/commit/0cb246e2663a288395a013ee78a1b34ab5b7c641)] - doc: Fix some translations with some icons (#3315) (Maledong <>) + * [[`9dc20377e`](http://github.com/eggjs/egg/commit/9dc20377e18e53278bcfac037e15a5a761cccdb3)] - doc: session special usage tip (#3304) (Jerry Wu <>) + * [[`6f4e91274`](http://github.com/eggjs/egg/commit/6f4e91274daa0685ba0ed8983ad5b6fd457322bc)] - docs: Update httpclient.md (#3276) (Albert <>) + * [[`64e88abfd`](http://github.com/eggjs/egg/commit/64e88abfd24d50096aa2d4ef442aafd46101429a)] - docs(egg-passport): add redirection desc while auth succeed (#3260) (Suyi <>) + +## 2018-11-24, Version 2.14.1 @atian25 + +### Notable changes + +* **fixes** + * remove timeout log msg + +* **others** + * use circular-json-for-egg to remove deprecate message + +### Commits + + * [[`0fb5a96c0`](http://github.com/eggjs/egg/commit/0fb5a96c023e916cb9c14c5960df62547ed391d8)] - fix: remove timeout log msg (#3229) (TZ | 天猪 <>) + * [[`de81caef1`](http://github.com/eggjs/egg/commit/de81caef1d91c229effadd25ddf752297c2a08f5)] - deps: use circular-json-for-egg to remove deprecate message (#3211) (Yiyu He <>) + +## 2018-11-17, Version 2.14.0 @dead-horse + +### Notable changes + +* **features** + * add create anonymous context to agent + * support server timeout + +* **fixes** + * curl: allow request timeout bigger than agent timeout + * triggerServerDidReady should be triggered only once + +### Commits + + * [[`db999d3f7`](http://github.com/eggjs/egg/commit/db999d3f7afa210c855f3f1a4518e83f7d8c1dc6)] - docs: add serverTimeout to d.ts (#3200) (TZ | 天猪 <>) + * [[`a43fef4e1`](http://github.com/eggjs/egg/commit/a43fef4e1828937d3d84469989582df273de1493)] - docs(index.d.ts): curl 增加泛型 (#3197) (The Rock <>) + * [[`d40124a25`](http://github.com/eggjs/egg/commit/d40124a25fcd1b52ab862ee297139a022458b81d)] - feat: add create anonymous context to agent (#3193) (Hongcai Deng <>) + * [[`9dfd19ead`](http://github.com/eggjs/egg/commit/9dfd19eada8bae7be212155a2989d0ccc410e8eb)] - fix: triggerServerDidReady should be triggered only once (#3190) (killa <>) + * [[`7802528e1`](http://github.com/eggjs/egg/commit/7802528e122691eb2cb78174e1c1490d0a382c08)] - feat: support server timeout (#3133) (TZ | +天猪 <>) + * [[`ff79101b5`](http://github.com/eggjs/egg/commit/ff79101b592d59ad12d110dd26dd7fa3d044b968)] - docs: Update service.md (#3191) (肖金 <>) + * [[`327fa174f`](http://github.com/eggjs/egg/commit/327fa174ffd74a67a77520d839eac282c916e8c0)] - fix: allow request timeout bigger than agent timeout (#3146) (fengmk2 <>) + * [[`86093c03a`](http://github.com/eggjs/egg/commit/86093c03a822a2b925de94cfda96198dc8159ade)] - docs: remove promo logo (#3176) (Suyi <>) + +## 2018-11-07, Version 2.13.0 @mansonchor + +### Notable changes + +* **feature** + * emit event when runInBackground catch error + +* **perf** + * better TypeScript support + +* **docs** + * supplement documentation + + +### Commits + + * [[`03378b8c3`](https://github.com/mansonchor/egg.git/commit/03378b8c3e48e7000a580a4acf5375f9ffcac4dc)] - docs(plugin.md): fix 'path' declaration example (#3152) (maigozhang <>) + * [[`3c25221bd`](https://github.com/mansonchor/egg.git/commit/3c25221bd24a0a39cd06540fad46884e4dda363c)] - chore: use is.string() in utils.js for consistency (#3153) (ZYSzys <>) + * [[`a9b0fcec6`](https://github.com/mansonchor/egg.git/commit/a9b0fcec636f7241d0f578dca6b99444dabfeb83)] - chore(typings): add method `beforeClose` in index.d.ts (#3120) (Erona <>) + * [[`4709db746`](https://github.com/mansonchor/egg.git/commit/4709db746d8f97de99c04558f1ba86443e394668)] - feature(context): emit event when runInBackground catch error (#3118) (mansonchor <>) + * [[`e1dc2a7a4`](https://github.com/mansonchor/egg.git/commit/e1dc2a7a409a8bf56a817773229d5fc6dcde796b)] - docs: add promo logo (#3113) (Haoliang Gao <>) + * [[`51e9c1578`](https://github.com/mansonchor/egg.git/commit/51e9c1578496ed2afb44c47fdfd77867a95fec52)] - chore(typings): add interface IBoot (#3098) (killa <>) + * [[`8052d7ff7`](https://github.com/mansonchor/egg.git/commit/8052d7ff7bf6278e8ce4b4de46e0e6324d0d3861)] - doc: Update the `configWillLoad` explainations (#3116) (Maledong <>) + * [[`c3c4e2e3e`](https://github.com/mansonchor/egg.git/commit/c3c4e2e3e04a924595d6837ab15c7b292e3529f6)] - docs: add configWillLoad to lifecycle (#3101) (fengmk2 <>) + * [[`4abdb4980`](https://github.com/mansonchor/egg.git/commit/4abdb49801ebb3075b408d6fb3b72eba1a70c056)] - docs(CONTRIBUTION): Add missing link for `Accquire the submitted files` (#3102) (Maledong <>) + * [[`c7061ec62`](https://github.com/mansonchor/egg.git/commit/c7061ec6255faa250c6565a4c102f64c0498683c)] - fix(docs): Grammar of "lots of" (#3100) (waiting <>) + * [[`92181e83f`](https://github.com/mansonchor/egg.git/commit/92181e83f98d55bd1a796a871ab5d438d02c8e84)] - doc (CONTRIBUTION): Add missing English translations and clearify dns (#3035) (Maledong <>) + * [[`0a7497987`](https://github.com/mansonchor/egg.git/commit/0a7497987067ce1c3376dfc30676c35f484a5ccc)] - doc(logger.md): Fix incorrect description on default log output level. (#3082) (TX-Kunkun <>) + + +## 2018-10-08, Version 2.12.0 @dead-horse + +### Notable changes + +* **feature** + * add Subscription base class on app instance + +* **fix** + * upgrade to egg-logger@2, don't write log when stream was destroyed. + * pin circular-json@0.5.5 to avoid output deprecate message + +* **docs** + * corrected lots of documentation errors, thanks @Maledong + * use egg-logger definition + + +### Commits + + * [[`eb1eae736`](http://github.com/eggjs/egg/commit/eb1eae736c0fc541e6d21fb726d52d971d6a95da)] - refactor(typescript): use egg-logger definition (#3078) (Haoliang Gao <>) + * [[`04d9a3b85`](http://github.com/eggjs/egg/commit/04d9a3b85ef54819c0ad3ac505e7806db6a7e9b3)] - deps: egg-logger@2 (#3073) (Yiyu He <>) + * [[`886d9ad8f`](http://github.com/eggjs/egg/commit/886d9ad8fd11e1fbbd1712dd53ef464658f525b5)] - feat: add Subscription base class on app instance (#3058) (fengmk2 <>) + * [[`4c6fb2a17`](http://github.com/eggjs/egg/commit/4c6fb2a175c2481aa61aaad131f6812517bc7022)] - doc (socket.io): Make 'uws' cannot use anymore clear (#3068) (Maledong <>) + * [[`0d6798d22`](http://github.com/eggjs/egg/commit/0d6798d22d0e5016b0f7f25e5fa15ffe6900e16c)] - docs (Controller.md): Add new feat description (#3066) (Maledong <>) + * [[`399902680`](http://github.com/eggjs/egg/commit/39990268081d1da3fdb2d575802ea46cdf67bcd5)] - doc(typescript.md): Clarify the middleware's usages (#3039) (Maledong <>) + * [[`6bf812f73`](http://github.com/eggjs/egg/commit/6bf812f73603e967e405a815dcb2cc94dcb8384c)] - chore: fix middleware docs typo (#3060) (TZ | 天猪 <>) + * [[`b13d904d3`](http://github.com/eggjs/egg/commit/b13d904d302639c3b6068f109d4bcfa5aff12c61)] - test: avoid DNS pollution on local env (#3034) (fengmk2 <>) + * [[`bace2433b`](http://github.com/eggjs/egg/commit/bace2433bcc96d1507403c34711b2a2e450e6a6a)] - fix: remove loader.loadBootHook (Yiyu He <>) + * [[`6a7db2a35`](http://github.com/eggjs/egg/commit/6a7db2a3591a03b187bc3b52c67b37fde7984d34)] - doc (objects.md): Fix number and code errors (#3029) (Maledong <>) + * [[`c65a64899`](http://github.com/eggjs/egg/commit/c65a648991900b95a8ef0b21dcd8e3f715523df7)] - doc (TypeScript): Formation errors with missing translations (#3020) (Maledong <>) + * [[`abd8d1286`](http://github.com/eggjs/egg/commit/abd8d12861e17ff8fe5e950d589c00d17625beae)] - deps: pin circular-json@0.5.5 to avoid output deprecate message (#3023) (Yiyu He <>) + * [[`e3ffcbe64`](http://github.com/eggjs/egg/commit/e3ffcbe6449b95b50ea40583a28383e734c72fe1)] - docs (typescript.md): Add missing trans in English for TypeScript (#2998) (Maledong <>) + +## 2018-09-19, Version 2.11.2 @XadillaX + +### Notable changes + +* **fix** + * typescript: add missing 'ignore', 'match' +* **refactor** + * separate dumping config object and config file + +### Commits + + * [[`1d30166e0`](http://github.com/eggjs/egg/commit/1d30166e037e8890fc850e51bdba02af76772485)] - refactor: separate dumping config object and config file (#3014) (Khaidi Chu <>) + * [[`e3f183e96`](http://github.com/eggjs/egg/commit/e3f183e9658e603c74850376f2257bd88bc4a043)] - fix (typescript): Add missing 'ignore','match' (#3010) (Maledong <>) + +## 2018-09-14, Version 2.11.1 @popomore + +### Notable changes + +* **fix** + * httpclient: can't use runInBackground in agent + +* **deps** + * upgrade to debug@4 and coffee@5 + +### Commits + + * [[`eed74e861`](http://github.com/eggjs/egg/commit/eed74e8610e1ea189beed1c3526b38f0b59c48ab)] - chore: update deps, debug@4 and coffee@5 (#2995) (TZ | 天猪 <>) + * [[`a8a3dfb04`](http://github.com/eggjs/egg/commit/a8a3dfb04f11b1c48ed1f01154e4d4311bfafa4b)] - fix(httpclient): can't use runInBackground in agent (#3003) (Haoliang Gao <>) + * [[`4faf68f4b`](http://github.com/eggjs/egg/commit/4faf68f4b6dad160a151b3d76041a0521261b530)] - doc (loader.md): Add missing English translations (#2996) (Maledong <>) + +## 2018-09-11, Version 2.11.0 @atian25 + +### Notable changes + +* **feature** + * support boot lifecycle, see https://github.com/eggjs/egg/issues/2520 + * `dnshttpclient` now use async function instead of Promise + +* **fix** + * don't log when rawPacket is empty + +* **docs** + * add sequelize guide docs + * more document and typings improvement + +### Commits + + * [[`0d876c71a`](http://github.com/eggjs/egg/commit/0d876c71a9c4862b93cb039f564ae3d3171e1cad)] - feat: support boot lifecyle (#2972) (killa <>) + * [[`b02ce1547`](http://github.com/eggjs/egg/commit/b02ce154777fc78a6d344fe45d915d013096bea3)] - chroe(doc): Fix some typos (#2988) (Maledong <>) + * [[`688067ae0`](http://github.com/eggjs/egg/commit/688067ae09071316cbf5310b17d92d4fec39b42a)] - docs: fix 2 typos (#2982) (Jeff <>) + * [[`a719fd345`](http://github.com/eggjs/egg/commit/a719fd34507ebbfcf800768fb58adc81aa9c5e36)] - docs: Fix and add missing typos (#2935) (Maledong <>) + * [[`815c27879`](http://github.com/eggjs/egg/commit/815c278792e94ccf85822b3496f10396236e6628)] - fix (typings): Upgrade to the latest version of 'egg-cookie' to fetch (#2958) (Maledong <>) + * [[`a2df5ad13`](http://github.com/eggjs/egg/commit/a2df5ad137dea5faf7724d480edcc482a1df9393)] - docs: fixed typo. (#2961) (Ariel Yang <>) + * [[`b971e6633`](http://github.com/eggjs/egg/commit/b971e66336af4c8e241303866c8fa9acaaf4e66f)] - test: fix sitefile icon test (#2940) (Yiyu He <>) + * [[`81826ed1a`](http://github.com/eggjs/egg/commit/81826ed1a826a3436f8c42b7b5466295c60f241e)] - docs: fix link to angular commit-message-format (#2939) (Vincent <>) + * [[`45e302459`](http://github.com/eggjs/egg/commit/45e30245952619e4ed95867b2f76b0bdd06e94cc)] - fix: don't log when rawPacket is empty (#2924) (Haoliang Gao <>) + * [[`db1286de7`](http://github.com/eggjs/egg/commit/db1286de73de2cd987dc8f28c0616e9a683824a6)] - chore(typings): add class EggLoader (#2321) (waiting <>) + * [[`80528ccec`](http://github.com/eggjs/egg/commit/80528cceced500b5ae49ebf6d9df242ba2ce5ea4)] - refactor(dnshttpclient): use async function instead of Promise (#2774) (Haoliang Gao <>) + * [[`fe9e95654`](http://github.com/eggjs/egg/commit/fe9e9565472c7de9ff8dfb8894917764fe26fa0b)] - doc (package.json,README.zh-CN): Fix some typos (#2927) (Maledong <>) + * [[`289e96278`](http://github.com/eggjs/egg/commit/289e96278359a1468e366a6f3f7b2094dd3b7d6c)] - docs(sequelize): hostname shoule be host (#2921) (Will <<1078954008@qq.com>>) + * [[`72cd808b8`](http://github.com/eggjs/egg/commit/72cd808b86f37847cf340d88ec4eb73b9d7a7aa0)] - docs: fix sequelize link (#2909) (Yiyu He <>) + * [[`ae9ec30b4`](http://github.com/eggjs/egg/commit/ae9ec30b410bba3f8a99ba741e59fdb13e51c806)] - docs: add sequelize (#2902) (Yiyu He <>) + * [[`68135608b`](http://github.com/eggjs/egg/commit/68135608b3518b7d3dbd852453061179f63d5e4f)] - docs(deployment): fix typo on grep (#2898) (Baffin Lee <>) + * [[`6bfe70b3d`](http://github.com/eggjs/egg/commit/6bfe70b3d64206c85dea19c308abb40c46c6e347)] - doc (en,zh-cn): Fix translations error (#2885) (Maledong <>) + * [[`96ed020ce`](http://github.com/eggjs/egg/commit/96ed020ce04919049181808cee217029586d11c3)] - docs: fix config and socketio error (#2884) (Suyi <>) + + +## 2018-08-06, Version 2.10.0 @fengmk2 + +### Notable changes + +* **feature** + * allow runInBackground reuse on plugins + * use Math.floor instead of parseInt + +* **fix** + * use cache-content-type + +* **docs** + * add lifecycle doc + * add sequelize guide + * add allowDebugAtProd in document + * egg-scripts support windows + * schedule add env description + * more document and typings improvement + +### Commits + + * [[`ff7431d5c`](http://github.com/eggjs/egg/commit/ff7431d5c4ea1e1d40fd7e3656dc5ab52ca55726)] - feat: allow runInBackground reuse on plugins (#2872) (fengmk2 <>) + * [[`422b342b1`](http://github.com/eggjs/egg/commit/422b342b1fa419db145323927f4f2d2a8996b7fb)] - feat: Update index.d.ts (#2853) (Ben <>) + * [[`2ca8f0184`](http://github.com/eggjs/egg/commit/2ca8f018473274fa544234c91fc608fa9bf09032)] - feat(typings): define Messenger['on'] and Messenger['once'] (#2763) (waiting <>) + * [[`9f8926d7c`](http://github.com/eggjs/egg/commit/9f8926d7cc55ae103b6a37751538870cc70aa12d)] - fix: use cache-content-type (#2793) (Yiyu He <>) + * [[`033fe0ce1`](http://github.com/eggjs/egg/commit/033fe0ce1d39bd63346de1ec60c97b159be867aa)] - docs: optimize egg-validate usage (#2852) (Sean Zou <<405495715@qq.com>>) + * [[`c0b0bb834`](http://github.com/eggjs/egg/commit/c0b0bb8345df83bbd2949b0af34bb397b5185e17)] - docs(session): fix bug in example code of modify session value (#2824) (Baffin Lee <>) + * [[`b55b303ed`](http://github.com/eggjs/egg/commit/b55b303eddbaf545cdb06fd81df624fd3070110a)] - test: test on travis with node 10 (#2461) (Yiyu He <>) + * [[`38a472f24`](http://github.com/eggjs/egg/commit/38a472f24cf68acb9c64fafa2e4374115d578220)] - docs: add allowDebugAtProd in document (#2803) (Yiyu He <>) + * [[`e86669937`](http://github.com/eggjs/egg/commit/e866699379bc570f8bfcef9a090f1bdb5cddee32)] - perf: use Math.floor instead of parseInt (Eason <>) + * [[`67d538e0e`](http://github.com/eggjs/egg/commit/67d538e0e175e96fe2a64b9c8d17b063537236f7)] - docs(plugin): add details for plugin.js (#2780) (TZ | 天猪 <>) + * [[`8d0b29cc9`](http://github.com/eggjs/egg/commit/8d0b29cc9b0a3b8eefad1ab64a05478c81709144)] - docs(deployment): egg-scripts support windows (#2788) (Baffin Lee <>) + * [[`aaf8faf4f`](http://github.com/eggjs/egg/commit/aaf8faf4fd8d813c7938baa1533d768b9d205fc7)] - test: skip test (#2773) (Haoliang Gao <>) + * [[`eb70335bd`](http://github.com/eggjs/egg/commit/eb70335bd61b6887ffeb33f103340b89c857312a)] - docs(schedule): add env description (#2753) (TZ | 天猪 <>) + * [[`ef20ff756`](http://github.com/eggjs/egg/commit/ef20ff75633b6e83b115d32af603d0f4f34cb1e1)] - docs: add http://www.sofastack.tech (#2752) (Haoliang Gao <>) + * [[`1ecb521c5`](http://github.com/eggjs/egg/commit/1ecb521c50b6238397f9a0b628448c0d2b5ec4fa)] - doc: add lifecyle doc (#2708) (killa <>) + * [[`7930f0419`](http://github.com/eggjs/egg/commit/7930f0419fee741bcf6de73693bcdf1e9986f31e)] - docs: fix ws engine error (#2717) (Suyi <>) + +## 2018-06-14, Version 2.9.1 @dead-horse + +### Notable changes + +* **perf** + * improve set type performance + +* **docs** + * fix socketio's browser demo + * add Messenger in tsd + +### Commits + + * [[`1a820bd44`](http://github.com/eggjs/egg/commit/1a820bd4408b36cf3e48eda62f392006081c17a3)] - perf: improve set type performance by lru cache (#2697) (fengmk2 <>) + * [[`239ce03ef`](http://github.com/eggjs/egg/commit/239ce03efaf60d3d961ced29cf4bf95e44bde2db)] - docs: fix socketio's browser demo (#2645) (xcold <>) + * [[`73ca1b7a3`](http://github.com/eggjs/egg/commit/73ca1b7a3ef6546d4f8a3d227055121a93b80188)] - chore(typings): add Messenger (#2688) (waiting <>) + +## 2018-06-01, Version 2.9.0 @popomore + +### Notable changes + +* **feature** + * dump timing data for loader + +* **fix** + * the default value of config.allowDebugAtProd is false + * make definition of app.locals and ctx.locals definitions merge available + * add key any to Context in typescript define + +* **docs** + * more document improvement + +### Commits + + * [[`e5737d545`](http://github.com/eggjs/egg/commit/e5737d5455d536b908f37ba367446e511f30e663)] - fix: add key any to Context (#2650) (Axes <>) + * [[`65a43aa9e`](http://github.com/eggjs/egg/commit/65a43aa9e47ad2f799e328e4e0ab91a63669c5e3)] - feat: dump timing data for loader (#2521) (#2621) (Haoliang Gao <>) + * [[`48c6d3c9d`](http://github.com/eggjs/egg/commit/48c6d3c9d524e1cbba3e301e6613436741696cc0)] - fix: typo (#2615) (Yanan Che <>) + * [[`c91e67cc0`](http://github.com/eggjs/egg/commit/c91e67cc0246a22efee126ebd01866b05b8312dc)] - docs(logger): the unit of maxFileSize should be byte (#2575) (Haoliang Gao <>) + * [[`26c274174`](http://github.com/eggjs/egg/commit/26c274174c9a48ef1636933fbb2be9777d38f522)] - docs: tweek doc style (#2613) (Haoliang Gao <>) + * [[`3ee7fcf12`](http://github.com/eggjs/egg/commit/3ee7fcf1291a4968197fab4648e951176dfa2714)] - docs: fix quickstart typo error (#2578) (Zhuxy <>) + * [[`8b7c8bd35`](http://github.com/eggjs/egg/commit/8b7c8bd35f8695f4459cfd623f9961c276e0d5a6)] - docs(d.ts): add property of EggAppConfig.development (#2561) (SinaVee <>) + * [[`16a61231d`](http://github.com/eggjs/egg/commit/16a61231d12c91ae609e68509d29aac669e1b83c)] - docs: add d.ts for bodyparser (#2548) (wangtao0101 <>) + * [[`e7696a7d2`](http://github.com/eggjs/egg/commit/e7696a7d2b4bc8eb6fb984aaaa0e0f2422d1c048)] - fix(d.ts): make app.locals and ctx.locals definitions merging available (#2546) (Tony Hawking <>) + * [[`e5d47524e`](http://github.com/eggjs/egg/commit/e5d47524ef96138172c86e774014a6b26d5cac09)] - chroe: Correct an error syntax of English (#2544) (DongWei <>) + * [[`c0f4bd12d`](http://github.com/eggjs/egg/commit/c0f4bd12d422554351b1d1e9866a7b9bbc444e76)] - fix: config.allowDebugAtProd default to false (ZhangJan <>) + * [[`0723cd230`](http://github.com/eggjs/egg/commit/0723cd230514b623c4454120dae988fd5a68ec44)] - docs(cookie): how to get frontend cookie (#2542) (Yiyu He <>) + * [[`9fea64ee9`](http://github.com/eggjs/egg/commit/9fea64ee993de7c3ee2e239d7bba91f5f3b3408a)] - docs: Fix an error link, change a comment into English (#2535) (DongWei <>) + * [[`e96ddb6a8`](http://github.com/eggjs/egg/commit/e96ddb6a884ef767c8653242c666dcc7381222b7)] - docs: Modifications of comments and full translations (DongWei <>) + +## 2018-05-05, Version 2.8.1 @atian25 + +### Notable changes + +* **docs** + * fix missing d.ts + +### Commits + + * [[`20356bffc`](http://github.com/eggjs/egg/commit/20356bffcf7e99970b44f230a6fc2a8f9547a380)] - feat(d.ts): add createAnonymousContext & runInBackground (#2501) (Hengfei Zhuang <>) + * [[`c013ef3e6`](http://github.com/eggjs/egg/commit/c013ef3e64e049c6ef48e29d289f6d756b6ca1f7)] - feat(d.ts): add runSchedule & Subscription define (#2504) (Hengfei Zhuang <>) + +## 2018-05-03, Version 2.8.0 @dead-horse + +### Notable changes + +* **feature** + * add time duration for dump config + +* **fix** + * make singleton work for unextensible or frozen instance + +* **docs** + * switch to English document + * add middleware to Application and other ts improvement (typescript) + * update wxapp-socket-io project to weapp.socket.io + * update title and remove unused files + +### Commits + + * [[`4b602d037`](http://github.com/eggjs/egg/commit/4b602d037554b72c8261b7abb7efd94f8f59f3fe)] - fix: make singleton work for unextensible or frozen instance (#2472) (Yiyu He <>) + * [[`824200c11`](http://github.com/eggjs/egg/commit/824200c11cac8e20b2c275daa7f5a4a365c71259)] - feat: add time duration for dump config (#2485) (Haoliang Gao <>) + * [[`73dac083d`](http://github.com/eggjs/egg/commit/73dac083d2a029f893e9b6737080c921027e308f)] - docs: update wxapp-socket-io project to weapp.socket.io (#2421) (liuguili <>) + * [[`1ada8e384`](http://github.com/eggjs/egg/commit/1ada8e3848be9f09680d7cac091fb14206df5a11)] - feat(d.ts): add middleware to Application and other ts improvement (#2465) (Axes <>) + * [[`437785315`](http://github.com/eggjs/egg/commit/437785315f28a828ea0cf7bece80223d5b796dc5)] - docs: fix the code error of LOCALS in view.md (#2464) (zjz19901029 <<346663801@qq.com>>) + * [[`f341b9fb8`](http://github.com/eggjs/egg/commit/f341b9fb8bdf36b6280500578e8448c59aec10f1)] - chore: update title and remove unused files (#2433) (TZ | +天猪 <>) + * [[`a5ab29cbd`](http://github.com/eggjs/egg/commit/a5ab29cbd1de0f5425019085258a496b4bce8b45)] - docs: switch to English document (#2426) (Haoliang Gao <>) + * [[`4ab7df25f`](http://github.com/eggjs/egg/commit/4ab7df25f152609d494745eac2794b78e66444f0)] - deps: update dependencies, add @types/urllib to autod config (#2423) (Yiyu He <>) + +## 2018-04-17, Version 2.7.1 @dead-horse + +### Notable changes + +* **fix** + * imporve compatibility of singleton + +### Commits + + * [[`e4d219f`](http://github.com/eggjs/egg/commit/e4d219f1aaecbca13601c7813e57c67934e8c32b)] - fix: imporve compatibility of singleton (#2410) (Yiyu He <>) + +## 2018-04-16, Version 2.7.0 @dead-horse [DEPRECATED] + +### Notable changes + +* **feature** + * singleton support asynchronous create function + +* **fix** + * dump config support circular json + +* **docs** + * improve router and typescript + +### Commits + + * [[`3d499a9`](http://github.com/eggjs/egg/commit/3d499a90bab7095569e115e223de40e63812f2f5)] - docs(plugin): add singleton support async create function (#2392) (Yiyu He <>) + * [[`05d925f`](http://github.com/eggjs/egg/commit/05d925fea4e0b2d8efa48cb01ced2133c0c059cd)] - docs: change English document on Readme (#2397) (Haoliang Gao <>) + * [[`590bd8c`](http://github.com/eggjs/egg/commit/590bd8cb400845706ec7cc84232b812cb468c8ac)] - fix: dumpConfig support circular json (#2394) (Yiyu He <>) + * [[`3a489b6`](http://github.com/eggjs/egg/commit/3a489b6f47b39ff2ec31efe936504918300b3f08)] - feat(singleton): support async create function (#2382) (Yiyu He <>) + * [[`a5b6731`](http://github.com/eggjs/egg/commit/a5b673133b35e9b005e19c1e3267a2ff3d58e32b)] - docs: chore for router and typescript (#2390) (TZ | 天猪 <>) + * [[`ee2d2b3`](http://github.com/eggjs/egg/commit/ee2d2b3c33671a822b45a6c474d3710aab5e70d5)] - docs(passport): translation for passport tutorial (#2235) (Cemre Mengu <>) + * [[`6fad4e1`](http://github.com/eggjs/egg/commit/6fad4e1bed3c388e964fc656244e5e606b258085)] - chore: update package.json for release (#2381) (TZ | 天猪 <>) + +## 2018-04-12, Version 2.6.1 @atian25 + +### Notable changes + +* **docs** + * TypeScript Guide (#2324) + * fix d.ts with ts support + * docs improve + +### Commits + + * [[`2998bf733`](http://github.com/eggjs/egg/commit/2998bf733268d4d88d5fc77e05943b3fa0f824d4)] - chore(typings): add index signature of EggAppConfig (#2359) (waiting <>) + * [[`5f2358bbd`](http://github.com/eggjs/egg/commit/5f2358bbdd6e21a1ab387a8425d0fefc30954227)] - docs: intro session.renew in the doc (#2375) (Yiyu He <>) + * [[`f0e7773f2`](http://github.com/eggjs/egg/commit/f0e7773f28eb7a233230a847ff2f8bc737aa3c01)] - docs: add TypeScript Guide (#2324) (TZ | 天猪 <>) + * [[`cd418f57a`](http://github.com/eggjs/egg/commit/cd418f57a843b504dcac6d8c25b99026e1edf072)] - docs(controller): add ctx.redirect (#2373) (Yiyu He <>) + * [[`2fafb16b8`](http://github.com/eggjs/egg/commit/2fafb16b8810e41b86d15f51257c2a0531c78357)] - docs(socketio): update demo & solve problem on chrome (#2354) (Suyi <>) + * [[`ba708ca4e`](http://github.com/eggjs/egg/commit/ba708ca4e911a345d2ee6aea5d4cf5845f93212b)] - feat: support customized client error (#2283) (Khaidi Chu <>) + * [[`8697140d6`](http://github.com/eggjs/egg/commit/8697140d6ab10f42980ea301e7122331b6e5573a)] - chore: add export to declarations (#2344) (Axes <>) + * [[`441884145`](http://github.com/eggjs/egg/commit/4418841452a20a4fcca212e17dad0fbe9ff97646)] - chore(typings): export PowerPartial (#2327) (waiting <>) + * [[`33d39519e`](http://github.com/eggjs/egg/commit/33d39519e1bd9bb1451776abe4986cdf4dee7626)] - docs(passport): config passport-github behind of proxy (#2318) (Suyi <>) + * [[`84e0dc4e7`](http://github.com/eggjs/egg/commit/84e0dc4e74e4e907d39b5485e1b19c3900aec393)] - fix(d.ts): add modifier to plugin and add middleware to config (#2322) (Axes <>) + +## 2018-04-04, Version 2.6.0 @atian25 + +### Notable changes + +* **feature** + * TypeScript tool support (#2272) + +* **docs** + * improve d.ts with ts support (#2306) + * docs improve and translation + +### Commits + + * [[`406142758`](http://github.com/eggjs/egg/commit/40614275845f49512e80d1c8c00d1997ee91b113)] - chore: improve d.ts with ts support (#2306) (Axes <>) + * [[`7fba689b7`](http://github.com/eggjs/egg/commit/7fba689b73fa46fdf7447844338a7f538ad78665)] - docs(controller): session example bug (#2313) (Suyi <>) + * [[`e0e7ed146`](http://github.com/eggjs/egg/commit/e0e7ed146adfe932558628b815caa2d8c64d6939)] - chore(typings): change export interface to class definition (#2293) (waiting <>) + * [[`161107929`](http://github.com/eggjs/egg/commit/1611079291ddcf8cc82dba40a2406dcad20b75b5)] - docs(plugin): add config notice for `addSingleton` function (#2305) (Shangbin Yang <>) + * [[`1c74a8491`](http://github.com/eggjs/egg/commit/1c74a84918869ec035c5767884501a87cce945d5)] - docs: add assets document (#2220) (Haoliang Gao <>) + * [[`e4531e563`](http://github.com/eggjs/egg/commit/e4531e563214472e54b4c467e0d2879e6390cb52)] - docs: EN translation for view plugin dev doc (#2240) (Cemre Mengu <>) + * [[`348ff18d8`](http://github.com/eggjs/egg/commit/348ff18d82e357d9bceba5136c339ef7dfb44bda)] - docs: EN translation for style guide doc (#2239) (Cemre Mengu <>) + * [[`d9c4ec2bb`](http://github.com/eggjs/egg/commit/d9c4ec2bbb3aa29ddcba2efceec6edfa879267d7)] - EN translation for resources doc (#2238) (Cemre Mengu <>) + * [[`46217a5d2`](http://github.com/eggjs/egg/commit/46217a5d2e451433ec86614cfb65336300a074d9)] - docs(security): add ssrf in security (#2274) (Yiyu He <>) + * [[`c3586eab5`](http://github.com/eggjs/egg/commit/c3586eab535ee540ffac664f0311c656ef7adca2)] - docs: deprecate ignoreJSON (#2270) (Yiyu He <>) + * [[`a86334c59`](http://github.com/eggjs/egg/commit/a86334c595530c5f4e9cf65204a4591dfd26bcf0)] - docs: example for custom id when mysql update (#2165) (OnedayLiu <>) + * [[`10327e185`](http://github.com/eggjs/egg/commit/10327e185015098c9c29747abbaf79a352f975d7)] - docs: EN translation for socketio tutorial doc (#2167) (Cemre Mengu <>) + * [[`5b059db6a`](http://github.com/eggjs/egg/commit/5b059db6a879abb3ffe7b30e95855afc8a660107)] - docs: add boilerplate type desc (#2250) (QiChang Li <>) + * [[`9007b5847`](http://github.com/eggjs/egg/commit/9007b5847e67b678f6624da4610b6bdff9457c52)] - chore: update package.json for release (#2244) (Haoliang Gao <>) + +## 2018-03-20, Version 2.5.0 @atian25 + +### Notable changes + +* **feature** + * display router when log app (#2230) + * update `favicon.png` + * upgrade cluster-client to 2.x (#2236) + +* **docs** + * improve d.ts + * add socket.io webchat description (#2198) + +### Commits + + * [[`6040d6f8f`](http://github.com/eggjs/egg/commit/6040d6f8f1a67282ff697c6d86945bc0cb487fe6)] - chore: fix spelling error rotator (#2242) (HE ZIQIANG <>) + * [[`1554da57e`](http://github.com/eggjs/egg/commit/1554da57ef9d8b0fd2cb023a0cc68b50bee6b69f)] - chore: upgrade cluster-client to 2.x (#2236) (zōng yǔ <>) + * [[`9faa052bf`](http://github.com/eggjs/egg/commit/9faa052bfdffe887c1557126a73a62fe2e462dc5)] - feat: tsd add init module (#2233) (Eward Song <>) + * [[`d5f9059f1`](http://github.com/eggjs/egg/commit/d5f9059f1935a67748033143081755811664df9d)] - docs: translation for basic plugin (#2166) (Cemre Mengu <>) + * [[`7afc7e24b`](http://github.com/eggjs/egg/commit/7afc7e24b60776a71702ae5495d637b1ac4a3d06)] - feat: display router when log app (#2230) (Kiho · Cham <>) + * [[`5e99fd6fd`](http://github.com/eggjs/egg/commit/5e99fd6fd86be4de5a2eca24bc2025f120cef6aa)] - docs: egg-passsport-local -> egg-passport-local (楊傑文 Chuck Yang <>) + * [[`c042366df`](http://github.com/eggjs/egg/commit/c042366df6a691e56c528f16083516a53e114944)] - docs(socket.io): add webchat description (#2198) (TZ | 天猪 <>) + * [[`5cce8795a`](http://github.com/eggjs/egg/commit/5cce8795a733c9096de6e050fec5a87be99b0002)] - chore: fix typo. (#2172) (薛定谔的猫 <>) + +## 2018-03-05, Version 2.4.1 @dead-horse + +### Notable changes + +* **fix** + * [security] don't allow x-forwarded-host header by default + * `ctx.runInBackground` will try to use custom function name first + +* **docs** + * improve d.ts + * add regexp as type of path in Router + * fix type of `render` + * more semantic and moment installation in quickstart + +### Commits + + * [[`0eabce6`](http://github.com/eggjs/egg/commit/0eabce6389190cecc00011512ec7e4e63fd0471e)] - fix: don't allow x-forwarded-host header (#2163) (Haoliang Gao <>) + * [[`f0edf96`](http://github.com/eggjs/egg/commit/f0edf9622b6a18831f285e6ceb5a0e2b25b04fd0)] - fix: try to use custom function name first (#2161) (fengmk2 <>) + * [[`1a73720`](http://github.com/eggjs/egg/commit/1a73720d8ba14c612cc6fd38d419212e032049f8)] - fix(typings): add regexp as type of path (#2157) (AngrySean <>) + * [[`b55e908`](http://github.com/eggjs/egg/commit/b55e908643dc2ef1a21c7a4b11559e1785985792)] - doc(quickstart): more semantic and moment installation (#2154) (Kiho · Cham <>) + * [[`951e236`](http://github.com/eggjs/egg/commit/951e236586f3fdc988504f4138351b2c7778e67c)] - Fix type of `render` (#2155) (Arniu Tseng <>) + +## 2018-02-28, Version 2.4.0, @fengmk2 + +### Notable changes + +* **feature** + * support Keep-Alive Header + +* **fix** + * add logger in base_context_class + +* **docs** + * Lots of d.ts improved. + * add context + * add urllib + * add resources & logger + * new documents + * how to call the service + * socket.io tutorial + * add events on application + +### Commits + + * [[`79927324a`](http://github.com/eggjs/egg/commit/79927324a5aeb1f826fc9f133bed253d8324c62e)] - fix: add logger in base_context_class (#2149) (Axes <>) + * [[`a73900231`](http://github.com/eggjs/egg/commit/a7390023150ff4d5a7ec069276a94542a7ef67fa)] - feat: support Keep-Alive Header (#2146) (fengmk2 <>) + * [[`c8284367c`](http://github.com/eggjs/egg/commit/c8284367c727aa2da453a1a485c4d7f97cfb3967)] - docs(ts): fix some d.ts (#2144) (TZ | 天猪 <>) + * [[`e0282b923`](http://github.com/eggjs/egg/commit/e0282b923375132fcc3b936b471999a84eb1e941)] - docs(router): add definition of ctx (#2136) (重庆 <<1756260160@qq.com>>) + * [[`3e7ef6aa5`](http://github.com/eggjs/egg/commit/3e7ef6aa566d800411822d9a4195c9df34634789)] - docs(app-start): how to call service (#2133) (TZ | 天猪 <>) + * [[`9472b5828`](http://github.com/eggjs/egg/commit/9472b5828c95cd1dec2910b657d1e6c34372a6a2)] - docs(schedule): fix log dir (#2123) (TZ | 天猪 <>) + * [[`ede433fc5`](http://github.com/eggjs/egg/commit/ede433fc594c915683a519bf9b409209812806cf)] - docs(unittest):fix some mistakes (#2110) (恬竹 <<2632807692@qq.com>>) + * [[`2d03c79a1`](http://github.com/eggjs/egg/commit/2d03c79a1842846c4caf2f3b971a5bae5fc9f24d)] - chore: add urllib declaration support in index.d.ts (#2117) (SoraYama <>) + * [[`fd6fa2495`](http://github.com/eggjs/egg/commit/fd6fa24955a7a7bceaad7b2f754123282b7e1cbe)] - docs(2.x-advanced-plugin):fix some descriptions (#2111) (恬竹 <<2632807692@qq.com>>) + * [[`0a208d741`](http://github.com/eggjs/egg/commit/0a208d7413d77f12048df91b6bdb6e2dfd047c89)] - docs: translation for advanced/plugin.md (#2075) (DukeFightLife <>) + * [[`42e4ea4c1`](http://github.com/eggjs/egg/commit/42e4ea4c12a542671bac7ca92931e83d0fc439f4)] - docs(schedule):fix some places (#2105) (恬竹 <<2632807692@qq.com>>) + * [[`63278c229`](http://github.com/eggjs/egg/commit/63278c2293b0899165386288c38cac44aa7a0b71)] - docs(2.x-basic-extend):fix some mistakes (#2107) (恬竹 <<2632807692@qq.com>>) + * [[`7a604d37f`](http://github.com/eggjs/egg/commit/7a604d37f5184c268263779fa2b8ca459e3d6f5b)] - docs(2.x-basic-service):fix some mistakes of service (#2102) (恬竹 <<2632807692@qq.com>>) + * [[`a1a4e7dd3`](http://github.com/eggjs/egg/commit/a1a4e7dd32bf040b69e8c8bfbdcae3e483eee335)] - docs(plugin): add description for plugin.local.js (#2104) (TZ | 天猪 <>) + * [[`2cdfcc249`](http://github.com/eggjs/egg/commit/2cdfcc249863630dbb298374dbbe2b45864a0e1c)] - docs(development): adjust to new version vscode (#2098) (TZ | 天猪 <>) + * [[`bb4b29002`](http://github.com/eggjs/egg/commit/bb4b290027a6dcf8404ae357e29aaa6a76d5413a)] - docs(faq): add the most common mistake of config (#2086) (TZ | 天猪 <>) + * [[`5621a8574`](http://github.com/eggjs/egg/commit/5621a8574b60d61dab79f601105b69710559831c)] - docs(schedule): logging && args (#2091) (TZ | 天猪 <>) + * [[`03a894439`](http://github.com/eggjs/egg/commit/03a89443904211785ca600ec74f78d75bbf7a299)] - docs: d.ts of resources& logger (#2079) (x22x22 <>) + * [[`bbfacc5a7`](http://github.com/eggjs/egg/commit/bbfacc5a75984a7ddc111195b51d7da8bd6d0713)] - docs(middleware): use app.middleware instead of app.middlewares (#2077) (x22x22 <>) + * [[`7e9f330ee`](http://github.com/eggjs/egg/commit/7e9f330eea2efcc26e99eb89ad3fb40c517e0101)] - docs(socket.io): add tutorial (#1913) (Suyi <>) + * [[`1224dd65f`](http://github.com/eggjs/egg/commit/1224dd65f2e4dadcce70d9a6e8e66122d93fbdd7)] - docs(2.x-basic-controller):fix some descriptions of basic-controller (#2043) (恬竹 <<2632807692@qq.com>>) + * [[`fa5bdaeb5`](http://github.com/eggjs/egg/commit/fa5bdaeb5fec6385f81bc4c3781036df3fa6d870)] - style(app/extend/request.js): Some Comments from Chinese To English in union (#2051) (DongWei <>) + * [[`06e7710c7`](http://github.com/eggjs/egg/commit/06e7710c73c5f4ad313d08b770e5874919e21b88)] - docs: add events on application (#2039) (Yiyu He <>) + * [[`65e038132`](http://github.com/eggjs/egg/commit/65e038132c9183c66c11fb50e3a8bc6358cdae4c)] - docs(advanced/loader): translate (#1654) (Weilun Xiong <>) + +## 2018-01-26, Version 2.3.0, @dead-horse + +### Notable changes + +* **feature** + * emit `request` and `response` event in every request + +* **docs** + * improve english docs + * add alinode usage + +### Commits + + * [[`50a0f8a`](http://github.com/eggjs/egg/commit/50a0f8ac8fe246d664f73f171b8886f9b9c2eda7)] - doc: fix deploy example (dead-horse <>) + * [[`3b7a313`](http://github.com/eggjs/egg/commit/3b7a313965f9c8ae6e20a16dd74533b1885f216f)] - docs(deploy): more about alinode (#2036) (TZ | 天猪 <>) + * [[`950b9e6`](http://github.com/eggjs/egg/commit/950b9e684f2441674ed85a3c0152002991d2ff86)] - doc: fix deploy docs (dead-horse <>) + * [[`18d6436`](http://github.com/eggjs/egg/commit/18d6436195ca1a73098c643d39ce4560b20e7d76)] - docs: translate advanced/cluster-client.md (#1839) (学究 <>) + * [[`287c761`](http://github.com/eggjs/egg/commit/287c7615ad425b130e2c669a41409bfa763feef2)] - Update deployment.md (#1979) (juju <>) + * [[`22dfaa7`](http://github.com/eggjs/egg/commit/22dfaa72e3851196153f4ecb7f3599d2951e9b1b)] - feat: emit request and response event (#2020) (Yiyu He <>) + * [[`ddbb4b3`](http://github.com/eggjs/egg/commit/ddbb4b3c0ec7cfc5c9b1baa7e678770613bd4761)] - docs(deploy): add alinode (#2025) (TZ | 天猪 <>) + * [[`b5d823f`](http://github.com/eggjs/egg/commit/b5d823f52a770f879da46c6968adadd3fa14e8d7)] - docs(core/unittest): fix path of helper.js(#2029) (#2030) (Jiulong Hu <>) + * [[`1e3a4b3`](http://github.com/eggjs/egg/commit/1e3a4b35801e136dd4f1fbaf3c49b771a50c0f72)] - docs(basic-router):fix some places of basic-router (#2012) (恬竹 <<2632807692@qq.com>>) + +## 2018-01-22, Version 2.2.1, @dead-horse + +### Notable changes + +* **fix** + * log cookie's key when cookie exceed limit length + +* **document** + * improve english documents, fix some grammars + * add link to alicloud node.js perfomance platform + * use PATCH method in resource router + +### Commits + + * [[`aa46eb2`](http://github.com/eggjs/egg/commit/aa46eb26d45012036c69c524db512ed16fde7b6b)] - fix: log cookie's key when cookie exceed limit length (#1996) (Yiyu He <>) + * [[`7993b45`](http://github.com/eggjs/egg/commit/7993b45ec2af8c2d96d82370d877476786504dc8)] - docs(basic-middleware):fix some descriptions of basic-middleware (#1998) (恬竹 <<2632807692@qq.com>>) + * [[`b2d09e1`](http://github.com/eggjs/egg/commit/b2d09e150da70a08c1886b00031c0f07eeb7d830)] - docs: put => patch. (#1793) (#1938) (吴建金 <>) + * [[`dede240`](http://github.com/eggjs/egg/commit/dede240340570c00e3baed8098853a44c902dc21)] - feat: add helper interface in d.ts (#1989) (Axes <>) + * [[`19fe608`](http://github.com/eggjs/egg/commit/19fe6085fedabfc09eb9c26534df237decf4d28e)] - docs: add deer stat (#1974) (TZ | 天猪 <>) + * [[`cef371e`](http://github.com/eggjs/egg/commit/cef371e4a176c62d9b44c0f1e55668e992921d2d)] - docs(basic-env): fix some descriptions base on the Chinese version (#1930) (恬竹 <<2632807692@qq.com>>) + * [[`55d08bd`](http://github.com/eggjs/egg/commit/55d08bded812b81efeee96a0e3465728c7f4f5a2)] - fix(ts): error declare of route.resource (#1959) (AntSworD <>) + * [[`32d7c81`](http://github.com/eggjs/egg/commit/32d7c8199611b00cd5117e6adcf8904ea0b33ff5)] - docs: fix word error (#1965) (jxDeveloper <<896222652@qq.com>>) + * [[`3acf45f`](http://github.com/eggjs/egg/commit/3acf45f77ef791b1e6467bd4047511d867d46cc9)] - docs(basic-config): fix some word spelling (#1931) (恬竹 <<2632807692@qq.com>>) + * [[`0e90819`](http://github.com/eggjs/egg/commit/0e9081954a765228ee9d590f01f3bfaaf1a4e5d8)] - docs(advanced/framework): translation (#1668) (freebyron <>) + * [[`ab1b08e`](http://github.com/eggjs/egg/commit/ab1b08ef520ab8db4cddd8f6cf52f1aa87d6975f)] - docs: fix en index (#1915) (Weilun Xiong <>) + * [[`2270f7f`](http://github.com/eggjs/egg/commit/2270f7f0417f9c78958c6b51e70ad7a0d838d6ec)] - docs(basic-objects): fix some descriptions (#1903) (恬竹 <<2632807692@qq.com>>) + * [[`c136470`](http://github.com/eggjs/egg/commit/c136470861b35a5f796d4edcdd8f6fbce41f7314)] - test: use Buffer.alloc, Buffer.from. (#1895) (薛定谔的猫 <>) + * [[`73bc636`](http://github.com/eggjs/egg/commit/73bc636ddb82bd73fa14fb5f56e8ffe6260b46cc)] - docs(links): Add link to alicloud node.js perfomance platform (#1894) (Jackson Tian <>) + * [[`55d1b0e`](http://github.com/eggjs/egg/commit/55d1b0eb5c4ca27668559b94259f0670b60d57b6)] - docs(deploy): add --ignore-stderr (#1876) (TZ | 天猪 <>) + * [[`532110a`](http://github.com/eggjs/egg/commit/532110abbc01cf3f225c47ed6219d9434c48808c)] - fix: fix 404 page url (#1881) (sam <<289623783@qq.com>>) + +## 2017-12-26, Version 2.2.0, @dead-horse + +### Notable changes + +* **feature** + * `config.meta.logging` to enable log every request when received + +* **document** + * fix some grammars + * add rule for issue + +### Commits + +* [[`9fe5b85`](http://github.com/eggjs/egg/commit/9fe5b8563958d313b02482e5b3fe69c342acfa71)] - feat: enable request started log on meta middleware (#1877) (fengmk2 <>) +* [[`8ce9611`](http://github.com/eggjs/egg/commit/8ce9611e2e2e5098a7a4557e0f8d29cd93ab468c)] - docs(objects): fix some grammars (#1806) (恬竹 <<2632807692@qq.com>>) +* [[`e43aa2b`](http://github.com/eggjs/egg/commit/e43aa2bad227475744ef6422f376475d0ee266c4)] - docs(error-handling): fix some words (#1874) (Fan <>) +* [[`4c1617a`](http://github.com/eggjs/egg/commit/4c1617a16ee3df1b455f5eeb1cb31e37e5f593c1)] - docs(faq): add rule for issue (#1861) (TZ | 天猪 <>) + +## 2017-12-15, Version 2.1.0, @dead-horse + +### Notable changes + +* **feature** + * add 400 response for broken client request to instead of empty response + * dump application router json + +* **fix** + * fix: run dumpConfig at the last ready callback + +* **document** + * migrate docs to egg 2 + * add document for passport + +### Commits + +* [[`40df153`](http://github.com/eggjs/egg/commit/40df153dd7ca8124a7502ba6cdc838835388a0ae)] - feat: add 400 response for broken client request to instead of empty response (#1829) (Khaidi Chu <>) +* [[`d0ee9f2`](http://github.com/eggjs/egg/commit/d0ee9f2500e69a1e0662c9ea597bf97db3418041)] - docs(passport): fix some description (#1828) (TZ | 天猪 <>) +* [[`f7c6a0a`](http://github.com/eggjs/egg/commit/f7c6a0a835ea950e98e40a0b4b83736912b5ab82)] - docs(passport): add description (#1825) (TZ | 天猪 <>) +* [[`f66d9be`](http://github.com/eggjs/egg/commit/f66d9be57807c04058511b47611afa890884b2a5)] - docs(passport): the missing docs for passport (#1824) (TZ | 天猪 <>) +* [[`18f93f0`](http://github.com/eggjs/egg/commit/18f93f0b927a08e6bd356f9fcc6a3141e813e85f)] - docs(core/view.md): translation (#1577) (Zhongyuan <>) +* [[`7e05669`](http://github.com/eggjs/egg/commit/7e056692506f5801390fd804d75bf6756991a54b)] - 1. docs(error-handle): missing function keywords. (#1819) (M.Y.Akashi <>) +* [[`89e114c`](http://github.com/eggjs/egg/commit/89e114cb88ef1ef96e479001bf0f8250867111c9)] - docs: add AntV links (#1809) (TZ | 天猪 <>) +* [[`bdfd3cc`](http://github.com/eggjs/egg/commit/bdfd3cc62b8377cadac2a6c108944d86eaca3df0)] - docs(router): new style & remove app.verb (#1803) (TZ | 天猪 <>) +* [[`4c9eacb`](http://github.com/eggjs/egg/commit/4c9eacbb7d4560924602103e5e23ae578ac34a52)] - docs(middleware): add description of import koa middleware (#1805) (TZ | 天猪 <>) +* [[`c152dee`](http://github.com/eggjs/egg/commit/c152deec69c3dbb06ce87433a46bab0bc61e295b)] - docs(loader): adjust extends way (#1729) (TZ | 天猪 <>) +* [[`289f8cd`](http://github.com/eggjs/egg/commit/289f8cd3d90cc24c55fa51e3d75f5750233af7ee)] - docs(progressive):changes some grammar (#1773) (恬竹 <<2632807692@qq.com>>) +* [[`ae87460`](http://github.com/eggjs/egg/commit/ae87460d6aacb38f4d60d703450f8085c72d3b0d)] - docs(migration): add description for plugin breakchange (#1766) (TZ | 天猪 <>) +* [[`a2788a8`](http://github.com/eggjs/egg/commit/a2788a870175d6c1abdad3c379bbb9adc6c24ba9)] - docs(controller): import base controller directly (#1771) (Yiyu He <>) +* [[`7ebfc9b`](http://github.com/eggjs/egg/commit/7ebfc9b96b03a6b1bdffc3da65c6940902dc3086)] - docs(quickstart): fix typo in code example (#1765) (Darren Poon <>) +* [[`6ff6998`](http://github.com/eggjs/egg/commit/6ff699824dce6962d7aa9e9e48f41a50a994834f)] - docs: add security english translation (#1691) (Adams <>) +* [[`a061f21`](http://github.com/eggjs/egg/commit/a061f21178b2253b587dab780ba74f19605109a4)] - docs(intro): make some changes for egg-and-koa (#1739) (恬竹 <<2632807692@qq.com>>) +* [[`d752b3b`](http://github.com/eggjs/egg/commit/d752b3b795cc0c5a579770695fcadd3db713ff6f)] - docs(deployment): adjust with new version egg-scripts (#1757) (TZ | 天猪 <>) +* [[`1b12b51`](http://github.com/eggjs/egg/commit/1b12b519937e80728d133ea24ff88a2568b72a57)] - docs(cookie-session): use async (#1723) (TZ | 天猪 <>) +* [[`5c88026`](http://github.com/eggjs/egg/commit/5c880266f9968a2e9b102db7c0eea2c7b0f09a43)] - docs(plugin): use async (#1730) (TZ | 天猪 <>) +* [[`ebb8adf`](http://github.com/eggjs/egg/commit/ebb8adfadcf21ba29997c65d5adc5a92235ffa8d)] - some changes of docs(what is egg) (#1734) (恬竹 <<2632807692@qq.com>>) +* [[`2da00fc`](http://github.com/eggjs/egg/commit/2da00fca45d9bc161cae5ab9754a3fcc0321b9c7)] - docs(framework): use new way (#1728) (TZ | 天猪 <>) +* [[`47fbee5`](http://github.com/eggjs/egg/commit/47fbee574b94d9f6420d44e7a8f0ccec035d94f4)] - docs(cluster-client): use async (#1727) (TZ | 天猪 <>) +* [[`1420682`](http://github.com/eggjs/egg/commit/1420682dc5b6fb14342373d9b70614c3de0c015b)] - docs(ipc): use async (#1722) (TZ | 天猪 <>) +* [[`503b69b`](http://github.com/eggjs/egg/commit/503b69b2e5c3f59b9c3c307a50e711cd8eb8d967)] - feat: dump application router json (fengmk2 <>) +* [[`76ff783`](http://github.com/eggjs/egg/commit/76ff783b80a9d9ffc01db1b434c25fedd6e27ca7)] - fix: run dumpConfig at the last ready callback (fengmk2 <>) +* [[`50efe4c`](http://github.com/eggjs/egg/commit/50efe4ceb9a4c8ec902a503db7ad10ffe7819e1a)] - docs(httpclient): use async (#1724) (TZ | 天猪 <>) +* [[`d043148`](http://github.com/eggjs/egg/commit/d043148b8ee69614098b39604dd6b7d7e1a84810)] - docs: remove async-function (#1713) (TZ | 天猪 <>) +* [[`e3ef3ec`](http://github.com/eggjs/egg/commit/e3ef3ec65c5e2874c813f6cda18b61b630d137be)] - docs(restful): use async (#1709) (TZ | 天猪 <>) +* [[`b042937`](http://github.com/eggjs/egg/commit/b042937b1e77b8206206a248c9f3e3ab82b7d6d8)] - docs(error-handling): use async (#1721) (TZ | 天猪 <>) +* [[`80ab243`](http://github.com/eggjs/egg/commit/80ab2439d508e9e0574df31061b5bb14988c2e3e)] - docs(i18n): use async (#1720) (TZ | 天猪 <>) +* [[`6741999`](http://github.com/eggjs/egg/commit/67419996a3abd403ab8d67755ecb98c3a9b97338)] - docs(logger): use async (#1719) (TZ | 天猪 <>) +* [[`f39c105`](http://github.com/eggjs/egg/commit/f39c105067e08fe416f86d0a415f5475ce66ba17)] - docs(view): use async (#1717) (TZ | 天猪 <>) +* [[`cf3de0f`](http://github.com/eggjs/egg/commit/cf3de0f248e3435a7d6ac41ece16dea55f5e86c9)] - docs(unittest): use async (#1716) (TZ | 天猪 <>) +* [[`cb9c9a4`](http://github.com/eggjs/egg/commit/cb9c9a43015a47347273bf8a09d971205b0d57ec)] - docs(mysql): use async (#1711) (TZ | 天猪 <>) + +## 2017-11-20, Version 2.0.0, @dead-horse + +### Notable changes + +* **performance** + * By removing the wrapper code of `co` library, performance increase over 30% (which not include the performance boost coming with Node 8), see [#14](https://github.com/eggjs/benchmark/pull/14) and [benchmark](https://eggjs.github.io/benchmark/plot/) + +* **feature** + * [BREAKING CHANGE] drop node <8 support + * upgrade to egg-core@4(base on koa 2), but still supports all the usages in egg 1 + * upgrade built-in plugins to adapt egg@2 + * `runInBackground` use location as scope name when anonymous + +* **fix** + * dump async function as AsyncFunction + +* **document** + * migrate some documents to async function + * split plugin and plugin development + * refactor the description about cluster client @vincenthou + * add document for how to customize error handler + * translate cookie and session @zhang-z + * translate basics/schedule.md, thanks @Azard + +### Commits + + * [[`8197826`](http://github.com/eggjs/egg/commit/8197826a8dca062c91ba45c235cec66a93f335a4)] - docs: refine egg-and-koa with egg 2 (#1686) (Yiyu He <>) + * [[`757f275`](http://github.com/eggjs/egg/commit/757f275a16741c670f210876408aaeefe5797a23)] - fix: dump async function as AsyncFunction (#1687) (Yiyu He <>) + * [[`12edd64`](http://github.com/eggjs/egg/commit/12edd64915164df6b2d5fed9e179e90954f25687)] - test: use async function instead of generator function (#1684) (Yiyu He <>) + * [[`5513456`](http://github.com/eggjs/egg/commit/5513456e2c702fdc1b7a500f8d8d58048d1041fa)] - feat: runInBackground use location as scope name when anonymous (#1683) (Yiyu He <>) + * [[`212b077`](http://github.com/eggjs/egg/commit/212b077993cff01c08c55fa4545c324adb96322c)] - doc: Add th.yml (#1682) (NatPi <<31546528+NatJNP@users.noreply.github.com>>) + * [[`3ddd67f`](http://github.com/eggjs/egg/commit/3ddd67fbbb83a783541118a05d7e0febb2fde7f3)] - docs(advanced/cluster-client): refactor the description about cluster client (#1417) (vincent.hou <>) + * [[`3d948e4`](http://github.com/eggjs/egg/commit/3d948e44e55fbb88c318a8f14fa7a0b0a8b71b4e)] - docs(plugin): split plugin and plugin development (#1663) (TZ | 天猪 <>) + * [[`b1343ad`](http://github.com/eggjs/egg/commit/b1343ad55f08b15f8084104c54db0b5975716323)] - docs(core/unittest): translate unittest.md (#1660) (freebyron <>) + * [[`fb2d96a`](http://github.com/eggjs/egg/commit/fb2d96ae8e1759edc9126a2920f9028b6e4d15df)] - docs(app-start): generator -> async (#1662) (TZ | 天猪 <>) + * [[`12c0a8a`](http://github.com/eggjs/egg/commit/12c0a8afb8cd332037670f7db8e8662566c1407f)] - docs(quickstart): fix app.Service (#1661) (TZ | 天猪 <>) + * [[`49b0071`](http://github.com/eggjs/egg/commit/49b00712de6eed7c386b07c7c91082ef36cc667f)] - docs(core/cookie-and-session): translate section Cookie (#1562) (Zhongyuan <>) + * [[`ac55d5e`](http://github.com/eggjs/egg/commit/ac55d5eb0b90e2333e3d92523075615e80835647)] - docs: fix typo in async function (#1657) (BccSafe <>) + * [[`9f362d8`](http://github.com/eggjs/egg/commit/9f362d878b61e1144ceab851215dbafb974fb85f)] - docs(basics/schedule.md): translate (#1648) (Weilun Xiong <>) + * [[`448d094`](http://github.com/eggjs/egg/commit/448d0945c0030d2f2bdf8e0f85ccfcbde4ba2b25)] - deps: upgrade all plugins to adapt egg@2 (#1653) (Yiyu He <>) + * [[`4993ee8`](http://github.com/eggjs/egg/commit/4993ee8fae81bf14f92c86ac1d4d952d62e1d165)] - docs(quickstart): generator -> async (#1650) (TZ | 天猪 <>) + * [[`8c6f16d`](http://github.com/eggjs/egg/commit/8c6f16d64834d46b0689ce079cc5d71155848ac8)] - docs: how to customize error handler (#1651) (Yiyu He <>) + * [[`8e8869a`](http://github.com/eggjs/egg/commit/8e8869a4d73908503cf1f60de3be49461639ca08)] - refactor: upgrade egg-core@4 (#1631) (Yiyu He <>) + +## 2017-11-08, Version 1.11.0, @dead-horse + +### Notable changes + +* **feature** + * export global namespace at d.ts @atian25 + +### Commits + + * [[`b131a4c`](http://github.com/eggjs/egg/commit/b131a4cec51cc783dcd4ccb8756439063c5b875c)] - feat: export global namespace at d.ts (#1633) (TZ | 天猪 <>) + +## 2017-11-08, Version 1.10.1, @dead-horse + +### Notable changes + +* **fix** + * use `app.options` instead of deprecated `app._options` +* **document** + * translate core/cluster-and-ipc.md, thanks @lslxdx + +### Commits + + * [[`9eec677`](http://github.com/eggjs/egg/commit/9eec677757ddbde6f7ddcff2c6a698087e07b70e)] - fix: use `app.options` instead of `app._options` (#1625) (Yiyu He <>) + * [[`fd1ff63`](http://github.com/eggjs/egg/commit/fd1ff638920fef4a3258767df982b87e70614215)] - test: fix tsc test case (#1620) (Yiyu He <>) + * [[`6804bd3`](http://github.com/eggjs/egg/commit/6804bd36cfc7a9bd54b3be2d9ce828d4e951f8b8)] - test: add node 9 and drop node 7 (#1602) (fengmk2 <>) + * [[`3878862`](http://github.com/eggjs/egg/commit/38788621ccf06fd6ac8f4068de3e49c5668e1915)] - docs: translate core/cluster-and-ipc.md (#1594) (lslxdx <>) + +## 2017-10-24, Version 1.10.0, @popomore + +### Notable changes + +* **feature** + * add Subscription @popomore +* **document** + * multipart example @dead_horse + * fix document @atian25 @beilunyang + * improve schedule document @atian25 + +### Commits + + * [[`6dd1594a5`](http://github.com/eggjs/egg/commit/6dd1594a5c300f24e668b3679c7ae8df733b6a39)] - docs: fix egg-scripts (#1552) (TZ | 天猪 <>) + * [[`46ed6fac9`](http://github.com/eggjs/egg/commit/46ed6fac9f94d300a23903a71cfafdb5c8b1ba91)] - feat: add Subscription (#1469) (Haoliang Gao <>) + * [[`c508f9fa7`](http://github.com/eggjs/egg/commit/c508f9fa7dedbc8c3c4f6319b7233a034db463b4)] - docs: fix csrf (#1551) (TZ | 天猪 <>) + * [[`7fb9bbf71`](http://github.com/eggjs/egg/commit/7fb9bbf71219debf35b4e864a65be22e24a0480a)] - docs: fix typo (#1537) (悖论 <<786220806@qq.com>>) + * [[`68c0e1a9c`](http://github.com/eggjs/egg/commit/68c0e1a9c053618133d3484043abfb77e3372a22)] - docs: adjust new schedule (#1477) (TZ | 天猪 <>) + * [[`aeae948ec`](http://github.com/eggjs/egg/commit/aeae948ec986f5f7204ad6a0f748403b8e6e6fe1)] - docs: adjust middleware config at framework (#1523) (TZ | 天猪 <>) + * [[`7b37d2393`](http://github.com/eggjs/egg/commit/7b37d2393f59f3c5efbc84cf1d5f51e9332b0cd8)] - docs: multipart example use yield parts() (#1518) (Yiyu He <>) + * [[`6846badc8`](http://github.com/eggjs/egg/commit/6846badc8da89b00483aa7be5c69b1cd2f06d797)] - docs: add plugin.js description (#1499) (TZ | 天猪 <>) + +## 2017-09-25, Version 1.9.0, @gxcsoccer + +### Notable changes + +* **feature** + * make cluster client configurable in egg + * don’t force logger to use INFO level in prod +* **document** + * correct sample codes, by @Jawnkuin + * fix devtools debug, by @atian25 + * adjust debug docs with new egg-bin debug, by @atian25 + * fix port should be number, @atian25 + +### Commits + + * [[`21425e7`](https://github.com/eggjs/egg/commit/21425e7a9c451cfa07f3cb580d0b770eb5b0c890)] - feat: make cluster client configurable in egg (#1459) (gxcsoccer <>) + * [[`d0797b1`](https://github.com/eggjs/egg/commit/d0797b1c2d078d1bea97c104471388bedc5e61c9)] - docs: correct sample codes (#1434) (Jawnkuin <>) + * [[`6eac07e`](https://github.com/eggjs/egg/commit/6eac07eb287ecf158b2c182a0e36a81fa14700ce)] - refactor: httpclient args tracer to be enforced (#1421) (hui <>) + * [[`c56274b`](https://github.com/eggjs/egg/commit/c56274bb818526370f857b926d178ff520b3bea8)] - docs(development): fix devtools debug (#1428) (TZ | 天猪 <>) + * [[`e3f29de`](https://github.com/eggjs/egg/commit/e3f29de9bbbfb67c641cf54272883759d7256d89)] - docs(development): adjust debug docs with new egg-bin debug (#1427) (AnzerWall <>) + * [[`5a9531a`](https://github.com/eggjs/egg/commit/5a9531abbec83fbff08ddb6feb475f87498d2a3d)] - feat: don’t force logger to use INFO level in prod (#1218) (TZ | 天猪 <>) + * [[`95fbd47`](https://github.com/eggjs/egg/commit/95fbd47f4c20797df17dd210f30a40f43d1d8900)] - docs(deployment): port should be number (#1424) (TZ | 天猪 <>) + +## 2017-09-11, Version 1.8.0, @leoner + +### Notable changes + +* **feature** + * support app.httpclient and agent.httpclient auto set tracer +* **fix** + * should extends from egg-core BaseContextClass +* **document** + * English documents `basics/objects`,`core/docs-logger` and `core/httpclient` + have been translated by @DarrenWong, @Azard and @gztchan + * documents typo fixed and improved by @vincenthou, @waitingsong and @hyj1991 + +### Commits + + * [[`54be7dc09`](http://github.com/eggjs/egg/commit/54be7dc099f47fb65b9bc3d9bb29de4d70ac25cd)] - docs(core/cluster-and-ipc): fix some typo (#1415) (vincent.hou <>) + * [[`6cf17c11a`](http://github.com/eggjs/egg/commit/6cf17c11af51220904881ed99aa65cac0f212c2b)] - docs: (core/httpclient): [translate] Done (#1409) (Darren Wong <>) + * [[`105e1947e`](http://github.com/eggjs/egg/commit/105e1947ee0863ebd6c0a1111f218b025e0e9989)] - docs: translate basics/objects (#1238) (Weilun Xiong <>) + * [[`f7c0d8520`](http://github.com/eggjs/egg/commit/f7c0d85209c9e96f7812c4a2996f000a2667770d)] - feat: support app.httpclient and agent.httpclient auto set tracer (#1393) (hui <>) + * [[`3aaee8fbe`](http://github.com/eggjs/egg/commit/3aaee8fbea4aee8b5c40921670642772835bf40d)] - fix: should extends from egg-core BaseContextClass (#1392) (fengmk2 <>) + * [[`a9936a383`](http://github.com/eggjs/egg/commit/a9936a383174fd0b2c201ee759bc5174486970a1)] - fix: typo (#1388) (waiting <>) + * [[`eef30faf6`](http://github.com/eggjs/egg/commit/eef30faf69b41f4a352a592ad65d097698d27303)] - docs: adjust webstorm debug config (#1367) (TZ | 天猪 <>) + * [[`499454379`](http://github.com/eggjs/egg/commit/499454379b2234a80d3946933f7511ac83c292d6)] - docs: curl(url, opts) add parameter introduction (#1351) (#1352) (hyj1991 <<66cfat66@gmail.com>>) + * [[`4daf497eb`](http://github.com/eggjs/egg/commit/4daf497eb32c05c73911a01e861b9cf761ede451)] - docs(en/core/docs-logger): finish logger.md translation in English (#1254) (Tony Chan <>) + * [[`aaacd56c9`](http://github.com/eggjs/egg/commit/aaacd56c9c60dbf0cbfd0d1fcc77366a3e3993fe)] - docs: remove egg-scripts env default description (#1318) (TZ | 天猪 <>) + * [[`4feae70b8`](http://github.com/eggjs/egg/commit/4feae70b8c8e69890053bff9f3df9cc7024d69cd)] - docs: add egg-scripts to deployment (#1279) (TZ | 天猪 <>) + * [[`08ed1b3c6`](http://github.com/eggjs/egg/commit/08ed1b3c68e242eba187640c9f6cf8a0acd7489f)] - docs(unittest): typo of egg-mock (#1284) (TZ | 天猪 <>) + * [[`734854c84`](http://github.com/eggjs/egg/commit/734854c84ef8b0107df3101b6aa212d96574b317)] - docs(unittest): add bootstrap usage (#1278) (Yiyu He <>) + * [[`ebbbcd574`](http://github.com/eggjs/egg/commit/ebbbcd574f5bbd4d91bec345e8b35f9adc48d6c0)] - chore: skip docs deploy at ci cron (#1268) (TZ | 天猪 <>) + +## 2017-07-27, Version 1.7.0, @popomore + +### Notable changes + +* **feature** + * Support listen options in config.js +* **improve** + * `app.HttpClient` can be overwritten +* **document** + * Document improvement + * English documents have been translated by @gztchan + +### Commits + + * [[`dd07cacb2`](http://github.com/eggjs/egg/commit/dd07cacb209565cc8bdc240b2a3bd7f624a3e56c)] - docs: fix typo on CONTRIBUTING.zh-CN.md (#1266) (SuperEwe <>) + * [[`773343061`](http://github.com/eggjs/egg/commit/7733430614d62392fa1b06568e223ce2ae5b3709)] - docs: only deploy docs at 8 (#1252) (TZ | 天猪 <>) + * [[`4f2ebfda8`](http://github.com/eggjs/egg/commit/4f2ebfda81c067ba500ee22ac30c8b201f746cac)] - docs: fix const define (#1249) (TZ | 天猪 <>) + * [[`45bea3cb5`](http://github.com/eggjs/egg/commit/45bea3cb55636a09160bfc66befca476994dacc8)] - docs(core-deployment): translate deployment.md in English (#1235) (Tony Chan <>) + * [[`dda386e42`](http://github.com/eggjs/egg/commit/dda386e425ce96019f3d068e66603f80af966571)] - test: add test and doc for listen options (#1246) (Haoliang Gao <>) + * [[`3ef1de952`](http://github.com/eggjs/egg/commit/3ef1de95247aa3e3fdcbda71fe83e58a892a13d6)] - feat: set cluster options, include path, port, hostname (#1231) (Haoliang Gao <>) + * [[`e9f93cf83`](http://github.com/eggjs/egg/commit/e9f93cf83d46fd84c8c6b10ec2e7e3eb2bf24f9d)] - refactor: export app.HttpClient that can be overwritten (#1234) (Haoliang Gao <>) + * [[`96b3786eb`](http://github.com/eggjs/egg/commit/96b3786eb9640c9ec2d71a5a0a0b18ee32e9e3ad)] - docs(core/error-handling): translate error-handling.md in English (#1228) (Tony Chan <>) + * [[`c3c9fce55`](http://github.com/eggjs/egg/commit/c3c9fce557a8d8c57b2a5e5391d1c11a81ceeaa7)] - docs(controller): examples use controller class (#1221) (Yiyu He <>) + * [[`24f279005`](http://github.com/eggjs/egg/commit/24f2790051c0d248f6df9850ffe8513dc11e5780)] - docs: new VScode 1.14 default protocol changed. (#1212) (Anto <>) + * [[`2b78b4cf8`](http://github.com/eggjs/egg/commit/2b78b4cf8275171ddf788550745edc3aef948ca7)] - docs: Fix config name from egg-Plugin to eggPlugin in plugin's doc (#1215) (hansen <>) + +## 2017-07-19, Version 1.6.1, @fengmk2 + +### Notable changes + +* **fix** + * make sure config.httpclient.httpAgent.timeout >= 30000, and distinguish + options: request, httpAgent and httpsAgent on `config.httpclient`. + +### Commits + + * [[`988b8c8`](http://github.com/eggjs/egg/commit/988b8c84d0f63ce0e83e00bd12cff65ebf4f2ff5)] - fix: make sure config.httpclient.httpAgent.timeout >= 30000 (#1165) (fengmk2 <>) + * [[`894005c`](http://github.com/eggjs/egg/commit/894005c8e683e764ec234c915afce89b57343f98)] - docs: (core/i18n): [translate] Done (#1194) (Darren Wong <>) + * [[`410633b`](http://github.com/eggjs/egg/commit/410633b3e47098abc30d83429895b543431929ec)] - chore: kill ssh-agent after deploy (#1204) (Haoliang Gao <>) + * [[`05f4785`](http://github.com/eggjs/egg/commit/05f47858a6d74041a10539443a9ea2e195826bc4)] - chore: add travis_wait to avoid deploying document timeout (#1201) (Haoliang Gao <>) + * [[`367e1d6`](http://github.com/eggjs/egg/commit/367e1d66ef3bdb49ee41758246cdaf49e04ea140)] - docs: fix typo (#1191) (BingqiChan <>) + +## 2017-07-04, Version 1.6.0, @fengmk2 + +### Notable changes + +* **feature** + * tsd add ctx.logger and logger.error support Error object + * ignore any key contains "secret" on dump config files + * show who define the property of the config on `run/application_config_meta.json` +* **fix** + * don't cache the intermediate locals for application + +### Commits + + * [[`5dc56fa`](http://github.com/eggjs/egg/commit/5dc56fac043eab22187f9ae1dd7e73d2160fd7ae)] - feat: ignore any key contains "secret" (#1156) (fengmk2 <>) + * [[`74c8a54`](http://github.com/eggjs/egg/commit/74c8a547cc90939253946a145655996b59373457)] - feat: dump `run/${type}_config_meta.json` (#1155) (Haoliang Gao <>) + * [[`b80bb14`](http://github.com/eggjs/egg/commit/b80bb1405c1f47c5596ff4a2c9540af7447430ec)] - fix: don't cache the intermediate locals for application (#1146) (Jackson Tian <>) + * [[`7c70beb`](http://github.com/eggjs/egg/commit/7c70beb26ecf2176cda7547f3163fec11aff450f)] - docs: change istanbul to nyc (#1150) (TZ | 天猪 <>) + * [[`c7a87a8`](http://github.com/eggjs/egg/commit/c7a87a8abade84769b34a1ef0ba50a3cc12dec49)] - docs: adjust objects docs (#1140) (TZ | 天猪 <>) + * [[`0052351`](http://github.com/eggjs/egg/commit/005235162dce4b0e87768a201c9a68c4291592d4)] - docs: improve plugin dependencies (#1061) (luicfer <>) + * [[`4322212`](http://github.com/eggjs/egg/commit/43222127b922b486d8f523230fed82ba453ee8d8)] - docs: add missing class in objects.md (kaiye <>) + * [[`daa8227`](http://github.com/eggjs/egg/commit/daa82278332d7617d6ebb3d07e8cdfd1e95cf644)] - feat(tsd): add ctx.logger and logger.error support Error object (#1108) (Eward Song <>) + * [[`7c2e436`](http://github.com/eggjs/egg/commit/7c2e43626d93049b5f91f59773ef02c4b0f478b3)] - docs: improve feature describe (#1102) (Yiyu He <>) + * [[`5ae7814`](http://github.com/eggjs/egg/commit/5ae7814632b1f92d534a496fe4f51c6737447aba)] - chore: comments in english (#1101) (Yiyu He <>) + * [[`9099be9`](http://github.com/eggjs/egg/commit/9099be91afa806d0a8258441d11e1da2318777ef)] - docs: unify config in quickstart (#1094) (Yiyu He <>) + * [[`c31bc15`](http://github.com/eggjs/egg/commit/c31bc15097c692d08596a277c69fbd44f9d3e2bf)] - test: wait logger to flush (#1090) (Haoliang Gao <>) + * [[`82d2158`](http://github.com/eggjs/egg/commit/82d2158e4c399ead9567b67dc27d13d1ef2e104e)] - docs: add Enclose.IO to Links (#1089) (Minqi Pan <>) + +## 2017-06-21, Version 1.5.0, @fengmk2 + +### Notable changes + +* **feature** + * better TypeScript support, add `index.d.ts` file. + * enable overrideMethod middleware by default. +* **document** + * Documents improved. + +### Commits + + * [[`1d02601`](http://github.com/eggjs/egg/commit/1d026019df76525d2d9117c260eb5d892388121c)] - tsd: add another properties of FileStream (#1080) (Rwing <>) + * [[`2b1644e`](http://github.com/eggjs/egg/commit/2b1644e6d56e6481ee97bce009c5f53b4dd18441)] - feat: add tsd (#1027) (Eward Song <>) + * [[`a4ba2a2`](http://github.com/eggjs/egg/commit/a4ba2a2a1ef7de49e196c01447fd73ab22ed6d34)] - feat: enable overrideMethod middleware by default (#1069) (fengmk2 <>) + * [[`bfb8df5`](http://github.com/eggjs/egg/commit/bfb8df58bcc7d7fe0fd6ff3453efcb54b715b4a0)] - docs: typo (#1060) (chenbin92 <>) + * [[`64d1b00`](http://github.com/eggjs/egg/commit/64d1b0026648e1128f09efb6e6c2cc7f632bf608)] - docs: add chrome devtools debug information (#1050) (仙森 <>) + * [[`4e510b2`](http://github.com/eggjs/egg/commit/4e510b22836096a47d562dbd5ca8affd28f94f9e)] - chore: use app.httpRequest() instead of supertest (#1041) (fengmk2 <>) + * [[`78a13d5`](http://github.com/eggjs/egg/commit/78a13d52c3b6e8b40a0015b285cda33a059c0ee4)] - docs: add more description at quickstart (#1042) (TZ | 天猪 <>) + * [[`ef7c864`](http://github.com/eggjs/egg/commit/ef7c864fbddf7e70afbd93a16d5176787328400d)] - docs: add ant.design link (#1037) (Haoliang Gao <>) + * [[`f1b510c`](http://github.com/eggjs/egg/commit/f1b510c34039259c5772021432ab71a7a62b89e8)] - feat: add config.logger.disableConsoleAfterReady (#1001) (fengmk2 <>) + * [[`4890eda`](http://github.com/eggjs/egg/commit/4890eda31b9bc60ea4a1a7f36460ec1bf86dc134)] - docs: Uniform the standards that we should acquire this parsed parame… (#1038) (Ruanyq <>) + * [[`9d705e4`](http://github.com/eggjs/egg/commit/9d705e4687cdb98d327fbd9a1061604828218dfc)] - test: make sure app close (#1030) (fengmk2 <>) + * [[`1d72e37`](http://github.com/eggjs/egg/commit/1d72e3799822e252934d6218a978c2bd21f378d3)] - docs: fix caseStyle link (#1033) (Desen Meng <>) + * [[`9b50725`](http://github.com/eggjs/egg/commit/9b507250725ef3beda0ee51ac0c2bc2b007b2ecb)] - docs: (tutorials/index.md & async-function.md ): [translate] Done (#1028) (Darren Wong <>) + * [[`3d04199`](http://github.com/eggjs/egg/commit/3d041992912d9aca1eb0edc6b226474022e08236)] - docs: typo (#1029) (Jerry Wu <>) + * [[`13b7c19`](http://github.com/eggjs/egg/commit/13b7c19531d772a2b932ada28e186a0dbd0cf5f5)] - test: node 8 (#976) (fengmk2 <>) + * [[`1b108a7`](http://github.com/eggjs/egg/commit/1b108a72a96d3d8241b332b8e728a9ec409efbb1)] - docs: remove api that is from egg-rest (#1022) (Haoliang Gao <>) + * [[`057bc47`](http://github.com/eggjs/egg/commit/057bc47e4c5e3ec8faae0de3807f656fa4ef41d4)] - test: add doc test (#989) (Haoliang Gao <>) + * [[`c6eb7b2`](http://github.com/eggjs/egg/commit/c6eb7b2f59f24fe0c6a787829d33cdf0cd4a2e77)] - doc: fix view config doc (#991) (当轩 <>) + * [[`52865b4`](http://github.com/eggjs/egg/commit/52865b47c4d336833ef1151bae9f30867359ceb6)] - docs: devtool inspect at 8.x (#1018) (TZ | 天猪 <>) + * [[`8a120fd`](http://github.com/eggjs/egg/commit/8a120fde73df60e23f8c5559a3281acaf0a393e0)] - docs: remove max time limit at schdule (#995) (TZ | 天猪 <>) + * [[`9084c24`](http://github.com/eggjs/egg/commit/9084c24dd10fcbcd0d436ada9639b59f36dd2edf)] - docs: add plugin list (#988) (Haoliang Gao <>) + * [[`20a5d91`](http://github.com/eggjs/egg/commit/20a5d9127f7454c899f7701f02b04eefa7c61fce)] - test: disable coverage for schedule (#987) (Haoliang Gao <>) + * [[`3de963f`](http://github.com/eggjs/egg/commit/3de963f3881ef6fb9c5b6fa207730c6695853239)] - docs(basics/structure.md): [translate] (#970) (Weilun Xiong <<330815461@qq.com>>) + * [[`2f232f3`](http://github.com/eggjs/egg/commit/2f232f30b0ba7e14ab07c43e34d363bac3906a43)] - docs: file must appear after other fiels when using getFileStream (#982) (Yiyu He <>) + +## 2017-05-28, Version 1.4.0, @dead-horse + +### Notable changes + +* **feature** + * use lru to aovid oom when httpclient dns cache enabled +* **fix** + * fix port is missed when httpclient dns cache enabled + * fix request url object will be changed when httpclient dns cache enabled + * set maxSockets defautl value to Number.MAX_SAFE_INTEGER +* **document** + * Documents improved. Thanks @DarrenWong, @zousandian, @lslxdx, @Azard, @johnnychen, @coogleyao, @DanielWLam, @m31271n, @Brian175 + +### Commits + +* [[`7370a62`](http://github.com/eggjs/egg/commit/7370a62e190db55dab3fde7f39f621f449301eaa)] - docs: translate tutorials/restful.md (#908) (Darren Wong <>) +* [[`5d8ca65`](http://github.com/eggjs/egg/commit/5d8ca654f311c52fd5faaa939943071c3f69f43f)] - docs: translatebasics/controller.md (#889) (lslxdx <>) +* [[`5b959e0`](http://github.com/eggjs/egg/commit/5b959e0a382491b3111afb66e10b6e866105e0c8)] - docs: translate tutorials/progressive.md to English version (#966) (Darren Wong <>) +* [[`35fa5a9c`](http://github.com/eggjs/egg/commit/35fa5a9c4c2d969f66a5e4df28e1da7f69370709)] - fix: set maxSockets defautl value to Number.MAX_SAFE_INTEGER (#938) (tangyao <<2001-wms@163.com>>) +* [[`5b6fe2b`](http://github.com/eggjs/egg/commit/5b6fe2b187b2c1a4bcee4693b2b1043f2724fe68)] - feat: use lru to aovid oom in dns cache httpclient (#961) (Yiyu He <>) +* [[`3c5c0b8`](http://github.com/eggjs/egg/commit/3c5c0b8d81bb63166f6592390d14277d3baca283)] - docs: Fix objects.md typo (#969) (三点 <>) +* [[`2bca50b`](http://github.com/eggjs/egg/commit/2bca50b2217424b8cdacd48550dcc39a31e50cff)] - docs(core/unittest.md): update with app.httpRequest() (#943) (Weilun Xiong <<330815461@qq.com>>) +* [[`713e033`](http://github.com/eggjs/egg/commit/713e033f90eb39aad8ac48916985396ca5282815)] - docs: app.controller.foo instead of 'foo' (#942) (Yiyu He <>) +* [[`cfc76ec`](http://github.com/eggjs/egg/commit/cfc76ec721460780d703ead1dfdd315ed484e5c8)] - fix spell error from sign to signed (#932) (johnnychen <>) +* [[`12499d6`](http://github.com/eggjs/egg/commit/12499d636dd471f35e54aad9f09b5f452ea198bf)] - docs: fix yield db.query for en (#930) (Yao Mengfei <>) +* [[`25c7c95`](http://github.com/eggjs/egg/commit/25c7c95bff9eb51baf4f93724444982209872895)] - docs: translate basics/router.md (#896) (lslxdx <>) +* [[`a5c7ac4`](http://github.com/eggjs/egg/commit/a5c7ac462a275c5393f93308a7f31b21cba524a2)] - docs: translate basics/service.md (lslxdx <>) +* [[`7ee5de6`](http://github.com/eggjs/egg/commit/7ee5de6b0ad628332a5c130eb5a405b993a98c60)] - docs: translate basics/extend.md (#884) (DanielLam <>) +* [[`9bf3a65`](http://github.com/eggjs/egg/commit/9bf3a6511469ee85963096836ae8c2421313448d)] - docs: Update env.md (#918) (m31271n <>) +* [[`b3825f3`](http://github.com/eggjs/egg/commit/b3825f33406c01ebc19b16519eccfca9f60e770f)] - docs: fix objects.md (#928) (Yiyu He <>) +* [[`fd04ea2`](http://github.com/eggjs/egg/commit/fd04ea222af962e7fe9b82d108a0bd6f23b32891)] - docs: add document for built-in objects (#914) (Yiyu He <>) +* [[`6180d5d`](http://github.com/eggjs/egg/commit/6180d5db90047a58222ba24d660c1a19b93648f3)] - docs: use names of constants declared (#923) (Yao Mengfei <>) +* [[`02b02e0`](http://github.com/eggjs/egg/commit/02b02e0faf0d423105136723b9d2938a182fd486)] - docs: using a doctools as a external lib (#913) (Haoliang Gao <>) +* [[`5113088`](http://github.com/eggjs/egg/commit/51130889ad8d75baa157c43d9b88e7d08c6067fe)] - fix(docs): yield db.query (#921) (Yao Mengfei <>) +* [[`ddd342c`](http://github.com/eggjs/egg/commit/ddd342c84358319aaffe9ee6eab90c2df1a2e9dc)] - docs: translate basic/config.md (#875) (Brian175 <>) +* [[`ae99e5d`](http://github.com/eggjs/egg/commit/ae99e5d6ee032171d17ce7ce67a8cb3c2f7bd04b)] - fix(docs): basics/structure.md link agent typo (#909) (Weilun Xiong <<330815461@qq.com>>) +* [[`fac3e0c`](http://github.com/eggjs/egg/commit/fac3e0c7306b1143698c29a3685c8116c36b1434)] - refactor: rename private method name to symbol (#904) (Yu Qi <>) +* [[`8115c57`](http://github.com/eggjs/egg/commit/8115c575ea082a92ebda5e4fd08ba4ad37e47bc0)] - docs: translate docs/source/zh-cn/tutorials/mysql.md (#883) (Darren Wong <>) +* [[`e13c515`](http://github.com/eggjs/egg/commit/e13c515226566ae3c87c35b575a8e914e75c6a0b)] - Release 1.3.0 (#885) (fengmk2 <>) + +## 2017-05-11, Version 1.3.0, @fengmk2 + +### Notable changes + + * **document** + * Documents improved. Thanks @Rwing, @lslxdx, @solarhell, @magicdawn + * API document is out https://eggjs.org/api/ + * **refactor** + * Set coreLogger's consoleLevel to WARN in local env + +### Commits + + * [[`bd6681a`](http://github.com/eggjs/egg/commit/bd6681a509f74af7f39b1505962c0d75958ae0d3)] - chore: typo eggg=>egg (#881) (Rwing <>) + * [[`22c9cd9`](http://github.com/eggjs/egg/commit/22c9cd96df19bb43d1681ce0cffc59bc930c8f0f)] - docs: translated & proofread 'middleware.md' (#784) (lslxdx <>) + * [[`e55a134`](http://github.com/eggjs/egg/commit/e55a13439ec297081d33a7eb2f87ece605581908)] - docs: Add a link to issue template (#853) (Haoliang Gao <>) + * [[`b01d30e`](http://github.com/eggjs/egg/commit/b01d30e33e9b910ee24d69d3b55e2dbe887ff4e3)] - docs: Fix typo. (#869) (jethro <>) + * [[`b3403b5`](http://github.com/eggjs/egg/commit/b3403b56a5635394a4dc9825ef2780850449e573)] - docs: fix view typo (#867) (Tao <>) + * [[`5d6e067`](http://github.com/eggjs/egg/commit/5d6e067fc36697b7c01f290bccac06ce21fb4371)] - chore: add quality badge (#857) (仙森 <>) + * [[`8d6755b`](http://github.com/eggjs/egg/commit/8d6755b33c54d6230d1b20141dd6d043ed6c3897)] - deps: upgrade dependencies (#854) (Haoliang Gao <>) + * [[`bd0a827`](http://github.com/eggjs/egg/commit/bd0a827c38f0a2cff42c8a73909081a1f9cd939a)] - refactor: set consoleLevel WARN of coreLogger in local (#850) (Haoliang Gao <>) + * [[`af174ef`](http://github.com/eggjs/egg/commit/af174efb0a0dfe545849d03f8ec1fbee34559dae)] - docs: Add API document to menu (#845) (Haoliang Gao <>) + * [[`edfc07e`](http://github.com/eggjs/egg/commit/edfc07e841b751a4c195544167f50a2ad56971e8)] - chore: generate puml (#842) (Haoliang Gao <>) + +## 2017-05-04, Version 1.2.1, @popomore + +### Notable changes + + * **fix** + * loadPlugin can be extended + +### Commits + + * [[`13587667`](http://github.com/eggjs/egg/commit/13587667ac019ca516ae11aea728e84966dc57a5)] - fix(loader): loadPlugin can be extended (#836) (Haoliang Gao <>) + * [[`1a027ad7`](http://github.com/eggjs/egg/commit/1a027ad727468d48afe45d1f3ce54de2e4ecfd16)] - test: use assert instead of should (#837) (Haoliang Gao <>) + * [[`89b4df9d`](http://github.com/eggjs/egg/commit/89b4df9d21ddf07efd246145c52141a72e07ad80)] - docs: fix wrong name in chinese router doc (#833) (Tomatoo <<424203705@qq.com>>) + +## 2017-04-28, Version 1.2.0, @popomore + +### Notable changes + + * **document** + * Documents improved, Thanks @Rwing, @bingqichen, @okoala, @binsee, @lslxdx + * **feature** + * Move BaseContextClass to egg and add BaseContextLogger [#816](https://github.com/eggjs/egg/pull/816) + * Remove logger config in local environment [#695](https://github.com/eggjs/egg/pull/695) + +### Commits + + * [[`0757655c`](http://github.com/eggjs/egg/commit/0757655cfed451ab3b1ca5a480fb96a3da908708)] - feat: BaseContextClass add logger (#816) (Yiyu He <>) + * [[`9871e450`](http://github.com/eggjs/egg/commit/9871e45098ab4927236382656c4ac774eeffcd11)] - docs: only use inspect at 7.x+ (#813) (TZ | 天猪 <>) + * [[`394bf371`](http://github.com/eggjs/egg/commit/394bf3711312f09d851be728b718e4d0f8fc9c1f)] - docs:Modify some words (#811) (binsee <>) + * [[`1132779c`](http://github.com/eggjs/egg/commit/1132779c4057bf96be1b73a3473b1545c3b5ab7a)] - docs(head.swig):fix the document page anchor position offset. (#790) (binsee <>) + * [[`9ef9d6aa`](http://github.com/eggjs/egg/commit/9ef9d6aa5953106555f11ac5dee6fe544773ceb8)] - fix(package.json & doc.js): fix doc tool error. (#791) (binsee <>) + * [[`90234efb`](http://github.com/eggjs/egg/commit/90234efbae13066ced3d25e8ba7201c0197c3ab1)] - docs(middleware.md): fix grammar (lslxdx <>) + * [[`9200a51d`](http://github.com/eggjs/egg/commit/9200a51d5b5c530a8f5ce8af4dd61f38981dc4c8)] - docs(basic/controller.md): typo 'matchs' -> 'matches' (#802) (lslxdx <>) + * [[`b4eb05b3`](http://github.com/eggjs/egg/commit/b4eb05b301bb1226edfc634ec90d1a5ae53514e2)] - docs(zh-cn docs):fix some link and link text in docs (#789) (binsee <>) + * [[`df1bf345`](http://github.com/eggjs/egg/commit/df1bf3459fd03f948532f7b6d2d436a74c54ed59)] - docs: add inspector protocol vscode debug (#776) (仙森 <>) + * [[`a8893f7e`](http://github.com/eggjs/egg/commit/a8893f7e7d9937d675d8be0da7bed0f2c259ae39)] - docs: add vscode debug (#751) (#767) (仙森 <>) + * [[`d4c345d3`](http://github.com/eggjs/egg/commit/d4c345d3d29266e0eb248eecee27bc0e492f5e5e)] - docs: typo fix "aync => async" (BingqiChen <>) + * [[`492c97d6`](http://github.com/eggjs/egg/commit/492c97d61c75911ae0e987f65325a5c7493f63b9)] - docs: add vscode plugin link (#756) (TZ | 天猪 <>) + * [[`2bf23fef`](http://github.com/eggjs/egg/commit/2bf23feffb7b9ff1bc07d072a4052eec863d001c)] - docs: link plugins to github search results (#755) (Yiyu He <>) + * [[`5befb0b1`](http://github.com/eggjs/egg/commit/5befb0b1f0f525ba778d54a5dedb72f2e880ab60)] - feat: remove egg logger local config (#695) (TZ | 天猪 <>) + * [[`1ab42e02`](http://github.com/eggjs/egg/commit/1ab42e0243354eab7f602faebd76d7117038e877)] - docs: document for middleware order (#724) (Haoliang Gao <>) + * [[`d6be9499`](http://github.com/eggjs/egg/commit/d6be949973002880a2fe71313c7630f7f94fde97)] - chore: remove chinese commnets (#749) (Yiyu He <>) + * [[`3bdbcae2`](http://github.com/eggjs/egg/commit/3bdbcae2486073447849f6e09831860dc42995d6)] - docs: fix typo, egg-bin => egg-init (#747) (Rwing <>) + +## 2017-04-11, Version 1.1.0, @fengmk2 + +### Notable changes + + * **document** + * Lots of documents improve and typo fixes. Thanks @lslxdx, @zhennann, @dotnil, @no7dw, @cuyl, @Andiedie, @kylezhang, + @SF-Zhou, @yandongxu, @jemmyzheng, @Carrotzpc, @zbinlin, @OneNewLife, @monkindey, @simman, + @demohi, @xwang1024 and @davidnotes + * **feature** + * warn if some confused configurations exist in config [#637](https://github.com/eggjs/egg/pull/637) + * use extend2 instead of extend to support `Array` config value [#674](https://github.com/eggjs/egg/pull/674) + * expose context base classes on Application instance, make app or framework override context extend more easily [#737](https://github.com/eggjs/egg/pull/737) + * expose egg.Controller and egg.Service [#741](https://github.com/eggjs/egg/pull/741) + * **fix** + * remove unused `jsonp` context delegate to response, please use [jsonp middleware instead](https://eggjs.org/zh-cn/basics/controller.html#jsonp) [#739](https://github.com/eggjs/egg/pull/739) + +### Commits + + * [[`241b4e8`](http://github.com/eggjs/egg/commit/241b4e83c05e7086493564e536f5ce69d17dde0c)] - feat: expose egg.Controller and egg.Service (#741) (Yiyu He <>) + * [[`26efa42`](http://github.com/eggjs/egg/commit/26efa427cf34e0ef0482d69fc10a77280e5fea5e)] - fix: remove unused jsonp delegate (#739) (Yiyu He <>) + * [[`c33523d`](http://github.com/eggjs/egg/commit/c33523db3e086eafd1f7bc7486c6d1b2b68335e3)] - feat: export context base classes on Application (#737) (fengmk2 <>) + * [[`ee127ad`](http://github.com/eggjs/egg/commit/ee127ad46b33a19d43c84a04649569a404a7f6af)] - docs: add sub directory support for controller (#734) (Yiyu He <>) + * [[`88a1669`](http://github.com/eggjs/egg/commit/88a166933478373c4fd5cdd349d3b63e00cbaf7e)] - docs: typo at controller.md (#720) (lslxdx <>) + * [[`4c298c2`](http://github.com/eggjs/egg/commit/4c298c2c70017d12688e2801bfe6e66886ba24bd)] - docs: async-function typo, change generator to async (#712) (zhennann <>) + * [[`a9d27d0`](http://github.com/eggjs/egg/commit/a9d27d0ab3f3dea89487fc1e8c084b9ddc7e854d)] - docs: add schedule max interval (#711) (Yiyu He <>) + * [[`9e94b7b`](http://github.com/eggjs/egg/commit/9e94b7b31106ce578a67dd15984d847587527299)] - docs: little grammar issues (#707) (Chen Yangjian <>) + * [[`a4d12ec`](http://github.com/eggjs/egg/commit/a4d12ecc6c468ebf37ff6acba06e65b15cfde4f4)] - chore: remove unused config (#694) (Yiyu He <>) + * [[`88449f9`](http://github.com/eggjs/egg/commit/88449f9b292d69bd2f936f0ecb037efecbed2e8e)] - docs: add webstorm debug (#689) (TZ | 天猪 <>) + * [[`8517625`](http://github.com/eggjs/egg/commit/8517625b44f36909169032f8fff3ced3e1910a47)] - docs: correct spelling mistake (#682) (Wade Deng <>) + * [[`92ef92b`](http://github.com/eggjs/egg/commit/92ef92b7cec015d2843c9d7cb113694ad7ca34ec)] - docs: faq add where are my logs (#680) (Yiyu He <>) + * [[`b8fc4e4`](http://github.com/eggjs/egg/commit/b8fc4e460e2dcffe60364a71dec2d07bd354d2cf)] - deps: use extend2 instead of extend (#674) (Yiyu He <>) + * [[`0ccbcf9`](http://github.com/eggjs/egg/commit/0ccbcf98be8946891b520321743d3b5a95899955)] - docs: fix example code syntax error & typos (#672) (cuyl <<463060544@qq.com>>) + * [[`1486705`](http://github.com/eggjs/egg/commit/14867059b5070b274cbee26df3accf5463eb4fe8)] - docs: security match and ignore (#668) (Yiyu He <>) + * [[`7ab3791`](http://github.com/eggjs/egg/commit/7ab37915afc4a197cc58bc477e5b96cb1a73ced1)] - test: test for closing logger (#667) (Haoliang Gao <>) + * [[`5f5cf91`](http://github.com/eggjs/egg/commit/5f5cf91a6af118ebc558252e07bcfa0f094045e3)] - docs(quickstart): tip for controller and config style (#666) (TZ | 天猪 <>) + * [[`e47c24b`](http://github.com/eggjs/egg/commit/e47c24b3f1fd27b0f545f107913d6c6e1cae53ac)] - docs: fix example code typos (#629) (SF-Zhou <>) + * [[`7900576`](http://github.com/eggjs/egg/commit/7900576e690d038e4d75891890c467c743f03605)] - docs: fix egg-session-redis code (#642) (周长安 <>) + * [[`8c77e59`](http://github.com/eggjs/egg/commit/8c77e5907834cb110a99a4ace0356868107c88e6)] - feat: warn if some confused configurations exist in config (#637) (Yiyu He <>) + * [[`cd8c659`](http://github.com/eggjs/egg/commit/cd8c65965dc62fe7d45598450d6ef31ab344b878)] - docs: fix some typo (#638) (kyle <>) + * [[`7d830b7`](http://github.com/eggjs/egg/commit/7d830b7c92f81a9d133b7f1e6fe71b3d8a8d5a31)] - docs: fix reference framework path (#634) (kyle <>) + * [[`a471e93`](http://github.com/eggjs/egg/commit/a471e93977e67c98280af8517100bfe48495bbb2)] - docs: fix example code in basics/middleware (#624) (SF-Zhou <>) + * [[`e87c170`](http://github.com/eggjs/egg/commit/e87c170770c117d275fd84c02a9fb1e699fa94cf)] - docs: fix code syntax (#628) (dongxu <>) + * [[`531dadd`](http://github.com/eggjs/egg/commit/531dadd7c3f8bd813c365d705ce7293a719e98f3)] - docs(security): Cookie of token, the key must be csrfToken (#625) (jemmy zheng <>) + * [[`8d73b02`](http://github.com/eggjs/egg/commit/8d73b02dcb856e3d8075aa34bc47a2f6dbb3af2b)] - docs: move cnzz to layout (#622) (Haoliang Gao <>) + * [[`077bebe`](http://github.com/eggjs/egg/commit/077bebe17889d8a0cff2a1dbfebd72b4b8147ab3)] - docs: fix table render error in en env.md (#621) (SF-Zhou <>) + * [[`990d45e`](http://github.com/eggjs/egg/commit/990d45e75f2d73b9bb4cddbf76e67452740e3178)] - docs: fixed table render error in env.md (#619) (SF-Zhou <>) + * [[`e9428ba`](http://github.com/eggjs/egg/commit/e9428ba95fcd07ba255359a968dd027932ce2f77)] - docs: improve left padding when window between 1005 and 1130 (#617) (Haoliang Gao <>) + * [[`c22e005`](http://github.com/eggjs/egg/commit/c22e0055ca8df35c1aa9d7d6ed7e31c21dd4b547)] - docs: turn off safe write in Jetbrains softwares (#614) (Shawn <>) + * [[`2296b7b`](http://github.com/eggjs/egg/commit/2296b7b22cc3e240bb676444d4fd2f953338cea5)] - docs: fix document deploy (#609) (Haoliang Gao <>) + +## 2017-03-21, Version 1.0.0, @popomore + +Release the first stable version :egg: :clap::clap::clap: + +### Commits + + * [[`a3ad38d`](http://github.com/eggjs/egg/commit/a3ad38d649ff8eb0cd6dfcbe338466f1c59afef3)] - docs: fix HttpClient link in docs (#599) (Luobo Zhang <>) + * [[`242a4a1`](http://github.com/eggjs/egg/commit/242a4a1fbecfc4efa37cca58d1861040dd5838bd)] - docs: fix session's maxage (#598) (Yiyu He <>) + * [[`ee77e5c`](http://github.com/eggjs/egg/commit/ee77e5cdcb444f86bf9f50bfd89a63dd9321449f)] - docs: fix some typo (#597) (kyle <>) + * [[`984d732`](http://github.com/eggjs/egg/commit/984d7320881adf9420e5c7e49d62d5530ad887dd)] - refactor: app.cluster auto bind this (#570) (zōng yǔ <>) + * [[`4687f0f`](http://github.com/eggjs/egg/commit/4687f0f47566373938f9f928ac1dc4fa62590f4d)] - docs: fix session link (#595) (TZ | 天猪 <>) + * [[`3849c1c`](http://github.com/eggjs/egg/commit/3849c1c4b8f0354b12fd17bb884c33ef9e115e3c)] - docs: fix typo of httpclient & unittest (#591) (kyle <>) + * [[`871aa82`](http://github.com/eggjs/egg/commit/871aa82d28eeb026de6633cafbe168cca8ad3182)] - docs: add gitter & more controller ctx style (#585) (TZ | 天猪 <>) + * [[`a172960`](http://github.com/eggjs/egg/commit/a1729604959af84878dddb2776d621ee01c2d447)] - docs: typo (kyle <>) + * [[`54c10bc`](http://github.com/eggjs/egg/commit/54c10bc085b380f4f003d2f7987c205264dde1ad)] - docs: change controller showcase style to ctx (#568) (TZ | 天猪 <>) + * [[`d131f23`](http://github.com/eggjs/egg/commit/d131f236111981d7fb7021998bed200a46a4603d)] - docs: fix typo in docs (#563) (Jason Lee <>) + * [[`497b9a9`](http://github.com/eggjs/egg/commit/497b9a9e7c5cdcb0b769691ea40a74a4d284cfff)] - docs(faq): fix cluster link (#557) (Mars Wong <>) + * [[`0d37e42`](http://github.com/eggjs/egg/commit/0d37e42259647ce9cb43deeba7a887817c7ef408)] - docs: update the style for search (#558) (TZ | 天猪 <>) + * [[`24ef44f`](http://github.com/eggjs/egg/commit/24ef44fa662392c7b80dbba8da0c4d5a7c9b83dd)] - docs: fix typo (#565) (Colin Cheng <>) + * [[`9eecf7b`](http://github.com/eggjs/egg/commit/9eecf7b0f928fc33d47e93782c79289ca2a13289)] - docs: rule for transforming filepath to properties (#547) (Haoliang Gao <>) + * [[`d088283`](http://github.com/eggjs/egg/commit/d0882837c34a8b950a11e4f8fe4f47f29d8823f7)] - feat: show warning message with call stack (#549) (fengmk2 <>) + * [[`4a89c3b`](http://github.com/eggjs/egg/commit/4a89c3b563ef79f5ad557ef741c16f283c11e835)] - docs: replace customEgg to framework (#545) (fengmk2 <>) + * [[`c1464fb`](http://github.com/eggjs/egg/commit/c1464fbecb27caa0dc6766147d3b13d790466386)] - docs: more detail for mysql dynamic create (#540) (TZ | 天猪 <>) + +1.0.0-rc.3 / 2017-03-10 +======================= + + * docs: fix doc scroll bug (#532) + * test: fix development test (#546) + * doc: add Algolia docsearch (#542) + * feat: [BREAKING_CHANGE] override array when load config (#522) + * docs: fix cookie example (#533) + * feat: ignore types when dump (#518) + * docs: rotate csrf token (#520) + * refactor: [BREAKING CHANGE] remove userservice and userrole (#527) + * refactor: [BREAKING_CHANGE] remove default validate plugin (#526) + * docs: fix doc build (#524) + * docs: fix middleware typo (#519) + * docs(quickstart): fix keys again (#515) + * docs(quickstart): fix keys (#511) + * docs: add cookie and session (#510) + * docs: fix html closing tag in quickstart (#512) + * docs: quickstart tip (#502) + * docs: add English version of `egg and koa` (#490) + * feat: remove default customEgg (#487) + * doc: add the view config for the egg-view-nunjucks (#496) + * test: add qs security test cases (#491) + * docs: remove meaningless word (#488) + +1.0.0-rc.2 / 2017-03-01 +======================= + + * deps: upgrade egg-session@2 to support external session store (#480) + * docs: fix view plugin config at quickstart (#482) + * docs: update document for view that using egg-view (#475) + * docs: add config merge to faq (#478) + * docs(doc): add english version of "what is egg" (#462) + * docs: fix deployment link (#473) + * docs: add document for deployment (#448) + * test: travis test on node 8 using nightly building (#464) + * docs: seperate cluster-and-ipc and cluster-client (#441) + * docs: fixed typos 'BS' (#461) + * docs: fixed spelling mistake (#460) + * test: disable error log to stderr (#453) + * docs: fix async-function demo link (#457) + * feat: throw if config.keys not exists when access app.keys (#443) + * docs: add year to licence && mysql docs (#447) + * feat: extend runInBackground on application (#442) + +1.0.0-rc.1 / 2017-02-23 +======================= + + * feat: [BREAKING_CHANGE] reimplement view, use egg-view plugin (#402) + * fix: listen CookieLimitExceed in app (#429) + * fix: close gracefully (#419) + * docs: correct spelling mistake (#424) + * feat: log error when cookie value's length exceed the limit (#418) + * docs: Update mysql.md (#422) + * docs: add more complete example code for quickstart (#412) + * fix: deprecate warning when inspect & toJSON (#408) + * docs: should listen egg-ready using messenger (#406) + * docs: correct english description at README (#400) + * docs: fix character type error and link reference error (#396) + * docs: add csrf to faq (#393) + * fix: keep unhandledRejectionError err object stack (#390) + * docs: use compress replace bodyparser for example (#391) + * docs: add directory structure (#383) + * docs: add api-doc (#369) + * docs: how to use koa's middleware (#386) + * feat: dump config both after loaded and ready (#377) + * docs: fix filename in config.md (#376) + * docs: add plugin dep name description (#374) + * docs: update version automatically (#367) + * doc: add pm2 faq (#370) + * docs: fix jsonp config in controller.md (#372) + * feat: [BREAKING_CHANGE] remove notfound.enableRedirect (#368) + * docs: add resource page (#364) + * docs: add config result description (#365) + * deps: upgrade egg-mock (#362) + * docs: english wip description & remove unuse file (#361) + * docs: add tutorials index & fix async (#359) + +0.12.0 / 2017-02-12 +=================== + + * docs: fix async link (#357) + * docs: add async await (#349) + * docs: typo Github > GitHub (#356) + * docs: update site style (#340) + * deps: upgrade egg-core (#350) + * docs: add description to config/env file (#348) + * docs: add APIClient concept to cluster doc (#344) + * test: add async test case (#339) + * feat: view base promise to support async function (#343) + * feat: curl return promise (#342) + * test: add class style controller tests (#336) + * docs: add cnzz (#335) + * test: improve coverage to 100% (#333) + * docs: update egg-and-koa with async function (#334) + * fix: remove tair and hsf (#332) + * docs: quickstart - use controller class (#329) + +0.11.0 / 2017-02-07 +=================== + + * feat: remove overrideMethod middleware (#324) + * feat: remove worker client, use app.cluster (#282) + * chore(scripts): Add PATH to find hexo (#327) + * docs: fix quickstart example code (#326) + * chore(scripts): deploy document by travis (#325) + * docs: add httpclient tracer demo and docs (#313) + * feat: close cluster clients before app close (#310) + * test: mv benchmark to eggjs/benchmark (#320) + * docs: document for plugin.{env}.js and the reason of plugin name (#321) + * docs: add sigleton in plugin.md (#316) + * docs: plugin and framework list use github tags (#318) + * docs: remove outdated docs (#319) + * docs: controller support class and refactor jsonp (#314) + * docs: add more details about csrf (#315) + +0.10.0 / 2017-02-03 +=================== + + * feat: remove tracer (#311) + * refactor: use app.beforeClose (#306) + * feat: move ctx.runtime to egg-instrument (#302) + * feat: merge the api of application/agent from extend to instance (#294) + * docs: add egg-security config to router docs (#303) + * style: fix code style for app and config (#300) + * refactor: remove ctx.jsonp and add egg-jsonp plugin (#299) + * docs: fix typo $app to app (#297) + * docs: remove inner links (#298) + +0.9.0 / 2017-01-22 +================== + + * feat: remove isAjax (#295) + * test: fix cookie test cases (#296) + * docs: adjust some words (#291) + * feat: move clusterPort to egg-cluster (#281) + * feat: move app.Service egg-core (#279) + * docs: change egg-bin to egg-init (#284) + * docs: improve framework doc based on eggjs/examples#9 (#267) + * feat: remove instrument (#283) + * docs: add progressive link && adjust en docs directory (#275) + * docs: add progressive usage (#268) + +0.8.0 / 2017-01-18 +================== + + * test: dep -> dependencies (#270) + * docs: translate zh-cn/basics/app-start.md into english (#222) + * docs: fix quickstart typo (#266) + * docs: add http client debug docs (#265) + * docs: modify and fix 3 points (#264) + * docs(intro): improve decription (#263) + * docs: fix docs site version (#262) + * docs: Fix typo. (#261) + * docs: review 1st version docs (#257) + * fix: typo conext -> context (#259) + * docs: contributing && readme && deps (#253) + * docs: fix quickstart link in index.html (#256) + * docs: set the default locale zh-cn (#255) + * refactor: ctx.realStatus delegate ctx.response.realStatus (#252) + * docs: Add intro/index.md (#246) + * feat: adjust default plugins (#251) + * docs: add RESTful documents (#247) + * feat: delegate ctx.jsonp to ctx.response.jsonp (#248) + * chore: remove examples (#245) + * docs: improve mysql doc + * docs: add mysql doc + * docs: view (#228) + * docs: improve doc theme (#230) + * docs: add core/unittest.md (#199) + * docs: add advanced/framework.md (#225) + +0.7.0 / 2017-01-12 +================== + + * docs: add service doc (#221) + * docs: serverEnv => env (#239) + * feat: delegate configurations in app (#233) + * refactor: remove ctx.getCookie, ctx.setCookie and ctx.deleteCookie (#240) + * docs: remove mon-printable character (#242) + * feat: support app.config.proxy to identify app is behind a proxy (#231) + * doc: add plugin doc (#224) + * docs: add Quick Start in English (#223) + * docs: add basics/controller.md (#209) + * docs: add core/development.md (#214) + * docs: remove init.js from document, use app.beforeStart (#229) + * docs: quickstart (#217) + * docs: add security plugin doc (#196) + * docs: mv cluster.md to zh-cn (#216) + * feat: add cluster-client (#191) + * docs: add basics/router.md (#203) + * docs: add advanced/loader.md (#198) + * docs: fix i18n doc (#210) + * docs: add core/i18n.md (#208) + * docs: add core/httpclient document (#197) + * docs: typo (#207) + * docs: add core/logger.md (#204) + * docs: add one more reason why not use koa 2 (#206) + * docs: add error handling (#205) + * docs: add schedule (#202) + * docs: add english translation of basics/env.md + * docs: basics/middleware (#194) + * docs: add basics/config.md (#188) + * doc: app start (#193) + * docs: rename koa.md to egg-and-koa.md (#190) + * docs: egg and koa (#179) + * doc: add basics/env.md (#178) + * doc: rename guide/basics/extend.md to basics/extend.md (#189) + * doc: guide/basics/extend doc (#187) + +0.6.3 / 2016-12-30 +================== + + * refactor: use logger.close, .end is deprecated (#171) + +0.6.2 / 2016-12-22 +================== + + * refactor(config): set keepAliveTimeout 4000ms by default (#165) + +0.6.1 / 2016-12-21 +================== + + * refactor: use sendToApp/sendToAgent in worker client + * fix: protocolHeaders can split with whitespace (#164) + * deps: update version (#157) + +0.6.0 / 2016-12-03 +================== + + * deps: egg-cookies@2 (#155) + * fix: already supported in egg-core (#154) + * feat: body parser support disable, ignore and match (#150) + * feat: use appInfo.root in config (#147) + * test: refactor workclient test cases (#145) + * feat: add a dns cache httpclient (#146) + +0.5.0 / 2016-11-04 +================== + + * deps: upgrade dependencies (#144) + * feat: warn when agent send message before started (#143) + * feat: [BREAKING_CHANGE] refactor Messenger (#141) + * feat: print error to console on unittest env (#139) + * feat: add ip setter on request (#138) + * feat: add getLogger to app and ctx (#136) + * test: remove co-sleep deps + * test: add local server for curl test cases + * test: use fs read instead of curl test on runInBackground + +0.4.0 / 2016-10-29 +================== + + * deps: update version (#135) + * feat: support background task on ctx (#119) + * chore: add middleware example (#121) + +0.3.0 / 2016-10-28 +================== + + * test: fix unstable test (#133) + * feat: close return promise (#128) + * deps: update deps version (#113) + * fix: AppWorkerClient subscribe same data failed issue (#110) + +0.2.1 / 2016-09-16 +================== + + * feat(application): emit startTimeout event (#107) + * perf: get header using lower case (#106) + * chore: remove --fix for error check but not fix (#101) + * doc: Add Installation (#95) + * doc: add title (#94) + +0.2.0 / 2016-09-03 +================== + + * docs: improve documents + * test: update benchmark scripts (#79) + * test: add router for bench cases (#78) + * fix: set header use lowercase (#76) + * test: add toa benchmark (#75) + * test: add benchmark results (#74) + * test: fix security tests (#73) + * test: egg-view-nunjucks change views -> view (#72) + +0.1.3 / 2016-08-31 +================== + + * fix: utils.assign support undefined (#71) + * refactor: change accept to getter (#68) + +0.1.2 / 2016-08-31 +================== + + * deps: egg-security@1 (#67) + * Revert raw header (#65) + * feat: [BREAKING_CHANGE] remove poweredBy && config.core (#63) + +0.1.1 / 2016-08-29 +================== + + * refactor: use ctx.setRawHeader (#61) + * chore: add benchmarks (#62) + * fix(meta): remove server-id (#56) + * feat(response): add res.setRawHeader (#60) + * refator: use utils.assign instead of Object.assign (#59) + * feat: docs structure (#55) + * docs: web.md and web.zh_CN.md (#54) + +0.1.0 / 2016-08-18 +================== + + * feat: [BREAKING_CHANGE] use egg-core (#44) + * doc: translate to EN (#25) + * fix: Error of no such file or directory, scandir '/restful_api/app/api' (#42) + * test: fix default plugins test (#37) + * feat: add inner plugins (#24) + * docs: add schedule example (#30) + +0.0.5 / 2016-07-20 +================== + + * refactor(core): let ctx.cookies become a getter (#22) + * fix(messenger): init when create app and agent (#21) + * test: add test codes (#20) + +0.0.1 / 2016-07-13 +================== + + * init version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebfcd83aff..60c879bfa4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,28 @@ +English | [简体中文](./CONTRIBUTING.zh-CN.md) + # Contribution Guide If you have any comment or advice, please report your [issue](https://github.com/eggjs/egg/issues), -or make any change as you wish and submit an [PR](https://github.com/eggjs/egg/issues). +or make any change as you wish and submit a [PR](https://github.com/eggjs/egg/pulls). ## Reporting New Issues - Please specify what kind of issue it is. - Before you report an issue, please search for related issues. Make sure you are not going to open a duplicate issue. -- Explain your purpose clearly in tags(see __Useful Tags__), title, or content. +- Explain your purpose clearly in tags(see **Useful Tags**), title, or content. -Egg group members will confirm the purpose of the issue, replace more accurate tags for it, identify related milestone, and assign developers working on it. +Egg group members will confirm the purpose of the issue, replace more accurate tags for it, identify related milestone, and assign developers working on it. Tags can be divided into two groups, `type` and `scope`. - type: What kind of issue, e.g. `feature`, `bug`, `documentation`, `performance`, `support` ... -- scope: What did you modified. Which files are modified, e.g. `core: xx`, `plugin: xx`, `deps: xx` +- scope: What did you modified. Which files are modified, e.g. `core: xx`, `plugin: xx`, `deps: xx` + ### Useful Tags -- `support`: the issue asks helps from developers of our group. If you need helps to locate and handle problems or have any idea to improve Egg, mark it as `support`. +- `support`: the issue asks helps from developers of our group. If you need helps to locate and handle problems or have any idea to improve Egg, mark it as `support`. - `bug`: if you find a problem which possiblly could be a bug, please tag it as `bug`. Then our group members will review that issue. If it is confirmed as a bug by our group member, this issue will be tagged as `confirmed`. - A confirmed bug will be resolved prior. - - If the bug has negative impact on running online application, it will be tagged as `ciritical`, which refers to top priority, and will be fixed ASAP! + - If the bug has negative impact on running online application, it will be tagged as `critical`, which refers to top priority, and will be fixed ASAP! - A bug will be fixed from lowest necessary version, e.g. A bug needs to be fixed from 0.9.x, then this issue will be tagged as `0.9`, `0.10`, `1.0`, `1.1`, referring that the bug is required to be fixed in those versions. - `core: xx`: the issue is related to core, e.g. `core: loader` refers that the issue is related with `loader` config. - `plugin: xx`: the issue is related to plugins. e.g. `plugin: session` refers that the issue is related to `session` plugin. @@ -31,36 +34,59 @@ Tags can be divided into two groups, `type` and `scope`. All features must be submitted along with documentations. The documentations should satify several requirements. - Documentations must clarify one or more aspects of the feature, depending on the nature of feature: what it is, why it happens and how it works. -- It's better to include a series of procedues to explain how to fix the problem. You are also encourgaed to provide **simple, but self-explanatory** demo. -All demos should be compiled at [egg/examples](https://github.com/eggjs/examples) repository. +- It's better to include a series of procedues to explain how to fix the problem. You are also encourgaed to provide **simple, but self-explanatory** demo. +All demos should be compiled at [eggjs/examples](https://github.com/eggjs/examples) repository. - Please provide essential urls, such as application process, terminology explainations and references. -## Submitting Code +## Pulling and Submitting Code + +### Pulling Code + +Please click the "Fork" button in the main page of [Egg](https://github.com/eggjs/egg) to +fork the latest code into your own repository. Then clone yours to your local machine with +the help of [git](https://git-scm.com/download/) and work on that. + +### Install Dependencies + +You can install all the dependencies listed in `package.json` with `npm`: + +```bash +npm i +``` + +If there's something wrong related to dependencies happening during the installation, +you can temporarily solve it by adding `--legacy-peer-deps` when your npm version >= 7.X: + +```bash +npm i --legacy-peer-deps +``` + +Then you can submit a PR directly in the "Issues" list to notify the author in time. ### Pull Request Guide -If you are developer of egg repo and you are willing to contribute, feel free to create a new branch, finish your modification and submit a PR. Egg group will review your work and merge it to master branch. +If you are a developer of egg repo and you are willing to contribute, feel free to create a new branch, finish your modification and submit a PR. Egg group will review your work and merge it to master branch. ```bash -// Create a new branch for development. The name of branch should be semantic, avoiding words like 'update' or 'tmp'. We suggest to use feature/xxx, if the modification is about to implement a new feature. +# Create a new branch for development. The name of branch should be semantic, avoiding words like 'update' or 'tmp'. We suggest to use feature/xxx, if the modification is about to implement a new feature. $ git checkout -b branch-name -// Run the test after you finish your modification. Add new test cases or change old ones if you feel necessary +# Run the test after you finish your modification. Add new test cases or change old ones if you feel necessary $ npm test -// If your modification pass the tests, congradulations it's time to push your work back to us. Notice that the commit message should be wirtten in the following format. -$ git add . // git add -u to delete files +# If your modification pass the tests, congradulations it's time to push your work back to us. Notice that the commit message should be wirtten in the following format. +$ git add . # git add -u to delete files $ git commit -m "fix(role): role.use must xxx" $ git push origin branch-name ``` -Then you can create a Pull Request at [egg](https://github.com/eggjs/egg/pulls) +Then you can create a Pull Request at [egg](https://github.com/eggjs/egg/pulls) No one can garantee how much will be remembered about certain PR after some time. To make sure we can easily recap what happened previously, please provide the following information in your PR. 1. Need: What function you want to achieve (Generally, please point out which issue is related). -2. Updating Reason: Different with issue. Briefly describe your reason and logic about why you need to make such modification. -3. Related Testing: Briefly descirbe what part of testing is relevant to your modification. +2. Updating Reason: Different with issue. Briefly describe your reason and logic about why you need to make such modification. +3. Related Testing: Briefly descirbe what part of testing is relevant to your modification. 4. User Tips: Notice for Egg users. You can skip this part, if the PR is not about update in API or potential compatibility problem. ### Style Guide @@ -69,7 +95,7 @@ Eslint can help to identify styling issues that may exist in your code. Your cod ### Commit Message Format -You are encouraged to use [angular commit-message-format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) to write commit message. In this way, we could have a more trackable history and an automatically generated changelog. +You are encouraged to use [angular commit-message-format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) to write commit message. In this way, we could have a more trackable history and an automatically generated changelog. ```xml (): @@ -107,9 +133,9 @@ Feel free to add more content in the body, if you think subject is not self-expl (5)footer -- ___If the commit is a Breaking Change, please note it clearly in this part.___ +- **If the commit is a Breaking Change, please note it clearly in this part.** - related issues, like `Closes #1, Closes #2, #3` -- If there is a change about an old feaure or a new feature, please associate `doc` and `egg-init`, like `eggjs/egg-bin#123` +- If there is a change about an old feaure or a new feature, please associate `doc` and `egg-core`, like `eggjs/egg-core#123` e.g. @@ -129,17 +155,49 @@ BREAKING CHANGE: Breaks foo.bar api, foo.baz should be used instead ``` -Look at [these files](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit) for more detials. +Look at [these files](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit) for more details. + +### Principles of English Translations + +We follow the normal principles of English articles when translating, however, due to there're some special principles of titles, we should follow these rules: + +- For nouns, verbs, pronouns, adjectives and adverbs, capitalize the first character. For prepsitions, articles, conjections, interjections and auxiliary words, the first character should be in lowercase. **the character of the first word and the last word for title should be capitalized, regardless of what it is**. +- For proper nouns such as the direct reference of a variable or the name of a plugin, we must use backtick (underneath the 'Esc') to surround them and keep what they are in origin. +- For prepsitions more than 5 characters, their first characters should be also capitalized, otherwise not. +- For some very important titles or some fixed proper nouns such as methods of Http: POST,GET,PUT,DELETE, every charater can be capitalized (USE WITH CAUTION). +- If the article belongs to the form of O-V (Object-Verb) such as "Config Management", we'd better translate it as "Management Configuration", or "Managing Configuration" in the form of "gerund+noun". +- If your title is taken as a sentence, write in 'Sentence Case' (e.g: In FAQ, each title is actually an English sentence). -## Release +For more info, please refer [English Title Case]. -egg uses semantic versioning in release process based on [semver]. +### Preview the generated documents + +If you have changed any file under the "docs" inside "site" folder, you need to regenerate the documents to see the real effect. + +If you are using Node version between 14 and 16, please use the following command: + +```bash +$ npm run site:devWithNode14-16 +``` + +Otherwises please use: + +```bash +$ npm run site:dev +``` + +Node.js won't work properly after 17.X for the OpenSSL problem, you have to downgrade the version of it as a solution. +If you just want to build the documents, use `site:build` instead. + +## Release Management + +Egg uses semantic versioning in release process based on [semver]. ### Branch Strategy `master` branch is the latest stable version. `next` branch is the next stable version working in progress. -- All new features will be added into `master` or `next` branch as well as all bug-fix except security issues. In such way, we can motivate developers to update to the latest stable version. +- All new features will be added into `master` or `next` branch as well as all bug-fix except security issues. In such way, we can motivate developers to update to the latest stable version. - If any API is discarded, it should be noted with `deprecate` in current stable version. The old version of API should be compatiable until the release of next stable version. - `master` branch doesn't have publish tag. High-level framework can work with stable versions defined by semantic versioning. - `next` branch is labelled with `next` tag, high-level framework can use `egg@next` to test the in-progress version. @@ -147,7 +205,7 @@ egg uses semantic versioning in release process based on [semver]. ### Release Strategy -In the release of every stable version, there will be a PM who has the following responsibilities in different stages of the release. +In the release of every stable version, there will be a PM who has the following responsibilities in different stages of the release. #### Preparation @@ -158,14 +216,21 @@ In the release of every stable version, there will be a PM who has the following - Confirm that performance test is passed and all issues in current Milestone are either closed or can be delayed to later versions. - Open a new [Release Proposal MR], and write `History` as [node CHANGELOG]. Don't forget to correct content in documentation which is related to the releasing version. Commits can be generated automatically. - ``` + + ```bash $ npm run commits ``` + - Nominate PM for next stable version. #### During Release -All tags mentioned above refere to adding tags from npm in `package.json`. +- Back up the stable version (master) onto the branch named after the current major (e.g: `1.x`), and set the tag to `release-{v}.x` (v is the current version like `release-1.x`). +- Push the `next` branch to `master`, make it to the last stable one and remove `next` tag, change the contents corresponding to the branch in README. +- Publish the latest stable version to [npm], and notify the previous framework to be upgraded. +- Before doing `npm publish`, please read [How to deploy an npm package]. + +All tags mentioned above means the tags of npm in `package.json`. ```json "publishConfig": { @@ -173,9 +238,10 @@ All tags mentioned above refere to adding tags from npm in `package.json`. } ``` -[semver]: http://semver.org/lang/zh-CN/ -[Release proposal MR]: https://github.com/nodejs/node/pull/4181 +[semver]: https://semver.org/ +[Release Proposal MR]: https://github.com/nodejs/node/pull/4181 [node CHANGELOG]: https://github.com/nodejs/node/blob/master/CHANGELOG.md [1.x milestone]: https://github.com/eggjs/egg/milestone/1 -[alinpm]: http://web.npm.alibaba-inc.com/ -[『我是如何发布一个 npm 包的』]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package +[npm]: http://npmjs.com/ +[How to deploy an npm package]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package +[English Title Case]: https://headlinecapitalization.com/ diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index fa746783ef..7f13de5d60 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -1,13 +1,15 @@ +[English](./CONTRIBUTING.md) | 简体中文 + # 代码贡献规范 有任何疑问,欢迎提交 [issue](https://github.com/eggjs/egg/issues), -或者直接修改提交 [MR](https://github.com/eggjs/egg/issues)! +或者直接修改提交 [PR](https://github.com/eggjs/egg/pulls)! ## 提交 issue - 请确定 issue 的类型。 - 请避免提交重复的 issue,在提交之前搜索现有的 issue。 -- 在标签(分类参考__标签分类__), 标题 或者内容中体现明确的意图。 +- 在标签(分类参考**标签分类**), 标题 或者内容中体现明确的意图。 随后 egg 负责人会确认 issue 意图,更新合适的标签,关联 milestone,指派开发者。 @@ -36,31 +38,51 @@ - 必须说清楚问题的几个方面:what(是什么),why(为什么),how(怎么做),可根据问题的特性有所侧重。 - how 部分必须包含详尽完整的操作步骤,必要时附上 **足够简单,可运行** 的范例代码, -所有范例代码放在 [egg/examples](https://github.com/eggjs/examples) 库中。 +所有范例代码放在 [eggjs/examples](https://github.com/eggjs/examples) 库中。 - 提供必要的链接,如申请流程,术语解释和参考文档等。 +- 同步修改中英文文档,或者在 PR 里面说明。 + +## 下拉与提交代码 + +### 下拉代码 + +请现在 GitHub 上点击 [Egg 项目](https://github.com/eggjs/egg)的“Fork”按钮,将 Egg 项目克隆到自己的仓库中,然后借助 [git](https://git-scm.com/download/) 将代码克隆到本地,以后的开发都在本地进行。 + +### 安装依赖 + +你可使用 Node 自带的 `npm` 包管理工具命令安装所有在“package.json”上的必备依赖: + +```bash +npm i +``` + +请注意: 如你安装过程中看到依赖性相关的错误,而导致安装失败,且你的 npm 版本 >=7.X,临时 +解决方案是加上 `--legacy-peer-deps`: + +```bash +npm i --legacy-peer-deps +``` -## 提交代码 +然后请及时在 Issues 里边提 PR,告知开发者。 ### 提交 Pull Request -如果你有仓库的开发者权限,而且希望贡献代码,那么你可以创建分支修改代码提交 MR,egg 开发团队会 review 代码合并到主干。 +如果你有仓库的开发者权限,而且希望贡献代码,那么你可以创建分支修改代码提交 PR,egg 开发团队会 review 代码合并到主干。 ```bash -// 先创建开发分支开发,分支名应该有含义,避免使用 update、tmp 之类的 +# 先创建开发分支开发,分支名应该有含义,避免使用 update、tmp 之类的 $ git checkout -b branch-name -// 开发完成后跑下测试是否通过,必要时需要新增或修改测试用例 -$ tnpm test +# 开发完成后跑下测试是否通过,必要时需要新增或修改测试用例 +$ npm test -// 测试通过后,提交代码,message 见下面的规范 +# 测试通过后,提交代码,message 见下面的规范 -$ git add . // git add -u 删除文件 +$ git add . # git add -u 删除文件 $ git commit -m "fix(role): role.use must xxx" $ git push origin branch-name ``` -提交后就可以在 [egg](https://github.com/eggjs/egg/pulls) 创建 Pull Request 了。 - 由于谁也无法保证过了多久之后还记得多少,为了后期回溯历史的方便,请在提交 MR 时确保提供了以下信息。 1. 需求点(一般关联 issue 或者注释都算) @@ -113,9 +135,9 @@ $ git push origin branch-name (5)footer -- ___当有非兼容修改(Breaking Change)时必须在这里描述清楚___ +- **当有非兼容修改(Breaking Change)时必须在这里描述清楚** - 关联相关 issue,如 `Closes #1, Closes #2, #3` -- 如果功能点有新增或修改的,还需要关联文档 `doc` 和 `egg-init` 的 PR,如 `eggjs/egg-bin#123` +- 如果功能点有新增或修改的,还需要关联文档 `doc` 和 `egg-core` 的 PR,如 `eggjs/egg-core#123` 示例 @@ -135,7 +157,39 @@ BREAKING CHANGE: Breaks foo.bar api, foo.baz should be used instead ``` -查看具体[文档](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit) +详情请查看具体[文档](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)。 + +### 英语翻译规范 + +英语正文按照一般英语语法规律书写即可,但标题比较特殊,应该按照以下规范进行书写: + +- 名词、动词、代词、形容词、副词等首字母大写,介词、冠词、连词、感叹词和助词首字母小写,*标题第一个单词、最后一个单词无论词性首字母应该大写*。 +- 专有名词(如直接引用某个变量,或者某个插件名称等),必须使用反单引号(键盘上 Esc 正下方)进行引用,并保持原来大小写。 +- 超过5个字母的介词首字母应该大写,否则一律小写。 +- 如果是重要提示性标题,或者是专有名称标题(例如 Http 请求方法:GET,POST,PUT,DELETE),可以全部字母都用大写(慎重考虑)。 +- 如果标题属于“动宾”性质的短语(如“配置管理”),尽量翻译成“宾+动词名词”的形式(Configuration Management),或者是“动名词+宾语”的形式(Managing Configuration)。 +- 如果标题被当做一个完整的英语句子,请按照英语句子的语法格式大小写(如:常见问题 FAQ 中每一个标题都是一个英语句子)。 + +有关详情,可以参考[英语标题大小写]。 + +### 预览已生成的文档 + +如果你修改了 site 文件夹下的 docs 中的某个 md 文件内容,需要重新生成文档,然后才能看到效果。 + +如果你使用的 Node 版本在 14——16 之间,请使用如下命令: + +```bash +$ npm run site:devWithNode14-16 +``` + +否则请使用此命令: + +```bash +$ npm run site:dev +``` + +这是因为 Node.js 在 17.X 之后编译文档存在兼容性问题,因此必须降级“OpenSSL”方可正常打包编译。 +如仅仅是编译打包生成文档,使用 `site:build` 相关命令即可。 ## 发布管理 @@ -143,7 +197,7 @@ egg 基于 [semver] 语义化版本号进行发布。 ### 分支策略 -`master` 分支为当前稳定发布的版本,`next` 分支为下一个开发中的大版本。 +`master` 分支为当前稳定发布的版本,`next` 分支为下一个开发中的大版本。 - 只维护两个版本,除非有安全问题,否则修复只会 patch 到 `master` 和 `next` 分支,其他更新推动上层框架升级到稳定大版本的最新版本。 - 所有 API 的废弃都需要在当前的稳定版本上 `deprecate` 提示,并保证在当前的稳定版本上一直兼容到新版本的发布。 @@ -157,24 +211,26 @@ egg 基于 [semver] 语义化版本号进行发布。 #### 准备工作: -- 建立 milestone,确认需求关联 milestone,指派和更新 issues,如 [1.x milestone]。 +- 建立发布里程碑,确认需求关联它,指派和更新已知问题,如 [1.x 发布里程碑]。 - 从 `master` 分支新建 `next` 分支,并设置 tag 为 `next`。 #### 发布前: -- 确认当前 Milestone 所有的 issue 都已关闭或可延期,完成性能测试。 -- 发起一个新的 [Release Proposal MR],按照 [node CHANGELOG] 进行 `History` 的编写,修正文档中与版本相关的内容,commits 可以自动生成。 - ``` +- 确认当前发布里程碑所有的已知问题都已关闭或可延期,完成性能测试。 +- 发起一个新的 [发布合并请求],按照 [node 变更日志] 进行 `History` 的编写,修正文档中与版本相关的内容,commits 可以自动生成: + + ```bash $ npm run commits ``` + - 指定下一个大版本的 PM。 #### 发布时: -- 将老的稳定版本(master)备份到以当前大版本为名字的分支上(例如 `1.x`),并设置 tag 为 `release-{v}.x`( v 为当前版本,例如 `release-1.x`)。 -- 将 `next` 分支推送到 `master`,成为新的稳定版本分支,并去除 `next` tag,修改 README 中与分支相关的内容(CISE task id)。 -- 发布新的稳定版本到 [alinpm],并通知上层框架进行更新。 -- `tnpm publish` 之前,请先阅读[『我是如何发布一个 npm 包的』]。 +- 将老的稳定版本(master)备份到以当前大版本为名字的分支上(例如 `1.x`),并设置 tag 为 `release-{v}.x`( v 为当前版本,例如 `release-1.x`)。 +- 将 `next` 分支推送到 `master`,成为新的稳定版本分支,并去除 `next` tag,修改 README 中与分支相关的内容。 +- 发布新的稳定版本到 [npm],并通知上层框架进行更新。 +- `npm publish` 之前,请先阅读 [我是如何发布一个 npm 包的]。 上述描述中所有的设置 tag 都是指在 `package.json` 中设置 npm 的 tag。 @@ -184,9 +240,10 @@ egg 基于 [semver] 语义化版本号进行发布。 } ``` -[semver]: http://semver.org/lang/zh-CN/ -[Release proposal MR]: https://github.com/nodejs/node/pull/4181 -[node CHANGELOG]: https://github.com/nodejs/node/blob/master/CHANGELOG.md -[1.x milestone]: https://github.com/eggjs/egg/milestone/1 -[alinpm]: http://web.npm.alibaba-inc.com/ -[『我是如何发布一个 npm 包的』]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package +[semver]: https://semver.org/lang/zh-CN/ +[发布合并请求]: https://github.com/nodejs/node/pull/4181 +[node 变更日志]: https://github.com/nodejs/node/blob/master/CHANGELOG.md +[1.x 发布里程碑]: https://github.com/eggjs/egg/milestone/1 +[npm]: http://npmjs.com/ +[我是如何发布一个 npm 包的]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package +[英语标题大小写]: https://headlinecapitalization.com/ diff --git a/History.md b/History.md index b863b55512..861881bbf9 100644 --- a/History.md +++ b/History.md @@ -1,67 +1,52 @@ -0.2.1 / 2016-09-16 +3.29.0 / 2024-11-30 ================== - * feat(application): emit startTimeout event (#107) - * perf: get header using lower case (#106) - * chore: remove --fix for error check but not fix (#101) - * doc: Add Installation (#95) - * doc: add title (#94) +**features** + * [[`e4f706990`](http://github.com/eggjs/egg/commit/e4f7069904b99c842e65692f2531a72543719207)] - feat: use urllib@4.5.0 (#5371) (fengmk2 <>) -0.2.0 / 2016-09-03 +3.28.0 / 2024-09-16 ================== - * docs: improve documents - * test: update benchmark scripts (#79) - * test: add router for bench cases (#78) - * fix: set header use lowercase (#76) - * test: add toa benchmark (#75) - * test: add benchmark results (#74) - * test: fix security tests (#73) - * test: egg-view-nunjucks change views -> view (#72) +**features** + * [[`46d3fb222`](http://github.com/eggjs/egg/commit/46d3fb222bcf455d2aee9a99005d18c271f29b16)] - feat: support allowH2 on urllib@4 (#5357) (fengmk2 <>) -0.1.3 / 2016-08-31 +3.27.1 / 2024-07-12 ================== - * fix: utils.assign support undefined (#71) - * refactor: change accept to getter (#68) +**fixes** + * [[`f3d8df1b`](http://github.com/eggjs/egg/commit/f3d8df1b7c2aef88cca3beec8c753656f8cc629c)] - fix: add httpclient.safeCurl typing (#5341) (killa <>) -0.1.2 / 2016-08-31 +3.27.0 / 2024-07-12 ================== - * deps: egg-security@1 (#67) - * Revert raw header (#65) - * feat: [BREAKING_CHANGE] remove poweredBy && config.core (#63) +**features** + * [[`68cbd241`](http://github.com/eggjs/egg/commit/68cbd241e2172b8018328d77ea087dd5974a580f)] - feat: impl httpclient.safeCurl (#5339) (killa <>) -0.1.1 / 2016-08-29 +3.26.1 / 2024-07-04 ================== - * refactor: use ctx.setRawHeader (#61) - * chore: add benchmarks (#62) - * fix(meta): remove server-id (#56) - * feat(response): add res.setRawHeader (#60) - * refator: use utils.assign instead of Object.assign (#59) - * feat: docs structure (#55) - * docs: web.md and web.zh_CN.md (#54) +**fixes** + * [[`872273cb`](http://github.com/eggjs/egg/commit/872273cb23cd6f5d76b3f33528916effade31011)] - fix: xframe value type (#5336) (hongzzz <>) -0.1.0 / 2016-08-18 -================== - - * feat: [BREAKING_CHANGE] use egg-core (#44) - * doc: translate to EN (#25) - * fix: Error of no such file or directory, scandir '/restful_api/app/api' (#42) - * test: fix default plugins test (#37) - * feat: add inner plugins (#24) - * docs: add schedule example (#30) +**others** + * [[`8ae76d09`](http://github.com/eggjs/egg/commit/8ae76d09db8cf020cbdacfc64fee0750b9612136)] - docs: fix typo (#5330) (Fu Yuchen <<78291982+fyc09@users.noreply.github.com>>) -0.0.5 / 2016-07-20 +3.26.0 / 2024-07-01 ================== - * refactor(core): let ctx.cookies become a getter (#22) - * fix(messenger): init when create app and agent (#21) - * test: add test codes (#20) +**features** + * [[`b0292a8b`](http://github.com/eggjs/egg/commit/b0292a8b7e76d5dbf7441b7164c39441dbae51ec)] - feat: allow to create httpClient from app (#5334) (fengmk2 <>) -0.0.1 / 2016-07-13 +3.25.0 / 2024-06-27 ================== - * init version +**features** + * [[`ceded0b1`](http://github.com/eggjs/egg/commit/ceded0b1c9217503c5ed9226f96c493d6bd00547)] - feat: allow to httpClient use HTTP2 first (#5332) (fengmk2 <>) + +**others** + * [[`8553c3f2`](http://github.com/eggjs/egg/commit/8553c3f23e423e9f60144b11a484b703fe7c9229)] - chore: remove auto release (fengmk2 <>) + * [[`b4f01a1c`](http://github.com/eggjs/egg/commit/b4f01a1c6bf006c943c85fce334b81d61f55b7d0)] - chore: fix release branches name (fengmk2 <>) + * [[`a8073b04`](http://github.com/eggjs/egg/commit/a8073b04fc3821bb23326c6c8b4fd0ccaeb5c200)] - chore: add release config (fengmk2 <>) + * [[`8ce2ff90`](http://github.com/eggjs/egg/commit/8ce2ff90bfbb9e4580a23ea49a15fdb1c185fbb5)] - chore: add npm publish tag (fengmk2 <>) + * [[`44950ed8`](http://github.com/eggjs/egg/commit/44950ed82a3ce4d5d4b9028aee98d6650298a552)] - chore: start 3.x LTS (fengmk2 <>) diff --git a/LICENSE b/LICENSE index 8363a1e45b..7295685291 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) Alibaba Group Holding Limited and other contributors. +Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MEMBER_GUIDE.md b/MEMBER_GUIDE.md deleted file mode 100644 index 269a8cf24c..0000000000 --- a/MEMBER_GUIDE.md +++ /dev/null @@ -1 +0,0 @@ -# Member Guide diff --git a/README.md b/README.md index 9009883324..8a926ab348 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,64 @@ -![](https://raw.githubusercontent.com/eggjs/egg/master/docs/assets/egg-logo.png) +English | [简体中文](./README.zh-CN.md) -Born to build better enterprise frameworks and apps +
+ +
-[![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![Test coverage][codecov-image]][codecov-url] -[![David deps][david-image]][david-url] -[![Known Vulnerabilities][snyk-image]][snyk-url] -[![npm download][download-image]][download-url] +[![NPM version](https://img.shields.io/npm/v/egg.svg?style=flat-square)](https://npmjs.org/package/egg) +[![NPM quality](http://npm.packagequality.com/shield/egg.svg?style=flat-square)](http://packagequality.com/#?package=egg) +[![NPM download](https://img.shields.io/npm/dm/egg.svg?style=flat-square)](https://npmjs.org/package/egg) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fegg.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fegg?ref=badge_shield) -[npm-image]: https://img.shields.io/npm/v/egg.svg?style=flat-square -[npm-url]: https://npmjs.org/package/egg -[travis-image]: https://img.shields.io/travis/eggjs/egg.svg?style=flat-square -[travis-url]: https://travis-ci.org/eggjs/egg -[codecov-image]: https://codecov.io/gh/eggjs/egg/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/eggjs/egg -[david-image]: https://img.shields.io/david/eggjs/egg.svg?style=flat-square -[david-url]: https://david-dm.org/eggjs/egg -[snyk-image]: https://snyk.io/test/npm/egg/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/egg -[download-image]: https://img.shields.io/npm/dm/egg.svg?style=flat-square -[download-url]: https://npmjs.org/package/egg - -## Installation - - -```bash -$ npm install egg --save -``` - -Node.js >= 4.0.0 required, check [document for installation](https://eggjs.org/guide/installation.html). +[![Continuous Integration](https://github.com/eggjs/egg/actions/workflows/nodejs-3.x.yml/badge.svg)](https://github.com/eggjs/egg/actions?query=branch%3A3.x) +[![codecov](https://codecov.io/gh/eggjs/egg/branch/3.x/graph/badge.svg?token=2sKMCDNkcl)](https://app.codecov.io/gh/eggjs/egg/tree/3.x) +[![Known Vulnerabilities](https://snyk.io/test/npm/egg/badge.svg?style=flat-square)](https://snyk.io/test/npm/egg) +[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/eggjs?style=flat-square)](https://opencollective.com/eggjs) ## Features -- ✔︎ Build-in process management -- ✔︎ Plugin system -- ✔︎ Framework customization -- ✔︎ Lots of [plugins](https://eggjs.org/badgeboard/) - -## Docs & Community +- Built-in Process Management +- Plugin System +- Framework Customization +- Lots of [plugins](https://github.com/search?q=topic%3Aegg-plugin&type=Repositories) -- [Website](https://eggjs.org) -- [Plugin List](https://eggjs.org/badgeboard/) -- [Frameworks](https://eggjs.org/frameworks.html) +## Quickstart -## Getting Started - -Follow the step +Follow the commands listed below. ```bash -$ npm install egg-init -g -$ egg-init --type simple showcase && cd showcase +$ mkdir showcase && cd showcase +$ npm init egg --type=simple # Optionally pnpm create egg --type=simple $ npm install $ npm run dev $ open http://localhost:7001 ``` -## Examples +> Node.js >= 14.20.0 required. + +## Documentations + +- [Documentations](https://eggjs.org/en/index.html) +- [Plugins](https://github.com/search?q=topic%3Aegg-plugin&type=Repositories) +- [Frameworks](https://github.com/search?q=topic%3Aegg-framework&type=Repositories) +- [Examples](https://github.com/eggjs/examples) + +## Contributors + +[![contributors](https://contrib.rocks/image?repo=eggjs/egg&max=240&columns=26)](https://github.com/eggjs/egg/graphs/contributors) ## How to Contribute -Please let us know what we can help, check [issues](https://github.com/eggjs/egg/issues) for bug reporting and suggestion. +Please let us know how can we help. Do check out [issues](https://github.com/eggjs/egg/issues) for bug reports or suggestions first. + +To become a contributor, please follow our [contributing guide](CONTRIBUTING.md). -If you are a contributor, follow [CONTRIBUTING](CONTRIBUTING.md). +## Sponsors and Backers -If you are a member of egg, follow [MEMBER_GUIDE](MEMBER_GUIDE.md). +[![sponsors](https://opencollective.com/eggjs/tiers/sponsors.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) +[![backers](https://opencollective.com/eggjs/tiers/backers.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) ## License [MIT](LICENSE) + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fegg.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fegg?ref=badge_large) diff --git a/README.zh-CN.md b/README.zh-CN.md index be6f007b6c..dd0082caca 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,70 +1,58 @@ -![](https://raw.githubusercontent.com/eggjs/egg/master/docs/assets/egg-logo.png) - -为企业级框架和应用而生 - -[![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![Test coverage][codecov-image]][codecov-url] -[![David deps][david-image]][david-url] -[![Known Vulnerabilities][snyk-image]][snyk-url] -[![npm download][download-image]][download-url] - -[npm-image]: https://img.shields.io/npm/v/egg.svg?style=flat-square -[npm-url]: https://npmjs.org/package/egg -[travis-image]: https://img.shields.io/travis/eggjs/egg.svg?style=flat-square -[travis-url]: https://travis-ci.org/eggjs/egg -[codecov-image]: https://codecov.io/gh/eggjs/egg/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/eggjs/egg -[david-image]: https://img.shields.io/david/eggjs/egg.svg?style=flat-square -[david-url]: https://david-dm.org/eggjs/egg -[snyk-image]: https://snyk.io/test/npm/egg/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/egg -[download-image]: https://img.shields.io/npm/dm/egg.svg?style=flat-square -[download-url]: https://npmjs.org/package/egg - -## 安装 +[English](./README.md) | 简体中文 -```bash -$ npm install egg --save -``` +
+ +
-Node.js >= 4.0.0 required, check [document for installation](https://eggjs.org/guide/installation.html). +[![NPM version](https://img.shields.io/npm/v/egg.svg?style=flat-square)](https://npmjs.org/package/egg) +[![NPM quality](http://npm.packagequality.com/shield/egg.svg?style=flat-square)](http://packagequality.com/#?package=egg) +[![NPM download](https://img.shields.io/npm/dm/egg.svg?style=flat-square)](https://npmjs.org/package/egg) -## 特性 +[![Continuous Integration](https://github.com/eggjs/egg/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg/actions?query=branch%3Amaster) +[![codecov](https://codecov.io/gh/eggjs/egg/branch/3.x/graph/badge.svg?token=2sKMCDNkcl)](https://app.codecov.io/gh/eggjs/egg/tree/3.x) +[![Known Vulnerabilities](https://snyk.io/test/npm/egg/badge.svg?style=flat-square)](https://snyk.io/test/npm/egg) +[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/eggjs?style=flat-square)](https://opencollective.com/eggjs) -- ✔︎ Build-in process management -- ✔︎ Plugin system -- ✔︎ Framework customization -- ✔︎ Lots of [plugins](https://eggjs.org/badgeboard/) -) +## 特性 -## 文档和社区 +- 内置多进程管理 +- 高度可扩展的插件机制 +- 深度框架定制 +- 丰富的[插件](https://github.com/search?q=topic%3Aegg-plugin&type=Repositories) -- [Website](https://eggjs.org) -- [Plugin List](https://eggjs.org/badgeboard/) -- [Frameworks](https://eggjs.org/frameworks.html) +> 支持 Node.js 14.20.0 及以上版本。 ## 快速开始 -Follow the step - ```bash -$ npm install egg-init -g -$ egg-init --type simple showcase && cd showcase +$ mkdir showcase && cd showcase +$ npm init egg --type=simple $ npm install $ npm run dev $ open http://localhost:7001 ``` -## 示例 +## 文档 + +- [官方文档](https://eggjs.org/zh-cn/) +- [插件列表](https://github.com/search?q=topic%3Aegg-plugin&type=Repositories) +- [框架列表](https://github.com/search?q=topic%3Aegg-framework&type=Repositories) +- [官方示例](https://github.com/eggjs/examples) + +## 贡献者 + +[![contributors](https://contrib.rocks/image?repo=eggjs/egg&max=240&columns=26)](https://github.com/eggjs/egg/graphs/contributors) ## 贡献代码 -Please let us know what we can help, check [issues](https://github.com/eggjs/egg/issues) for bug reporting and suggestion. +请告知我们可以为你做些什么,不过在此之前,请检查一下是否有[已经存在的Bug或者意见](https://github.com/eggjs/egg/issues)。 + +如果你是一个代码贡献者,请参考[代码贡献规范](CONTRIBUTING.md)。 -If you are a contributor, follow [CONTRIBUTING](CONTRIBUTING.md). +## 项目赞助 -If you are a member of egg, follow [MEMBER_GUIDE](MEMBER_GUIDE.md). +[![sponsors](https://opencollective.com/eggjs/tiers/sponsors.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) +[![backers](https://opencollective.com/eggjs/tiers/backers.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) ## 开源协议 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..92c0187f62 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +These versions are currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 3.x | :white_check_mark: | +| 2.x | :white_check_mark: | +| < 2.0 | :x: | + +## Reporting a Vulnerability + +To report a security vulnerability, please do not open an issue, as this notifies attackers of the vulnerability. +Instead, please email [fengmk2](mailto:fengmk2+eggjs-security@gmail.com) to disclose. diff --git a/SPECIFICATION.md b/SPECIFICATION.md deleted file mode 100644 index af2159984e..0000000000 --- a/SPECIFICATION.md +++ /dev/null @@ -1,672 +0,0 @@ -# Base Web Framework - -Egg is an open-source web framework for building a flexible Node.js web and mobile applications. It includes a series of rules that defines the file structure of a web application, loaders, configurations, scheduler scripts, and plugins system. - - -**Glossary:** - -- Based on [koa](http://koajs.com/) -- Web application file structure and loading process - - `package.json` - - `app` (directory) - - `app/router.js` - - `app/controller` - - `app/middleware` - - `app/service` - - `app/proxy` - - `app/public` (static resources directory) - - `app.js` - - koa extension - - `test` -- Configuration file and configuration loader - - Environmental variables naming rules -- Plugins - - What is a plugin - - Opening and closing a plugin - - Naming a plugin -- Multi-process model and communication between processes - - Master & worker process - - Agent process - - Communication between multiple processes - - Robustness -- File Watching -- User object - -## Based on Koa - -`Koa` is a web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. - -**Egg** framework is built on top of `Koa` and its ecosystem. The [core contributors of **egg** framework](https://github.com/eggjs/egg/graphs/contributors) are also the core contributors of [`koa` web framework](https://github.com/koajs/koa/graphs/contributors). In addition, We are maintaining [many](https://github.com/repo-utils) [Node.js](https://github.com/node-modules) [open source](https://github.com/stream-utils) [projects](https://github.com/cojs) across the entire Node.js ecosystem. - -**Egg** framework is originated from **Alibaba** internal Node.js web framework. It is an open source version of what **Alibaba** Node.js team used. It is based on what the team have learned from maintaining production applications over the course of five years. - - -## Web Application File Structure and Loading Process - -This rule is only for the directories that are mentioned later in this section. For file directories that is not coverd, this rule is not applicable. - -**Egg** is an opinionated framework for creating ambitious Node.js web applications. Simply following the naming convention, our friendly APIs help you get your job done fast. - -Let's use an app called `helloweb` as an example. Its file structure may look like following: - -```sh -. helloweb -├── package.json -├── app.js (optional) -├── agent.js (optional) -├── app -│ ├── router.js -│ ├── controller -│ │ └── home.js -│ ├── extend (optional,for egg extention) -│ │ ├── helper.js (optional) -│ │ ├── filter.js (optional) -│ │ ├── request.js (optional) -│ │ ├── response.js (optional) -│ │ ├── context.js (optional) -│ │ ├── application.js (optional) -│ │ └── agent.js (optional) -│ ├── service (optional) -│ ├── public (optional) -│ │ ├── favicon.ico -│ │ └── ... -│ ├── middleware (optional) -│ │ └── response_time.js -│ └── view (optional, base view plugin rule, we suggest to use view) -│ ├── layout.html -│ └── home.html -├── config -│ ├── config.default.js -│ ├── config.prod.js -│ ├── config.test.js (optional) -│ ├── config.local.js (optional) -│ ├── config.unittest.js (optional) -│ ├── plugin.js -│ └── role.js (optional, we use role as an example of plugin, special config file for plugin should be placed in config directory) -└── test - ├── middleware - | └── response_time.test.js - └── controller - └── home.test.js -``` - -### `package.json` - -Like all Node.js application, it must contain a `package.json` file. It should have following attributes: - -- `name`: Application name -- `engines`: It specifies the Node.js version that the application depends on. Use `install-node` attribute to get the required version of Node, For example `4.1.1` is required. -```json -"name": "helloweb" -"engines": { - "install-node": "4.1.1" -} -``` - -### `app` directory - -`app` directory is used to store central logic of this application. - -`app` directory can include directories, such as `controller`, `public`, `middleware`, `schedule`, `apis` etc. The files that were contained in those directories would be loaded automatically by [egg-core](https://github.com/eggjs/egg-core) - -`app` directory can include files, such as `router.js`. Those files are stored at the root of `app` directory and would be loaded loaded automatically by [egg-core](https://github.com/eggjs/egg-core). - -#### `app/router.js` - -`app/router.js` contains the routing configuration for the entire application. We use [koa-router](https://github.com/alexmingoia/koa-router) middleware under the hood, so that `koa-router`'s [APIs](https://github.com/alexmingoia/koa-router#api-reference) are applied fully here. - -`router.js` file exports a function that takes a single parameter called `app`. The `app` object is an instance of the **Egg** application. On `app` object, you can use route methods, for example, `get`, `post`, `put`, `delete`, `head`, and much more, to achieve routing functionality. The route interface takes two parameters. First parameter is a string representation of the application partial URL. Second parameter is the controller function is called when the partial URL has been matched. - -Here is an example for `router.js`: - -```js -module.exports = function(app) { - app.get('/', app.controller.home); - app.post('/blog/:id/upload', app.controller.blog.upload); -}; -``` - -```js -module.exports = app => { - app.get('/', app.controller.home); - app.get('/forget', app.controller.forget); - app.post('/remember', app.controller.remember); -}; -``` - -#### `app/controller` - -Every `app/controller/*.js` file will be automatically loaded into `app.controller.*`, thanks for the loader, [egg-core](https://github.com/eggjs/egg-core) - -The following example explains how a directory is loaded: - -```js -├── app - └── controller - ├── foo_bar (automatically change name to Camel-Case, foo_bar => fooBar, foo-bar-ok => fooBarOk) - | └── user.js ==> app.controller.fooBar.user - ├── blog.js ==> app.controller.blog - └── home.js ==> app.controller.home -``` - -`controller` is a Koa (v1) middleware. Use the generator format, (star function), for example `function*([next])`; - -```js -// home.js -module.exports = function*() { - this.body = 'hello world'; -}; -``` - - -```js -// blog.js -exports.upload = function*() { - // handle file upload -}; -``` - -Generally, a HTTP request will be handled by one controller. A controller function is the last handler in the middleware chain of executing HTTP request. - -A Controller can call dependent directories, such as `service`, `proxy` etc. - -#### `app/middleware` - -All custom middlewares should be placed in this directory. The execution order of the middlewares should be declared in `config/config.${env}.js`. - -```js -// config/config.js -exports.middleware = [ - 'responseTime', - 'locals', -]; -``` - -Generally speaking, middleware is executed by every HTTP requests so that you should have a clear awareness about the order that middlewares are executed. - -For example, here is a simple middleware to calculate `response time`: - -```js -// middleware/response_time.js -module.exports = function(options, app) { - return function* (next) { - const start = Date.now(); - yield next; - const elapsed = Date.now() - start; - this.set('x-readtime', elapsed); - }; -}; -``` - -#### `app/service` - -You can use **services** to organize and share code across your application. - -Here is an base `Service` class, and you can extend it to make your own services: - -```js -// Service.js -class Service { - constructor(ctx) { - this.ctx = ctx; - this.app = ctx.app; - // So, you can use ctx and app in class extended from Service. - } - - get proxy() { - return this.ctx.proxy; - } -} - -module.exports = Service; -``` - -An example that shows `UserService` extends the base `Service`: - -```js -const Service = require('egg').Service; - -class UserService extends Service { - constructor(ctx) { - super(ctx); - this.userClient = userClient; - }, - - * get(uid) { - const ins = instrument(this.ctx, 'buc', 'get'); - const result = yield userClient.get(uid); - ins.end(); - return result; - } -} - -module.exports = UserService; -``` - -Each service is defined in `app/service/*.js` will be injected into `ctx.service`. For example, `app/service/user.js` service class can be accessed by `ctx.service.user`. Because of `ctx` is defined in application level, it can be accessed in every middlewares. - -```js -├── app - └── service - ├── foo_bar (automatically change file name into Camal-Case, foo_bar => fooBar, foo-bar-ok => fooBarOk) - | └── user.js ==> ctx.service.fooBar.user - ├── blog.js ==> ctx.service.blog - └── user.js ==> ctx.service.user -``` - -#### `app/view` - -This directory is used to store template files in scripts used in rendering client side view templates. For more detail, please see `template rendering guide` - -#### `app/public` - -This directory is used to store static resources, such as 'favicon', 'images', 'fonts', etc. - -**Egg** framework serves files in public directory at an absolute url `${domain}/public/${path-to-file}` - -```js -app/public/js/main.js => /public/js/main.js -app/public/styles/bluc.css => /public/styles/blue.css -``` - -### `app.js` - -`app.js` is responsible to do initializing work when an application starts. In general, most apps don't need this feature. - -When an application starts and uses some custom services in client-side, it may need to check the dependencies status. Those inspections can be placed in `app.js`. - -```js -// app.js -const MyClient = require('some-client'); - -module.exports = function(app) { - app.myClient = new MyClient(); - - const done = app.async('my-client-ready'); - app.myClient.ready(done); - - // listen for exception, if necessary. - app.myClient.once('error', done); -}; -``` - -### `agent.js` - -Similar to `app.js`, `agent.js` is responsible to do initializing work when an agent worker starts. - -### Koa Extension Guide - -All extensions in `extend` directory is used to extend `Koa` framework APIs. In another words, the extension is added to `Koa` application prototype. For example, `extend/application.js` extends `Application.prototype`. - -- `app/extend/request.js`: extend koa request -- `app/extend/response.js`: extend koa response -- `app/extend/context.js`: extend koa context -- `app/extend/application.js`: extend koa application -- `app/extend/agent.js`: extend agent object - -### `test` - -All unit tests and integration tests goes into this directory. Group all your tests files into the central location is convenient to run testing scripts. - -`test` directory is based on current directory structure. It should have the same structure as `app` directory, except the file name end with `*.test.js`: - -```sh -. helloweb -├── app -│ ├── controller -│ │ └── home.js -│ ├── middleware (optional) -│ │ └── response_time.js -└── test - └── controller - └── home.test.js - ├── middleware - | └── response_time.test.js -``` - -## Config Guide and Loading Process - -```js -// config/config.default.js -module.exports = { - keys: 'super secure passkey' -}; -``` - -### Running Environment Guide - -Configuration settings to run in different environments. - -| name | NODE_ENV | description | -| --- | --- | --- | -| prod | production | production environment,pre-production environment | -| test | production | system integration test environment,a.k.a sit environment | -| default | production | development server,normally every iteration will apply for a development server | -| local | development or null | local environment, developers computer, which is very likely developing multiple apps | -| unittest | test | unit test environment, such as developer's local environment and ci environment | - -### 根据环境加载配置 `config.*.js` Loading Configs Based on Environment - -- `{appname}/config/config.default.js`: default, all env will load this config -- `{appname}/config/config.prod.js`: prod env config -- `{appname}/config/config.test.js`: test env config -- `{appname}/config/config.local.js`: local env config -- `{appname}/config/config.unittest.js`: unittest env config - -#### 配置自动加载流程 Auto Loading of Configs - -Suppose the current environment is `${env}`, the final configuration will be built based on the following hierarchy. - -- Loading order: loading from inside to outside - - `core -> plugin -> app` - -- Priority: Outer config can replace inner config - - `app > plugin > core` - -The process of loading config files by loader: - -``` -egg/config/config.default.js - ${plugin}/config/config.default.js - ${app}/config/config.default.js - egg/config/config.${env}.js - ${plugin}/config/config.${env}.js - ${app}/config/config.${env}.js -``` - -## Plugins - -**Koa has the concept of middleware. Why do we still need *plugins*?** - -In short, middleware cannot satisfy the requirement in some specific situation. - -We could use diamond-client as an example. It need to be injected into applications, so it is not suitable to be a middleware. Moreover, diamond-client needs to be started before the application starts so that it requires to have some inspections to its dependencies. - -### What a plugin can do? - -A plugin is like a small application. It is an extension for application, but does not have `controller` and `router`. - -- If you need to extend koa, simply create files like `app/extend/request.js, app/extend/response.js, app/extend/context.js, app/extend/application.js`. - -- If you need to add custom middlewares, edit `app.js` and create `app/middleware/*.js`. - -For example, ensure `bodyParser` middleware is present and `static plugin` is executed before other middlewares that lists inside `app.config.appMiddleware`. - -```js -// plugins/static/app.js -const assert = require('assert'); - -module.exports = function (app) { - assert.equal(app.config.appMiddleware.indexOf('static'), -1, - 'middleware of custom plugin static has the same name as default middleware static, please set a new name, like appStatic'); - - //put static's middleware before bodyParser - const index = app.config.coreMiddleware.indexOf('bodyParser'); - assert(index >= 0, 'Must have middleware bodyParser'); - - app.config.coreMiddleware.splice(index, 0, 'static'); -}; -``` - -`app.use` will always load `app.config.coreMiddleware` before `app.config.appMiddleware`. - -- To inspect status of dependencies before starting, use `app.readyCallback(asyncName)` - -```js -// app.js -app.myClient = new MyClient(); - -// ready -app.myClient.ready(app.readyCallback('my-client-ready')); - -// event -app.myClient.once('connect', app.readyCallback('my-client-ready')); -``` - -### 定义插件 Writing a plugin - -Here is a example of plugin, whose structure is similar to app. - -```sh -. helloclient -├── package.json -├── app.js (optional) -├── agent.js (optional) -├── app -│ ├── extend (optional) -│ | ├── helper.js (optional) -│ | ├── request.js (optional) -│ | ├── response.js (optional) -│ | ├── context.js (optional) -│ | ├── application.js (optional) -│ | └── agent.js (optional) -│ ├── proxy (optional) -│ ├── service (optional) -│ └── middleware (optional) -│ └── my.js -├── config -| ├── config.default.js -│ ├── config.prod.js -| ├── config.test.js (optional) -| ├── config.local.js (optional) -| └── config.unittest.js (optional) -└── test - └── middleware - └── my.test.js -``` - -define attributes of plugin in `package.json` - -```json -{ - "name": "egg-mysql", - "eggPlugin": { - "name": "egg-mysql", - "dep": [ "configclient" ], - } -} -``` - -- {String} name - plugin name is required and should be a unique name. -- {Array} dep - dependencies for this plugin -- {Array} env - only running in designated environment - -**Note: plugin's dependencies must be listed in `dep` property. Using NPM 'dependencies' are not allowed!** - -### Openning and Closing a Plugin - -Modify `{appname}/config/plugin.js` to use plugin in an application. - -Every configuration has server parameters: - -- {Boolean} enable - enable this plugin or not -- {String} package - allow plugin to be imported as npm module -- {String} path - absolute path for plugin - -For example, - -```js -module.exports = { - /** - * depd plugin, store all deprecated api - * @member {Object} Plugin#depd - * @property {Boolean} enable - default true - * @since 1.0.0 - */ - depd: { - enable: true, - path: path.join(__dirname, '../../plugins/depd'), - }, - - /** - * dataman - * @member {Object} Plugin#drm - * @property {Boolean} enable - default true - * @property {Array} dep - list of dataman starting dependencies - * @since 1.0.0 - */ - drm: { - enable: true, - dep: ['configclient'], - }, - - /** - * development helper - jsonview - * add `?__json` to return data in page in json format - * @member {Object} Plugin#jsonview - * @property {Boolean} enable - default true - * @property {Array} env - open in non-production environment - * @since 1.0.0 - */ - jsonview: { - enable: true, - env: ['local', 'unittest', 'test', 'default', 'net'], - dep: ['view'], - }, - - // close omeo plug which is opened by default - omeo: false, -}; -``` - -### Naming a Plugin - -- simplest package name: `egg-xx`. corresponding `pluginName` should be in lowercase. - -- Use hyphen separated format for longer name, for example: `egg-foo-bar`, corresponding pluginName should be in small Camel-Case, `fooBar`. - -- Hyphen is not compulsory, for example: - - * `sessiontair`(`egg-sessiontair`) or `sessionTair`(`egg-session-tair`) - * `userservice`(`egg-userservice`) or `user-service`(`egg-user-service`)。 - -Follow the rules above. If you choose to use hyphen, pluginName should be in small Camel-Case. - -## Multi-process Model and Communication Between Processes - -![multi-process-model](http://aligitlab.oss-cn-hangzhou-zmf.aliyuncs.com/uploads/node/team/a44668d0ab/multi-process-model.png) -![start-seq](http://aligitlab.oss-cn-hangzhou-zmf.aliyuncs.com/uploads/node/team/202e55b92b/start-seq.png) - -### master&worker process - -To take advantage of existing server resource, master process start a cluster with multiple processes based on number of processors. - -### agent process - -Some of common work can be done on a central process, then facilitate the results to `worker` process. For example, accessing public resources, executing universal operations, watching local files, communicating with remote configuration providers, etc. - -Therefore, we create a new type of process, `agent process`. It is created by master process using `child_process`. It is a task execution process, which doesn't response to external http request. In some scenario with huge workload, `worker` process can ask `agent` process for help. Worker process can share part of task with agent process. Agent process will notify worker process when the task is finished. - -Therefore `plugin` and `app` can use `agent` process to execute tasks by writing a `agent.js` file. - -```sh -. example-package -├── package.json -├── app.js (optional) -|── agent.js (optional) -├── app -├── config -└── test -``` - -For more guide about `agent.js`, please see [egg-schedule:agent.js](https://github.com/eggjs/egg-schedule/blob/master/agent.js)。 - -### Communication Between Multiple Processes - -![communication-seq](https://github.com/eggjs/egg/blob/master/docs/assets/communication-seq.png) - -- `agent` process is created by `master` process using `child_process`. `worker` process is created by `master` process using cluster. Therefore, `master<->agent`, `master<->worker` can use IPC channel from node to communicate with each other. - -- When app is running, the most frequent communication is between agent and worker. That is being done by a virtual channel redirected by master. - -`agent` and `worker` process can use `messager` send and receive messages: - -```js -messager.broadcast('msg from agent'); -messager.on('msg form worker', callback); -``` - -See details in [egg-diamond](http://gitlab.alibaba-inc.com/egg/egg-diamond/tree/master) about communication between agent and worker process. - -### Robustness - -- Master process has the highest requirement for robustness. At any given time, master process need to be healthy and it should not run any functional operation. - -- Agent process is responsible to execute common tasks and heavy work load. Worker process depends on it. Master process is in control the lifecycle of agent process including start and restart if agent process is terminated. - -- worker process is the process that response to external requests. Master process is in controls the lifecycle of worker process, including starting and restarting if it is terminated. - -## File Watching - -The Node.js built-in file watcher has a cross-platform compatibility problem. To get a consistent system of file watcher, please see [egg-watcher](https://github.com/eggjs/egg-watcher). - -## User Object - -For a Web application, login and store of user information is an inevitable function. To be consistent so that other plugins can get user information easily, we have the following rules for API: - -- ctx.user - to get current user information -- ctx.userId - to get user id - -Generally, the rules above are implemented through middleware, which is responsible to get user information and user id from user store and inject into `ctx` object. - -**Egg** has a built-in implementation of `userservice`. You can use config files to implement how to get user information. If that is not good enough, feel free to write a `userservice` plugin, and override the built-in implementation. Make sure the plugin is named as `userservice`. - -## Template Rendering - -A Web system usually needs to dynamically render template to page. We also set some rules for API of template rendering: - -- ctx.render(name, locals) - render template file, and assign value for `ctx.body` -- ctx.renderString(tpl, locals) - render template string. Not assign value, only return value. -- app.view - instance of View class constructed by Egg - -Common Implementation: - -- Egg has already set two interfaces: `ctx.render` and `ctx.renderString`. -- Other framework should provide specific View class and implementation of these two interfaces. -- Template engine is not restricted. Feel free to use as you wish. - -> Note: If you are writing a separate view plugin, there is no need to add egg as a dependency. - -```js -// plugins/nunjucks-view/app/application.js - -const egg = require('egg'); - -class NunjucksView { - constructor(app) { - this.app = app; - // get config info from app.config.view - } - - /** - * render template, return finished string - * @method View#render - * @param {String} name template file name - * @param {Object} [locals] variables in page - * @return {Promise} result string of rendering - */ - render(name, locals) { - // Note: render returns a Promise object - return Promise.resolve('some html'); - } - - /** - * render template string - * @method View#renderString - * @param {String} tpl template string - * @param {Object} [locals] variables in page - * @return {Promise} result string of rendering - */ - renderString(tpl, locals) { - } -} - -module.exports = { - get [Symbol.for('egg#view')]() { - // Note: It's fine to just return class. Egg will turn it to an instance. - return NunjucksView; - } -}; -``` diff --git a/SPECIFICATION.zh_CN.md b/SPECIFICATION.zh_CN.md deleted file mode 100644 index 0957495ceb..0000000000 --- a/SPECIFICATION.zh_CN.md +++ /dev/null @@ -1,671 +0,0 @@ -# Web 基础框架 - -将实现一个适应阿里,蚂蚁环境的通用 Web 基础框架,包含 Web 应用目录结构约定, -代码加载机制 (Loader),配置文件约定和加载机制 启动脚本和部署脚本约定,插件机制。 - -**本文章节:** - -- 基础框架基于[koa](http://koajs.com/) -- 基础框架架构 - - `package.json` - - `app` (文件夹) - - `app/router.js` - - `app/controller` - - `app/middleware` - - `app/service` - - `app/proxy` - - `app/public` (静态资源文件夹) - - `app.js` - - koa extension - - `test` -- 配置文件约定和加载机制 - - 运行环境名称约定 -- 插件机制 - - 插件能做什么 - - 开启和关闭插件 - - 插件命名规则 -- 多进程模型及进程间通讯 - - master&worker 进程 - - agent 进程 - - 进程间通信 - - 健壮性 -- 文件监听 -- user 约定 - -## 基础框架基于`koa` - -选择基于 [koa](http://koajs.com/),是因为它是当前解决异步编程最好的 Web 通用框架。并且将在 2016 年自动适配 async-await 的 es2016 推荐的异步编程方案。我们已经对它的所有源代码 100% 掌握并且参与到核心代码贡献中。 - -## Web 应用目录结构约定和加载机制 - -此约定只限制本文描述的目录,不在本文描述的目录范围的其他目录,不在本约定范围。 - -以一个名称为 `helloweb` 的应用为例,它的目录结构如下: - -```sh -. helloweb -├── package.json -├── app.js (optional) -├── agent.js (optional) -├── app -│ ├── router.js -│ ├── controller -│ │ └── home.js -│ ├── extend (optional,for egg extention) -│ │ ├── helper.js (optional) -│ │ ├── filter.js (optional) -│ │ ├── request.js (optional) -│ │ ├── response.js (optional) -│ │ ├── context.js (optional) -│ │ ├── application.js (optional) -│ │ └── agent.js (optional) -│ ├── service (optional) -│ ├── public (optional) -│ │ ├── favicon.ico -│ │ └── ... -│ ├── middleware (optional) -│ │ └── response_time.js -│ └── view (optional, base view plugin rule, we suggest to use view) -│ ├── layout.html -│ └── home.html -├── config -│ ├── config.default.js -│ ├── config.prod.js -│ ├── config.test.js (optional) -│ ├── config.local.js (optional) -│ ├── config.unittest.js (optional) -│ ├── plugin.js -│ └── role.js (optional, we use role as an example of plugin, special config file for plugin should be placed in config directory) -└── test - ├── middleware - | └── response_time.test.js - └── controller - └── home.test.js -``` - -### `package.json` - -每个应用都必须包含 `package.json` 文件。 - -每个 `package.json` 至少包含以下配置项: - -- `name`:表示当前应用名,并且应用名需要跟 `aone` 上的一致。 -- `engines`:复用 `engines` 字段,用来表示当前应用所依赖的 Node 版本。 - -### `app` directory - -`app` 目录是一个应用业务逻辑代码存放的地方。 -它是整个应用的核心目录,包含 `router.js`,`controller`,`view`,`middleware` 等常用功能目录。 -同时还包含可选的 `service`,`proxy` 等服务调用相关功能代码目录。 - -#### `app/router.js` - -`app/router.js` 是应用的路由配置文件,所有路由配置都在此设置, -放在同一个文件非常方便通过 url 查找到对应的 `controller` 代码。 - -所有 `router.js` 文件约定的入口都是一个 `function(app)` 接口, -会自动传入当前的 app 实例对象, -开发者就可以通过 app 的路由方法 `get`, `post`, `put`, `delete`, `head` 等设置路由配置项。 - -Here is an example for `router.js`: - -```js -module.exports = function(app) { - app.get('/', app.controller.home); - app.post('/blog/:id/upload', app.controller.blog.upload); -}; -``` - -```js -module.exports = app => { - app.get('/', app.controller.home); - app.get('/forget', app.controller.forget); - app.post('/remember', app.controller.remember); -}; -``` - -#### `app/controller` - -每个 `app/controller/*.js` 文件,都会被自动加载到 `app.controller.*` 上。 -这样就能在 `app/router.js` 里面方便地进行路由配置。 - -以下目录将按约定加载: - -```js -├── app - └── controller - ├── foo_bar (automatically change name to Camel-Case, foo_bar => fooBar, foo-bar-ok => fooBarOk) - | └── user.js ==> app.controller.fooBar.user - ├── blog.js ==> app.controller.blog - └── home.js ==> app.controller.home -``` - -`controller` 就是一个普通的 koa middleware,格式为 `function*([next])`: - -`controller` is a Koa (v1) middleware. Use the generator format, (star function), for example `function*([next])`; - -```js -// home.js -module.exports = function*() { - this.body = 'hello world'; -}; -``` - - -```js -// blog.js -exports.upload = function*() { - // handle file upload -}; -``` - -通常来说,controller 是一个 HTTP 请求链中最后的一个处理者, -按约定不太可能一个 HTTP 请求会经过 2 个 controller 的。 - -在 controller 中可以调用 `service`,`proxy` 等依赖目录。 - -#### `app/middleware` - -应用自定义中间件都放在此目录,然后需要在 `config/config.default.js` 配置中间件的启动顺序。 - -```js -// config/config.js -exports.middleware = [ - 'responseTime', - 'locals', -]; -``` - -通常来说,middleware 是每一个 HTTP 请求都会经过,所以开发者需要明确了解自己开发的中间件前后顺序关系。 - -例如一个简单的 rt 计算中间件示例如下: - -```js -// middleware/response_time.js -module.exports = function(options, app) { - return function* (next) { - const start = Date.now(); - yield next; - const elapsed = Date.now() - start; - this.set('x-readtime', elapsed); - }; -}; -``` - -#### `app/service` - -数据服务逻辑层抽象,如果你在多个 controller 中都写了一段类似代码去取相同的数据, -那就代表很可能需要将这个数据服务层代码重构提取出来,放到 `app/service` 下。 - -于是我们根据一年多的项目实践, -抽象了一个 `Service` 基类,它只约定了一个构造函数接口: - -```js -// Service.js -class Service { - constructor(ctx) { - this.ctx = ctx; - this.app = ctx.app; - // 这样,你就可以在 Service 子类方法中直接获取到 ctx 和 app 了。 - } - - get proxy() { - return this.ctx.proxy; - } -} - -module.exports = Service; -``` - -一个对 User 服务的 Service 封装示例:`UserService.js` - -```js -const Service = require('egg').Service; - -class UserService extends Service { - constructor(ctx) { - super(ctx); - this.userClient = userClient; - }, - - * get(uid) { - const ins = instrument(this.ctx, 'buc', 'get'); - const result = yield userClient.get(uid); - ins.end(); - return result; - } -} - -module.exports = UserService; -``` - -特别注意的是,`app/service/*.js` 下的文件,每个 `Service` 都会像 `Context` 一样,在 -每个请求生成的时候,被自动实例化并且放到 `ctx.service` 下。 - -```js -├── app - └── service - ├── foo_bar (automatically change file name into Camal-Case, foo_bar => fooBar, foo-bar-ok => fooBarOk) - | └── user.js ==> ctx.service.fooBar.user - ├── blog.js ==> ctx.service.blog - └── user.js ==> ctx.service.user -``` - -#### `app/view` - -存放模板文件和只在客户端使用的脚本目录文件。 此规范有 view 插件约定。具体规范参见下文的 `模板渲染约定` - -#### `app/public` 静态资源目录 - -针对大部分内部应用,不需要将静态资源发布到 CDN 的场景,都可以将静态资源放到 `app/public` 目录下。 我们会自动对 `public` 目录下的文件,做以下映射: - -```js -app/public/js/main.js => /public/js/main.js -app/public/styles/bluc.css => /public/styles/blue.css -``` - -### `app.js` - -用于在应用启动的时候做一些初始化工作,一般来说,大部分应用都是不需要此功能的。 -如果一个应用使用了一些自定义服务客户端,那么需要做一些服务启动依赖检查的时候, -就可以通过 `app.js` 实现了。 - -```js -// app.js -const MyClient = require('some-client'); - -module.exports = function(app) { - app.myClient = new MyClient(); - - const done = app.async('my-client-ready'); - app.myClient.ready(done); - - // 如果有异常事件,也需要监听 - app.myClient.once('error', done); -}; -``` - -### `agent.js` - -和 `app.js` 类似,在 Agent Worker 进程中,如果需要做一些自定义处理,可以在这个文件中实现。 - -### koa 扩展约定 - -在 extend 目录下都是对已有 API 进行扩展,也就是追加到 prototype 上,如 `extend/application.js` 是扩展 Application.prototype。 - -- `app/extend/request.js`: extend koa request -- `app/extend/response.js`: extend koa response -- `app/extend/context.js`: extend koa context -- `app/extend/application.js`: extend koa application -- `app/extend/agent.js`: extend agent object - -### `test` - -单元测试目录,我们强制要求所有的 Web 应用都需要有足够多的单元测试保证。 - -约定好单元测试的目录结构,方便我们的测试驱动脚本统一。 - -通过目录结构可以看到,`test` 目录下的文件结构会跟 `app` 目录下的文件结构一致, -只是文件名变成了 `*.test.js`: - - -```sh -. helloweb -├── app -│ ├── controller -│ │ └── home.js -│ ├── middleware (optional) -│ │ └── response_time.js -└── test - └── controller - └── home.test.js - ├── middleware - | └── response_time.test.js -``` - -## 配置文件约定和加载机制 - -无论是使用 antx,还是 json 作为配置文件格式,最终都是为了生成一份 `{appname}/config/config.js` 配置文件。 - -```js -module.exports = { - keys: 'super secure passkey' -}; -``` - -### 运行环境名称约定 - -因为一份代码需要在不同的运行环境下运行,所以需要约定运行环境的名称。 - -| name | NODE_ENV | description | -| --- | --- | --- | -| prod | production | production environment,pre-production environment | -| test | production | system integration test environment,a.k.a sit environment | -| default | production | development server,normally every iteration will apply for a development server | -| local | development or null | local environment, developers computer, which is very likely developing multiple apps | -| unittest | test | unit test environment, such as developer's local environment and ci environment | - -### 根据环境加载配置 `config.*.js` Loading Configs Based on Environment - -- `{appname}/config/config.default.js`: default, all env will load this config -- `{appname}/config/config.prod.js`: prod env config -- `{appname}/config/config.test.js`: test env config -- `{appname}/config/config.local.js`: local env config -- `{appname}/config/config.unittest.js`: unittest env config - -#### 配置自动加载流程 Auto Loading of Configs - -假设当前环境为 `${env}`,那么最终的配置将按一下顺序加载合并而成。 - -- 加载顺序:core -> plugin -> app,从内到外顺序加载 -- 相同配置优先级:app > plugin > core,最外层的配置覆盖内层的配置 - -配置文件 loader 过程: - -``` -egg/config/config.default.js - ${plugin}/config/config.default.js - ${app}/config/config.default.js - egg/config/config.${env}.js - ${plugin}/config/config.${env}.js - ${app}/config/config.${env}.js -``` - -## 插件机制 - -**为什么有了 koa 的中间件,还需要提出一个插件机制呢?** - -因为中间件不能满足很多内部需求,如 diamond-client 入注到应用中,放在中间件不合适, -并且它还有启动检查依赖需求,必须确认 diamond-client 启动成功,才能让应用启动成功。 - -### 插件能做什么? - -一个插件就如一个 mini 应用,它是对应用的扩展,但它不包含 controller 和 router。 - -- 如需要对 koa 进行扩展,可以通过 `app/extend/request.js, response.js, context.js, application.js` 实现。 - -- 如需要插入自定义中间件,则可以结合 `app.js` 和 `app/middleware/*.js` 实现。 - -如将 static 插件的中间件放到应用中间件列表 `app.config.appMiddleware` 的前面: - -```js -// plugins/static/app.js -const assert = require('assert'); - -module.exports = function (app) { - assert.equal(app.config.appMiddleware.indexOf('static'), -1, - 'middleware of custom plugin static has the same name as default middleware static, please set a new name, like appStatic'); - - //put static's middleware before bodyParser - const index = app.config.coreMiddleware.indexOf('bodyParser'); - assert(index >= 0, 'Must have middleware bodyParser'); - - app.config.coreMiddleware.splice(index, 0, 'static'); -}; -``` - -`app.config.coreMiddleware` 总是会在 `app.config.appMiddleware` 之前被 `app.use` 加载。 - -- 如需要实现启动依赖检查,则通过 `app.js` 里面使用约定的 `app.readyCallback(asyncName)` 实现。 - -```js -// app.js -app.myClient = new MyClient(); - -// ready -app.myClient.ready(app.readyCallback('my-client-ready')); - -// event -app.myClient.once('connect', app.readyCallback('my-client-ready')); -``` - -### 定义插件 - -以下是一个插件大致的结构,和 app 类似 - -```sh -. helloclient -├── package.json -├── app.js (optional) -├── agent.js (optional) -├── app -│ ├── extend (optional) -│ | ├── helper.js (optional) -│ | ├── request.js (optional) -│ | ├── response.js (optional) -│ | ├── context.js (optional) -│ | ├── application.js (optional) -│ | └── agent.js (optional) -│ ├── proxy (optional) -│ ├── service (optional) -│ └── middleware (optional) -│ └── my.js -├── config -| ├── config.default.js -│ ├── config.prod.js -| ├── config.test.js (optional) -| ├── config.local.js (optional) -| └── config.unittest.js (optional) -└── test - └── middleware - └── my.test.js -``` - -在 `pakcage.json` 中定义插件的属性 - -```json -{ - "name": "egg-mysql", - "eggPlugin": { - "name": "egg-mysql", - "dep": [ "configclient" ], - } -} -``` - - -- {String} name - 插件名(必须配置),具有唯一性,配置 dep 时会指定依赖插件的 name。 -- {Array} dep - 此插件依赖的插件列表 -- {Array} env - 只有在指定运行环境才能开启 - -**注意:插件只能以 dep 的方式依赖,不能通过 npm 依赖** - -### 开启和关闭插件 - -可以在应用或框架使用插件,在 `{appname}/config/plugin.js` 进行配置。 - -每个配置项有一下配置参数: - - -- {Boolean} enable - 是否开启此插件 -- {String} package - npm 模块名称,允许插件以 npm 模块形式引入 npm module name -- {String} path - 插件绝对路径,跟 package 配置互斥 - -看看一个示例配置: - -```js -module.exports = { - /** - * depd plugin, store all deprecated api - * @member {Object} Plugin#depd - * @property {Boolean} enable - default true - * @since 1.0.0 - */ - depd: { - enable: true, - path: path.join(__dirname, '../../plugins/depd'), - }, - - /** - * dataman - * @member {Object} Plugin#drm - * @property {Boolean} enable - default true - * @property {Array} dep - list of dataman starting dependencies - * @since 1.0.0 - */ - drm: { - enable: true, - dep: ['configclient'], - }, - - /** - * development helper - jsonview - * add `?__json` to return data in page in json format - * @member {Object} Plugin#jsonview - * @property {Boolean} enable - default true - * @property {Array} env - open in non-production environment - * @since 1.0.0 - */ - jsonview: { - enable: true, - env: ['local', 'unittest', 'test', 'default', 'net'], - dep: ['view'], - }, - - // close omeo plug which is opened by default - omeo: false, -}; -``` - - -### 插件命名规则 - -- 最简单的 pacakge name: `@ali/egg-xx`,`@alipay/egg-xx`,pluginName 为全小写 `xx` - -- 比较长的用中划线package name `@ali/egg-foo-bar`,`@alipay/egg-foo-bar`,对应的 pluginName 使用小驼峰,小驼峰转换规则以 package name 的中划线为准 `fooBar` - -- 对于可以中划线也可以不用的情况,不做强制约定,例如 - - * `sessiontair`(`egg-sessiontair`) or `sessionTair`(`egg-session-tair`) - * `userservice`(`egg-userservice`) or `user-service`(`egg-user-service`)。 - -只要遵循上两条规则即可,如果选择用中划线,就要按照小驼峰命名 pluginName。 - - -## 多进程模型及进程间通讯 Multi-process Model and Communication Between Processes - -![multi-process-model](http://aligitlab.oss-cn-hangzhou-zmf.aliyuncs.com/uploads/node/team/a44668d0ab/multi-process-model.png) -![start-seq](http://aligitlab.oss-cn-hangzhou-zmf.aliyuncs.com/uploads/node/team/202e55b92b/start-seq.png) - - -### master&worker process - -为了最大限度的榨干服务器资源,我们不使用单进程模型。master 进程利用 cluster 根据 CPU 个数启动多个 worker 进程,以达到更好的吞吐率。 - -### agent process - -对于一些公共资源的访问,通用性的操作,例如本地文件监听、与配置中心、DRM交互等,每个 worker 都来一遍非常浪费,且会引发各种问题。故引入 agent 进程,由 master 使用 child_process 启动,它是一个任务执行进程,并不对外提供 http 访问。worker 进程如果有需要可以把这部分任务交给 agent 进程执行,agent 执行后告诉 worker。 - -不管对于插件还是应用来说,都可以通过在项目根目录放置一个 `agent.js`,在 agent 进程中执行任务。 - -```sh -. example-package -├── package.json -├── app.js (optional) -|── agent.js (optional) -├── app -├── config -└── test -``` - -For more guide about `agent.js`, please see [egg-schedule:agent.js](https://github.com/eggjs/egg-schedule/blob/master/agent.js)。 - -### 进程间通信 - -![communication-seq](https://github.com/eggjs/egg/blob/master/docs/assets/communication-seq.png) - -- agent 由 master 使用 child_process 启动,worker 由 master 使用 cluster 启动,所以 `master<->agent`,`master<->worker` 都可以使用 node 内置的 IPC 通道进行通讯。 - -- 对于应用运行时,发生最多的是 agent 和 worker 之间的通讯,由 master 转发消息完成,实现了一个虚拟的通道。 - -在 agent 和 worker 进程中都可以使用 messager 发送和监听消息: - -```js -messager.broadcast('msg from agent'); -messager.on('msg form worker', callback); -``` - -可参考 [egg-diamond](http://gitlab.alibaba-inc.com/egg/egg-diamond/tree/master) 中对于 agent 和 worker 之间通讯的实现。 - -### 健壮性 - -- master 进程健壮性要求最高,绝对不能挂掉。在 master 进程中不做任何业务代码执行。 - -- agent 进程会执行公共资源访问类操作,worker 非常需要它,所以 master 进程需要负责 agent 生命周期管理,包括启动和挂掉重启等。 - -- worker 进程是直接对外提供服务的进程,master 进程同样需要负责 worker 进程的生命周期管理,包括启动和挂掉重启。 - -## 文件监听 - -node 自带的文件监听有跨平台兼容问题,并且对文件监听的机制也不尽相同,所以需要一套统一的 API,屏蔽掉不同的实现。详细机制请移步 [egg-watcher](https://github.com/eggjs/egg-watcher)。 - - -## user 约定 - -对于一个 web 系统,通常都需要登录后获取 user 信息。为了能够实现通用性,其他功能/插件中能够通过统一的 API 获取用户信息,做出以下约定: - -- ctx.user 获取用户信息 -- ctx.userId 获取用户 id - -通常实现的方式,是通过 middleware 从 user store 中获取用户信息和用户 id 挂到 ctx 上,具体用户数据来源不做约定,由具体框架/业务自由选择。 - -egg 中内置了简单的 userservice 实现,可以通过配置实现自己获取 user 的逻辑。如果不能够满足需求,可以自己单独实现一个 userservice plugin,覆盖默认实现,但需要保持命名 `userservice`。 - -## 模板渲染约定 - -对于一个 web 系统,通常都动态渲染页面。为了能够实现通用性,其他功能/插件中能够通过统一的 API 渲染模板,做出以下约定: - -- ctx.render(name, locals) - 渲染模板文件, 并赋值给 ctx.body -- ctx.renderString(tpl, locals) - 渲染模板字符串, 仅返回不赋值 -- app.view - egg 实例化具体 View 后的引用 - -通常实现的方式: - -- egg 已经实现 ctx.render 和 ctx.renderString -- 框架层需提供具体的 View 模板渲染类, 需提供以下两个接口的实现。 -- 模板引擎选型不做约束,由框架/业务自由选择。 - -> 注意: 如果你写的是独立的 view 插件, 无需在 package.json 中声明对 egg 的依赖 - - -```js -// plugins/nunjucks-view/app/application.js - -const egg = require('egg'); - -class NunjucksView { - constructor(app) { - this.app = app; - // get config info from app.config.view - } - - /** - * render template, return finished string - * @method View#render - * @param {String} name template file name - * @param {Object} [locals] variables in page - * @return {Promise} result string of rendering - */ - render(name, locals) { - // Note: render returns a Promise object - return Promise.resolve('some html'); - } - - /** - * render template string - * @method View#renderString - * @param {String} tpl template string - * @param {Object} [locals] variables in page - * @return {Promise} result string of rendering - */ - renderString(tpl, locals) { - } -} - -module.exports = { - get [Symbol.for('egg#view')]() { - // Note: It's fine to just return class. Egg will turn it to an instance. - return NunjucksView; - } -}; -``` diff --git a/agent.js b/agent.js new file mode 100644 index 0000000000..17fbdc32eb --- /dev/null +++ b/agent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseHookClass = require('./lib/core/base_hook_class'); + +class EggAgentHook extends BaseHookClass { + configDidLoad() { + this.agent._wrapMessenger(); + } +} + +module.exports = EggAgentHook; diff --git a/app/extend/agent.js b/app/extend/agent.js deleted file mode 100644 index b796c193ad..0000000000 --- a/app/extend/agent.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const Singleton = require('../../lib/core/singleton'); - -// 空的 instrument 返回,用于生产环境,避免每次创建对象 -const emptyInstrument = { - end() {}, -}; - -module.exports = { - - /** - * 创建一个单例并添加到 app/agent 上 - * @method Agent#addSingleton - * @param {String} name 单例的唯一名字 - * @param {Object} create - 单例的创建方法 - */ - addSingleton(name, create) { - const options = {}; - options.name = name; - options.create = create; - options.app = this; - const singleton = new Singleton(options); - singleton.init(); - }, - - /** - * 记录操作的时间 - * @method Agent#instrument - * @param {String} event 类型 - * @param {String} action 具体操作 - * @return {Object} 对象包含 end 方法 - * @example - * ```js - * const ins = agent.instrument('http', `${method} ${url}`); - * // doing - * ins.end(); - * ``` - */ - instrument(event, action) { - if (this.config.env !== 'local') { - return emptyInstrument; - } - const payload = { - start: Date.now(), - agent: this, - event, - action, - }; - - return { - end() { - const start = payload.start; - const duration = Date.now() - start; - payload.agent.logger.info(`[${payload.event}] ${payload.action} ${duration}ms`); - }, - }; - }, -}; diff --git a/app/extend/application.js b/app/extend/application.js deleted file mode 100644 index 395581bade..0000000000 --- a/app/extend/application.js +++ /dev/null @@ -1,276 +0,0 @@ -'use strict'; - -const http = require('http'); -const assert = require('assert'); -const Keygrip = require('../../lib/core/keygrip'); -const Service = require('../../lib/core/base_service'); -const view = require('../../lib/core/view'); -const AppWorkerClient = require('../../lib/core/app_worker_client'); -const util = require('../../lib/core/util'); -const Singleton = require('../../lib/core/singleton'); - -const KEYS = Symbol('Application#keys'); -const APP_CLIENTS = Symbol('Application#appClients'); -const VIEW = Symbol('Application#View'); -const LOCALS = Symbol('Application#locals'); -const LOCALS_LIST = Symbol('Application#localsList'); - -// empty instrument object, use on prod env, avoid create object every time. -const emptyInstrument = { - end() {}, -}; - -module.exports = { - - /** - * Create egg context - * @method Application#createContext - * @param {Req} req - node native Request object - * @param {Res} res - node native Response object - * @return {Context} context object - */ - createContext(req, res) { - const app = this; - const context = Object.create(app.context); - const request = context.request = Object.create(app.request); - const response = context.response = Object.create(app.response); - context.app = request.app = response.app = app; - context.req = request.req = response.req = req; - context.res = request.res = response.res = res; - request.ctx = response.ctx = context; - request.response = response; - response.request = request; - context.onerror = context.onerror.bind(context); - context.originalUrl = request.originalUrl = req.url; - - /** - * Request start time - * @member {Number} Context#starttime - */ - context.starttime = Date.now(); - return context; - }, - - /** - * Service class - * @member {Service} Application#Service - * @since 1.0.0 - */ - Service, - - /** - * AppWorkerClient class - * @member {AppWorkerClient} Application#AppWorkerClient - */ - AppWorkerClient, - - /** - * 当前进程实例化的 AppWorkerClient 集合 - * @member {Map} Application#appWorkerClients - * @private - */ - get appWorkerClients() { - if (!this[APP_CLIENTS]) { - this[APP_CLIENTS] = new Map(); - } - return this[APP_CLIENTS]; - }, - - /** - * 创建一个 App worker "假"客户端 - * @method Application#createAppWorkerClient - * @param {String} name 客户端的唯一名字 - * @param {Object} impl - 客户端需要实现的 API - * @param {Object} options - * - {Number|String} responseTimeout - 响应超时间隔,默认为 3s - * @return {AppWorkerClient} - client - */ - createAppWorkerClient(name, impl, options) { - options = options || {}; - options.name = name; - options.app = this; - - const client = new AppWorkerClient(options); - Object.assign(client, impl); - // 直接 ready - client.ready(true); - return client; - }, - - /** - * 创建一个单例并添加到 app/agent 上 - * @method Application#addSingleton - * @param {String} name 单例的唯一名字 - * @param {Object} create - 单例的创建方法 - */ - addSingleton(name, create) { - const options = {}; - options.name = name; - options.create = create; - options.app = this; - const singleton = new Singleton(options); - singleton.init(); - }, - - /** - * 获取密钥 - * @member {String} Application#keys - */ - get keys() { - if (!this[KEYS]) { - if (!this.config.keys) { - if (this.config.env === 'local' || this.config.env === 'unittest') { - console.warn('Please set config.keys first, now using mock keys for dev env (%s)', - this.config.baseDir); - this.config.keys = 'foo, keys, you need to set your app keys'; - } else { - throw new Error('Please set config.keys first'); - } - } - const keyConfig = this.config.keys.split(',').map(s => s.trim()); - this[KEYS] = new Keygrip(keyConfig); - } - return this[KEYS]; - }, - - /** - * 获取模板渲染类, 继承于 view 插件提供的 View 基类 - * @member {View} Application#View - * @private - */ - get View() { - if (!this[VIEW]) { - assert(this[Symbol.for('egg#view')], 'should enable view plugin'); - this[VIEW] = view(this[Symbol.for('egg#view')]); - } - return this[VIEW]; - }, - - /** - * 全局的 locals 变量 - * @member {Object} Application#locals - * @see Context#locals - */ - get locals() { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - if (this[LOCALS_LIST] && this[LOCALS_LIST].length) { - util.assign(this[LOCALS], this[LOCALS_LIST]); - this[LOCALS_LIST] = null; - } - return this[LOCALS]; - }, - - set locals(val) { - if (!this[LOCALS_LIST]) { - this[LOCALS_LIST] = []; - } - this[LOCALS_LIST].push(val); - }, - - /** - * 创建一个匿名请求的 ctx 实例 - * @param {Request} [req] - 自定义 request 对象数据,会进行最多2级的深 copy,可选参数 - * @return {Context} Application#nonUserContext - */ - createAnonymousContext(req) { - const request = { - headers: { - 'x-forwarded-for': '127.0.0.1', - }, - query: {}, - querystring: '', - host: '127.0.0.1', - hostname: '127.0.0.1', - protocol: 'http', - secure: 'false', - method: 'GET', - url: '/', - path: '/', - socket: { - remoteAddress: '127.0.0.1', - remotePort: 7001, - }, - }; - if (req) { - for (const key in req) { - if (key === 'headers' || key === 'query' || key === 'socket') { - Object.assign(request[key], req[key]); - } else { - request[key] = req[key]; - } - } - } - const response = new http.ServerResponse(request); - return this.createContext(request, response); - }, - - /** - * 已经处理过的 Helper 类,包含用户 App Helper 的所有函数 - * @member {Helper} Application#Helper - */ - Helper: class Helper { - /** - * Helper class - * @class Helper - * @param {Object} ctx - context object, etc: this.ctx - * @example - * ```js - * const helper = new Helper(this); - * helper.csrfTag(); - * ``` - */ - constructor(ctx) { - this.ctx = ctx; - this.app = ctx.app; - } - }, - - /** - * 记录操作的时间 - * @method Application#instrument - * @param {String} event 类型 - * @param {String} action 具体操作 - * @param {Context} ctx 上下文,如果与上下文无关的记录可以不传入 - * @return {Object} 对象包含 end 方法 - * @example - * ```js - * const ins = app.instrument('http', `${method} ${url}`, ctx); - * // doing - * ins.end(); - * ``` - */ - instrument(event, action, ctx) { - if (this.config.env !== 'local') { - return emptyInstrument; - } - - const payload = { - start: Date.now(), - ctx, // 可选 - app: this, - event, - action, - }; - - return { - end() { - const start = payload.start; - const duration = Date.now() - start; - - const ctx = payload.ctx; - if (ctx) { - ctx.logger.info(`[${payload.event}] ${payload.action} ${duration}ms`); - if (ctx.runtime) { - ctx.runtime[payload.event] = ctx.runtime[payload.event] || 0; - ctx.runtime[payload.event] += duration; - } - } else { - payload.app.logger.info(`[${payload.event}] ${payload.action} ${duration}ms`); - } - }, - }; - }, - -}; diff --git a/app/extend/context.js b/app/extend/context.js index de17a0a7fe..8c5277235a 100644 --- a/app/extend/context.js +++ b/app/extend/context.js @@ -1,72 +1,56 @@ -/** - * 对 koa context 的所有扩展,都放在此文件统一维护。 - * - * - koa context: https://github.com/koajs/koa/blob/master/lib/context.js - */ - 'use strict'; +const { performance } = require('perf_hooks'); const delegate = require('delegates'); -const jsonpBody = require('jsonp-body'); -const ContextLogger = require('egg-logger').EggContextLogger; -const Cookies = require('egg-cookies'); -const util = require('../../lib/core/util'); +const { assign } = require('utility'); +const eggUtils = require('egg-core').utils; -const LOGGER = Symbol('LOGGER'); -const CORE_LOGGER = Symbol('CORE_LOGGER'); const HELPER = Symbol('Context#helper'); -const VIEW = Symbol('Context#view'); const LOCALS = Symbol('Context#locals'); const LOCALS_LIST = Symbol('Context#localsList'); const COOKIES = Symbol('Context#cookies'); +const CONTEXT_LOGGERS = Symbol('Context#logger'); +const CONTEXT_HTTPCLIENT = Symbol('Context#httpclient'); +const CONTEXT_ROUTER = Symbol('Context#router'); const proto = module.exports = { + + /** + * Get the current visitor's cookies. + */ get cookies() { if (!this[COOKIES]) { - this[COOKIES] = new Cookies(this, this.app.keys); + this[COOKIES] = new this.app.ContextCookies(this, this.app.keys, this.app.config.cookies); } return this[COOKIES]; }, /** - * 默认的 role 检测失败处理 - * session 插件等可以覆盖 ctx.roleFailureHandler(action) 来重置 - * @method Context#roleFailureHandler - * @param {String} action 导致失败的角色 + * Get a wrapper httpclient instance contain ctx in the hold request process + * + * @return {ContextHttpClient} the wrapper httpclient instance */ - roleFailureHandler(action) { - const message = `Forbidden, required role: ${action}`; - this.status = 403; - if (this.isAjax) { - this.body = { - message, - stat: 'deny', - }; - } else { - this.body = message; + get httpclient() { + if (!this[CONTEXT_HTTPCLIENT]) { + this[CONTEXT_HTTPCLIENT] = new this.app.ContextHttpClient(this); } + return this[CONTEXT_HTTPCLIENT]; }, /** - * 对 {@link urllib} 的封装,会自动记录调用日志,参数跟 `urllib.request(url, args)` 保持一致. - * @method Context#curl - * @param {String} url request url address. - * @param {Object} opts options for request, Optional. - * @return {Object} 与 {@link Application#curl} 一致 - * @since 1.0.0 + * Shortcut for httpclient.curl + * + * @function Context#curl + * @param {String|Object} url - request url address. + * @param {Object} [options] - options for request. + * @return {Object} see {@link ContextHttpClient#curl} */ - * curl(url, opts) { - opts = opts || {}; - opts.ctx = this; - const method = (opts.method || 'GET').toUpperCase(); - const ins = this.instrument('http', `${method} ${url}`); - const result = yield this.app.curl(url, opts); - ins.end(); - return result; + curl(url, options) { + return this.httpclient.curl(url, options); }, /** - * App 的 {@link Router} 实例,你可以用它生成 URL Path, 比如 `{@link Router#pathFor|router.pathFor}` + * Alias to {@link Application#router} * * @member {Router} Context#router * @since 1.0.0 @@ -76,56 +60,23 @@ const proto = module.exports = { * ``` */ get router() { - return this.app.router; - }, - - /** - * 用于记录 egg Request 周期的耗时,比如总耗时, View 渲染耗时 - * - * 如果你有自定义的访问,希望往 RequestLog 里面增加统计信息,可以往 ctx.runtime['xx'] 里面写信息 - * egg 会自动将它们打印出来。 - * @member {Object} Context#runtime - * @property {Float} rt - 总耗时 - * @property {Float} view - View Render 耗时 - * @property {Float} buc - Buc 查询耗时 - * @property {Float} mysql - MySQL 查询耗时 - * @property {Float} hsf - HSF 查询耗时 - * @property {Float} tr - TR 查询耗时 - * @property {Float} http - 外部 HTTP 请求耗时 - * @since 1.0.0 - */ - get runtime() { - this._runtime = this._runtime || {}; - return this._runtime; - }, - - /** - * 记录上下文相关的操作时间 - * @param {String} event 与 {@link Application#instrument} 一致 - * @param {String} action 与 {@link Application#instrument} 一致 - * @return {Object} 与 {@link Application#instrument} 一致 - */ - instrument(event, action) { - return this.app.instrument(event, action, this); + if (!this[CONTEXT_ROUTER]) { + this[CONTEXT_ROUTER] = this.app.router; + } + return this[CONTEXT_ROUTER]; }, /** - * 读/写真实的响应状态码,在一些特殊场景,如 404,500 状态,我们希望通过 302 跳转到统一的 404 和 500 页面, 但是日志里面又想正确地记录 404 或 500 真实的状态码而不是 302,那么我们就需要设置 realStatus 来实现了。 - * @member {Number} Context#realStatus + * Set router to Context, only use on EggRouter + * @param {EggRouter} val router instance */ - get realStatus() { - if (this._realStatus) { - return this._realStatus; - } - return this.status; - }, - - set realStatus(status) { - this._realStatus = status; + set router(val) { + this[CONTEXT_ROUTER] = val; }, /** - * 获取 helper 实例 + * Get helper instance from {@link Application#Helper} + * * @member {Helper} Context#helper * @since 1.0.0 */ @@ -137,61 +88,36 @@ const proto = module.exports = { }, /** - * 默认返回一个空对象,需要实现这个接口 + * Wrap app.loggers with context infomation, + * if a custom logger is defined by naming aLogger, then you can `ctx.getLogger('aLogger')` * - * 插件需要实现一个内部约定 getter: `_tracer` - * @member {Object} Context#tracer + * @param {String} name - logger name + * @return {Logger} logger */ - get tracer() { - return this._tracer || {}; - }, + getLogger(name) { + if (this.app.config.logger.enableFastContextLogger) { + return this.app.getLogger(name); + } + let cache = this[CONTEXT_LOGGERS]; + if (!cache) { + cache = this[CONTEXT_LOGGERS] = {}; + } - /** - * 写 Cookie - * @method Cookie#setCookie - * @param {String} name cookie name - * @param {String} value cookie value - * @param {Object} opts cookie options - * - {String} domain cookie domain, default is `ctx.hostname` - * - {String} path cookie path, default is '/' - * - {Boolean} encrypt encrypt cookie or not, default is false - * - {Boolean} httpOnly http only cookie or not, default is true - * - {Date} expires cookie's expiration date, default is expires at the end of session. - * @return {Context} koa context - */ - setCookie(name, value, opts) { - this.cookies.set(name, value, opts); - return this; - }, + // read from cache + if (cache[name]) return cache[name]; - /** - * 读取 Cookie - * @method Cookie#getCookie - * @param {String} name - Cookie key - * @param {Object} opts cookie options - * @return {String} cookie value - */ - getCookie(name, opts) { - return this.cookies.get(name, opts); - }, + // get no exist logger + const appLogger = this.app.getLogger(name); + if (!appLogger) return null; - /** - * 删除 Cookie - * @method Cookie#deleteCookie - * @param {String} name - Cookie key - * @param {Object} opts cookie options - * @return {Context} koa context - */ - deleteCookie(name, opts) { - this.setCookie(name, null, opts); - return this; + // write to cache + cache[name] = new this.app.ContextLogger(this, appLogger); + return cache[name]; }, /** - * 应用 Web 相关日志,用于记录 Web 行为相关的日志, - * 最终日志文件输出到 `{log.dir}/{app.name}-web.log` 中。 - * 每行日志会自动记录上当前请求的一些基本信息, - * 如 `[$logonId/$userId/$ip/$timestamp_ms/$sofaTraceId $use_ms $method $url]` + * Logger for Application, wrapping app.coreLogger with context infomation + * * @member {ContextLogger} Context#logger * @since 1.0.0 * @example @@ -199,101 +125,28 @@ const proto = module.exports = { * this.logger.info('some request data: %j', this.request.body); * this.logger.warn('WARNING!!!!'); * ``` - * 错误日志记录,直接会将错误日志完整堆栈信息记录下来,并且输出到 `{log.dir}/common-error.log` - * ``` - * this.logger.error(err); - * ``` */ get logger() { - if (!this[LOGGER]) { - this[LOGGER] = new ContextLogger(this, this.app.logger); - } - return this[LOGGER]; + return this.getLogger('logger'); }, /** - * app context 级别的 core logger,适合 core 对当前请求记录日志使用 + * Logger for frameworks and plugins, + * wrapping app.coreLogger with context infomation + * * @member {ContextLogger} Context#coreLogger * @since 1.0.0 */ get coreLogger() { - if (!this[CORE_LOGGER]) { - this[CORE_LOGGER] = new ContextLogger(this, this.app.coreLogger); - } - return this[CORE_LOGGER]; - }, - - /** - * 设置 jsonp 的内容,将会以 jsonp 的方式返回。注意:不可读。 - * @member {Void} Context#jsonp - * @param {Object} obj 设置的对象 - */ - set jsonp(obj) { - const options = this.app.config.jsonp; - const jsonpFunction = this.query[options.callback]; - if (!jsonpFunction) { - this.body = obj; - } else { - this.set('x-content-type-options', 'nosniff'); - this.type = 'js'; - this.body = jsonpBody(obj, jsonpFunction, options); - } + return this.getLogger('coreLogger'); }, /** - * 获取 view 实例 - * @return {View} view 实例 - */ - get view() { - if (!this[VIEW]) { - this[VIEW] = new this.app.View(this); - } - return this[VIEW]; - }, - - /** - * 渲染页面模板后直接返回 response - * @method Context#render - * @param {String} name 模板文件名 - * @param {Object} [locals] 需要放到页面上的变量 - * @see Context#renderView - */ - * render(name, locals) { - this.body = yield this.renderView(name, locals); - }, - - /** - * 渲染页面模板,返回字符串 - * @method Context#renderView - * @param {String} name 模板文件名 - * @param {Object} [locals] 需要放到页面上的变量 - * @return {String} 渲染后的字符串. - * @see View#render - */ - * renderView(name, locals) { - const ins = this.instrument('view', `render ${name}`); - const body = yield this.view.render(name, locals); - ins.end(); - return body; - }, - - /** - * 渲染模板字符串 - * @method Context#renderString - * @param {String} tpl 模板字符串 - * @param {Object} [locals] 需要放到页面上的变量 - * @return {String} 渲染后的字符串 - * @see View#renderString - */ - * renderString(tpl, locals) { - return yield this.view.renderString(tpl, locals); - }, - - /** - * locals 为模板使用的变量,可以在任何地方设置 locals,在渲染模板的时候会合并这些变量。 - * app.locals 和 this.locals 最大的区别是作用域不同,app.locals 是全局的,this.locals 是一个请求的。 + * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, + * which will be used as data when view is rendering. + * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. * - * 设置 locals 的时候只支持对象,他会和原来的数据进行合并 + * when you set locals, only object is available * * ```js * this.locals = { @@ -311,16 +164,16 @@ const proto = module.exports = { * }; * ``` * - * 注意:**this.locals 有缓存,只在第一次访问 this.locals 时合并 app.locals。** + * `ctx.locals` has cache, it only merges `app.locals` once in one request. * * @member {Object} Context#locals */ get locals() { if (!this[LOCALS]) { - this[LOCALS] = util.assign({}, this.app.locals); + this[LOCALS] = assign({}, this.app.locals); } if (this[LOCALS_LIST] && this[LOCALS_LIST].length) { - util.assign(this[LOCALS], this[LOCALS_LIST]); + assign(this[LOCALS], this[LOCALS_LIST]); this[LOCALS_LIST] = null; } return this[LOCALS]; @@ -334,37 +187,99 @@ const proto = module.exports = { }, /** - * egg 使用 locals 作为服务端传递给模板中的变量挂载容器 - * 当开启 egg-locals 插件时,this.state 返回 this.locals + * alias to {@link Context#locals}, compatible with koa that use this variable * @member {Object} state + * @see Context#locals */ get state() { - return this.locals || {}; + return this.locals; }, set state(val) { this.locals = val; }, -}; -/** - * Context delegation. - */ + /** + * Run async function in the background + * @param {Function} scope - the first args is ctx + * ```js + * this.body = 'hi'; + * + * this.runInBackground(async ctx => { + * await ctx.mysql.query(sql); + * await ctx.curl(url); + * }); + * ``` + */ + runInBackground(scope) { + // try to use custom function name first + /* istanbul ignore next */ + const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true); + scope._name = taskName; + this._runInBackground(scope); + }, -/** - * @member {Boolean} Context#isAjax - * @see Request#isAjax - * @since 1.0.0 - */ + // let plugins or frameworks to reuse _runInBackground in some cases. + // e.g.: https://github.com/eggjs/egg-mock/pull/78 + _runInBackground(scope) { + const ctx = this; + const start = performance.now(); + /* istanbul ignore next */ + const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true); + // use setImmediate to ensure all sync logic will run async + return new Promise(resolve => setImmediate(resolve)) + // use app.toAsyncFunction to support both generator function and async function + .then(() => ctx.app.toAsyncFunction(scope)(ctx)) + .then(() => { + ctx.coreLogger.info('[egg:background] task:%s success (%dms)', + taskName, Math.floor((performance.now() - start) * 1000) / 1000); + }) + .catch(err => { + // background task process log + ctx.coreLogger.info('[egg:background] task:%s fail (%dms)', + taskName, Math.floor((performance.now() - start) * 1000) / 1000); + + // emit error when promise catch, and set err.runInBackground flag + err.runInBackground = true; + ctx.app.emit('error', err, ctx); + }); + }, +}; /** - * @member {Array} Context#queries - * @see Request#queries - * @since 1.0.0 + * Context delegation. */ delegate(proto, 'request') - .getter('isAjax') + /** + * @member {Boolean} Context#acceptJSON + * @see Request#acceptJSON + * @since 1.0.0 + */ .getter('acceptJSON') + /** + * @member {Array} Context#queries + * @see Request#queries + * @since 1.0.0 + */ .getter('queries') - .getter('accept'); + /** + * @member {Boolean} Context#accept + * @see Request#accept + * @since 1.0.0 + */ + .getter('accept') + /** + * @member {string} Context#ip + * @see Request#ip + * @since 1.0.0 + */ + .access('ip'); + +delegate(proto, 'response') + /** + * @member {Number} Context#realStatus + * @see Response#realStatus + * @since 1.0.0 + */ + .access('realStatus'); diff --git a/app/extend/helper.js b/app/extend/helper.js index 92d2af88de..3120944d25 100644 --- a/app/extend/helper.js +++ b/app/extend/helper.js @@ -1,42 +1,43 @@ 'use strict'; -const path = require('path'); +const url = require('url'); + module.exports = { /** - * 基于路由规则生成 URL(不带 host)。 - * @method Helper#pathFor + * Generate URL path(without host) for route. Takes the route name and a map of named params. + * @function Helper#pathFor * @param {String} name - Router Name * @param {Object} params - Other params * * @example * ```js * app.get('home', '/index.htm', 'home.index'); - * helper.pathFor('home', { by: 'recent', limit: 20 }) + * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) * => /index.htm?by=recent&limit=20 * ``` - * @return {String} 路由对应的相对路径 + * @return {String} url path(without host) */ pathFor(name, params) { return this.app.router.url(name, params); }, /** - * 基于路由规则生成 URL,同时会包含 host 前缀。 - * @method Helper#urlFor + * Generate full URL(with host) for route. Takes the route name and a map of named params. + * @function Helper#urlFor * @param {String} name - Router name * @param {Object} params - Other params * @example * ```js * app.get('home', '/index.htm', 'home.index'); - * helper.pathFor('home', { by: 'recent', limit: 20 }) + * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 * ``` - * @return {String} 含有域名的完整 URL + * @return {String} full url(with host) */ urlFor(name, params) { - return this.ctx.protocol + '://' + this.ctx.host + path.join('/', this.app.router.url(name, params)); + return this.ctx.protocol + '://' + this.ctx.host + url.resolve('/', this.app.router.url(name, params)); }, }; diff --git a/app/extend/request.js b/app/extend/request.js index 15fed41ff8..7dccdd353a 100644 --- a/app/extend/request.js +++ b/app/extend/request.js @@ -6,10 +6,10 @@ const accepts = require('accepts'); const _querycache = Symbol('_querycache'); const _queriesCache = Symbol('_queriesCache'); const PROTOCOL = Symbol('PROTOCOL'); +const HOST = Symbol('HOST'); const ACCEPTS = Symbol('ACCEPTS'); const IPS = Symbol('IPS'); const RE_ARRAY_KEY = /[^\[\]]+\[\]$/; -const AJAX_EXT_RE = /\.(json|tile|ajax)$/i; module.exports = { /** @@ -18,26 +18,30 @@ module.exports = { * proxy is enabled. * @member {String} Request#host * @example + * ip + port * ```js * this.request.host * => '127.0.0.1:7001' * ``` - * 如果是域名访问,会得到域名 + * or domain * ```js * this.request.host * => 'demo.eggjs.org' * ``` */ get host() { - const host = this.get('x-forwarded-host') || this.get('host'); - if (!host) { - return 'localhost'; + if (this[HOST]) return this[HOST]; + + let host; + if (this.app.config.proxy) { + host = getFromHeaders(this, this.app.config.hostHeaders); } - return host.split(/\s*,\s*/)[0]; + host = host || this.get('host') || ''; + this[HOST] = host.split(/\s*,\s*/)[0]; + return this[HOST]; }, /** - * 由于部署在 Nginx 后面,Koa 原始的实现无法取到正确的 protocol * @member {String} Request#protocol * @example * ```js @@ -46,37 +50,69 @@ module.exports = { * ``` */ get protocol() { - if (this[PROTOCOL]) { - return this[PROTOCOL]; - } - + if (this[PROTOCOL]) return this[PROTOCOL]; + // detect encrypted socket if (this.socket && this.socket.encrypted) { this[PROTOCOL] = 'https'; - return 'https'; + return this[PROTOCOL]; } - - if (typeof this.app.config.protocolHeaders === 'string' && this.app.config.protocolHeaders) { - const protocolHeaders = this.app.config.protocolHeaders.split(','); - for (const header of protocolHeaders) { - let proto = this.get(header); - if (proto) { - proto = this[PROTOCOL] = proto.split(/\s*,\s*/)[0]; - return proto; - } + // get from headers specified in `app.config.protocolHeaders` + if (this.app.config.proxy) { + const proto = getFromHeaders(this, this.app.config.protocolHeaders); + if (proto) { + this[PROTOCOL] = proto.split(/\s*,\s*/)[0]; + return this[PROTOCOL]; } } + // use protocol specified in `app.conig.protocol` + this[PROTOCOL] = this.app.config.protocol || 'http'; + return this[PROTOCOL]; + }, - const proto = this[PROTOCOL] = this.app.config.protocol || 'http'; - return proto; + /** + * Get all pass through ip addresses from the request. + * Enable only on `app.config.proxy = true` + * + * @member {Array} Request#ips + * @example + * ```js + * this.request.ips + * => ['100.23.1.2', '201.10.10.2'] + * ``` + */ + get ips() { + if (this[IPS]) return this[IPS]; + + // return empty array when proxy=false + if (!this.app.config.proxy) { + this[IPS] = []; + return this[IPS]; + } + + const val = getFromHeaders(this, this.app.config.ipHeaders) || ''; + this[IPS] = val ? val.split(/\s*,\s*/) : []; + + let maxIpsCount = this.app.config.maxIpsCount; + // Compatible with maxProxyCount logic (previous logic is wrong, only for compatibility with legacy logic) + if (!maxIpsCount && this.app.config.maxProxyCount) maxIpsCount = this.app.config.maxProxyCount + 1; + + if (maxIpsCount > 0) { + // if maxIpsCount present, only keep `maxIpsCount` ips + // [ illegalIp, clientRealIp, proxyIp1, proxyIp2 ...] + this[IPS] = this[IPS].slice(-maxIpsCount); + } + return this[IPS]; }, /** - * 返回远程 ip 地址,总是返回 ipv4 + * Get the request remote IPv4 address * @member {String} Request#ip + * @return {String} IPv4 address * @example * ```js * this.request.ip * => '127.0.0.1' + * => '111.10.2.1' * ``` */ get ip() { @@ -84,73 +120,47 @@ module.exports = { return this._ip; } const ip = this.ips[0] || this.socket.remoteAddress; - // ::ffff:x.x.x.x/96 是用于IPv4映射地址 - // 如果是 IPV6 也不会做处理,现在还未遇到 IPV6 的场景 + // will be '::ffff:x.x.x.x', should convert to standard IPv4 format // https://zh.wikipedia.org/wiki/IPv6 this._ip = ip && ip.indexOf('::ffff:') > -1 ? ip.substring(7) : ip; return this._ip; }, /** - * 从请求头获取所有 ip - * 1. 先从 `X-Forwarded-For` 获取,这个值是从 spanner 传递过来的,如果前置没有 spanner 返回为空 - * 2. 再从 `X-Real-IP` 获取,这个值为请求 nginx 的客户端 ip,如果前置是非 spanner 的服务器,那么 ip 可能不准确 - * - * @member {String} Request#ips - */ - get ips() { - let ips = this[IPS]; - if (ips) { - return ips; - } - - // TODO: should trust these headers after trust proxy config set - const val = this.get('x-forwarded-for') || this.get('x-real-ip'); - ips = this[IPS] = val - ? val.split(/ *, */) - : []; - - return ips; - }, - - /** - * 判断当前请求是否 AJAX 请求, 具体判断规则: - * - HTTP 包含 `X-Requested-With: XMLHttpRequest` header - * - `ctx.path` 以 `.json`, `.ajax`, `.tile` 为扩展名 - * @member {Boolean} Request#isAjax - * @since 1.0.0 + * Set the request remote IPv4 address + * @member {String} Request#ip + * @param {String} ip - IPv4 address + * @example + * ```js + * this.request.ip + * => '127.0.0.1' + * => '111.10.2.1' + * ``` */ - get isAjax() { - return this.get('x-requested-with') === 'XMLHttpRequest' || AJAX_EXT_RE.test(this.path); + set ip(ip) { + this._ip = ip; }, /** - * 判断当前请求是否接受 JSON 响应 - * 1. 如果是 ajax 请求,认为应该接受 JSON 响应 - * 2. 如果设置过响应类型,通过响应类型来判断 - * 3. 最后通过 accept 来判断 + * detect if response should be json + * 1. url path ends with `.json` + * 2. response type is set to json + * 3. detect by request accept header * * @member {Boolean} Request#acceptJSON - * @since 2.0.0 + * @since 1.0.0 */ get acceptJSON() { - if (this.isAjax) { - return true; - } - if (this.response.type && this.response.type.indexOf('json') >= 0) { - return true; - } - return this.accepts('html', 'text', 'json') === 'json'; + if (this.path.endsWith('.json')) return true; + if (this.response.type && this.response.type.indexOf('json') >= 0) return true; + if (this.accepts('html', 'text', 'json') === 'json') return true; + return false; }, - // 关于如何安全地读取 query 参数的讨论 + // How to read query safely // https://github.com/koajs/qs/issues/5 _customQuery(cacheName, filter) { - const str = this.querystring; - if (!str) { - return {}; - } - + const str = this.querystring || ''; let c = this[cacheName]; if (!c) { c = this[cacheName] = {}; @@ -159,8 +169,8 @@ module.exports = { if (!cacheQuery) { cacheQuery = c[str] = {}; const isQueries = cacheName === _queriesCache; - // querystring.parse 不会解析 a[foo]=1&a[bar]=2 的情况 - const query = querystring.parse(str); + // `querystring.parse` CANNOT parse something like `a[foo]=1&a[bar]=2` + const query = str ? querystring.parse(str) : {}; for (const key in query) { if (!key) { // key is '', like `a=b&` @@ -169,7 +179,7 @@ module.exports = { const value = filter(query[key]); cacheQuery[key] = value; if (isQueries && RE_ARRAY_KEY.test(key)) { - // 支持兼容 this.queries['key'] => this.queries['key[]'] + // `this.queries['key'] => this.queries['key[]']` is compatibly supported const subkey = key.substring(0, key.length - 2); if (!cacheQuery[subkey]) { @@ -182,13 +192,13 @@ module.exports = { }, /** - * 获取当前请求以 querystring 传递的参数,所有参数值都以 String 类型返回 + * get params pass by querystring, all values are of string type. * @member {Object} Request#query * @example * ```js * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 * this.query - * => { 'name': 'Foo', 'age': 20 } + * => { 'name': 'Foo', 'age': '20' } * * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val * this.query @@ -206,7 +216,7 @@ module.exports = { }, /** - * 获取当前请求以 querystring 传递的参数,所有参数值都以 Array 类型返回,类似 {@link Request#query} + * get params pass by querystring, all value are Array type. {@link Request#query} * @member {Array} Request#queries * @example * ```js @@ -237,10 +247,9 @@ module.exports = { /** * Set query-string as an object. * - * @method Request#query + * @function Request#query * @param {Object} obj set querystring and query object for request. * @return {void} - * @api public */ set query(obj) { this.querystring = querystring.stringify(obj); @@ -261,3 +270,13 @@ function arrayValue(value) { } return value; } + +function getFromHeaders(ctx, names) { + if (!names) return ''; + names = names.split(/\s*,\s*/); + for (const name of names) { + const value = ctx.get(name); + if (value) return value; + } + return ''; +} diff --git a/app/extend/response.js b/app/extend/response.js index ea8c359eff..643d53f610 100644 --- a/app/extend/response.js +++ b/app/extend/response.js @@ -1,13 +1,25 @@ 'use strict'; -const getType = require('mime-types').contentType; +const getType = require('cache-content-type'); const isJSON = require('koa-is-json'); +const REAL_STATUS = Symbol('Context#realStatus'); + module.exports = { - set length(n) { + + /** + * Get or set the length of content. + * + * For Get: If the original content length is null or undefined, it will read out + * the body's content length as the return value. + * + * @member {Number} Response#type + * @param {Number} len The content-length to be set. + */ + set length(len) { // copy from koa // change header name to lower case - this.set('content-length', n); + this.set('content-length', len); }, get length() { @@ -26,10 +38,22 @@ module.exports = { return parseInt(len, 10); }, + /** + * Get or set the content-type. + * + * For Set: If type is null or undefined, this property will be removed. + * + * For Get: If the value is null or undefined, an empty string will be returned; + * if you have multiple values seperated by `;`, ONLY the first one will be returned. + * + * @member {String} Response#type + * @param {String} type The content-type to be set. + */ set type(type) { // copy from koa - // change header name to lower case - type = getType(type) || false; + // Different: + // - change header name to lower case + type = getType(type); if (type) { this.set('content-type', type); } else { @@ -43,4 +67,35 @@ module.exports = { if (!type) return ''; return type.split(';')[0]; }, + + /** + * Get or set a real status code. + * + * e.g.: Using 302 status redirect to the global error page + * instead of show current 500 status page. + * And access log should save 500 not 302, + * then the `realStatus` can help us find out the real status code. + * @member {Number} Response#realStatus + * @return {Number} The status code to be set. + */ + get realStatus() { + if (this[REAL_STATUS]) { + return this[REAL_STATUS]; + } + return this.status; + }, + + /** + * Set a real status code. + * + * e.g.: Using 302 status redirect to the global error page + * instead of show current 500 status page. + * And access log should save 500 not 302, + * then the `realStatus` can help us find out the real status code. + * @member {Number} Response#realStatus + * @param {Number} status The status code to be set. + */ + set realStatus(status) { + this[REAL_STATUS] = status; + }, }; diff --git a/app/middleware/meta.js b/app/middleware/meta.js index b0da79186f..9e5283f53c 100644 --- a/app/middleware/meta.js +++ b/app/middleware/meta.js @@ -2,12 +2,19 @@ * meta middleware, should be the first middleware */ -'use strict'; +const { performance } = require('perf_hooks'); -module.exports = () => { - return function* meta(next) { - yield next; +module.exports = options => { + return async function meta(ctx, next) { + if (options.logging) { + ctx.coreLogger.info('[meta] request started, host: %s, user-agent: %s', ctx.host, ctx.header['user-agent']); + } + await next(); // total response time header - this.set('x-readtime', Date.now() - this.starttime); + if (ctx.performanceStarttime) { + ctx.set('x-readtime', Math.floor((performance.now() - ctx.performanceStarttime) * 1000) / 1000); + } else { + ctx.set('x-readtime', Date.now() - ctx.starttime); + } }; }; diff --git a/app/middleware/notfound.js b/app/middleware/notfound.js index 1057931f81..4ec4288f8e 100644 --- a/app/middleware/notfound.js +++ b/app/middleware/notfound.js @@ -1,34 +1,36 @@ 'use strict'; module.exports = options => { - return function* notfound(next) { - yield next; + return async function notfound(ctx, next) { + await next(); - if (this.status !== 404 || this.body) { + if (ctx.status !== 404 || ctx.body) { return; } // set status first, make sure set body not set status - this.status = 404; + ctx.status = 404; - if (this.acceptJSON) { - this.body = { + if (ctx.acceptJSON) { + ctx.body = { message: 'Not Found', }; return; } - if (options.enableRedirect && options.pageUrl) { - this.realStatus = 404; - this.redirect(options.pageUrl); + const notFoundHtml = '

404 Not Found

'; + + // notfound handler is unimplemented + if (options.pageUrl && ctx.path === options.pageUrl) { + ctx.body = `${notFoundHtml}

config.notfound.pageUrl(${options.pageUrl})
is unimplemented

`; return; } - const title = '

404 Not Found

'; - if (!options.enableRedirect && options.pageUrl) { - this.body = `${title}Because you are in a non-prod environment, you will be looking at this page, otherwise it will jump to ${options.pageUrl}`; - } else { - this.body = title; - } + if (options.pageUrl) { + ctx.realStatus = 404; + ctx.redirect(options.pageUrl); + return; + } + ctx.body = notFoundHtml; }; }; diff --git a/app/middleware/site_file.js b/app/middleware/site_file.js index 7e1cc2e5fb..4e3ff16b2c 100644 --- a/app/middleware/site_file.js +++ b/app/middleware/site_file.js @@ -1,29 +1,31 @@ 'use strict'; const path = require('path'); -const MAX_AGE = 'public, max-age=2592000'; // 30 days module.exports = options => { - return function* siteFile(next) { - if (this.method !== 'HEAD' && this.method !== 'GET') return yield next; + return async function siteFile(ctx, next) { + if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return next(); + /* istanbul ignore if */ + if (ctx.path[0] !== '/') return next(); - if (!options.hasOwnProperty(this.path)) return yield next; + let content = options[ctx.path]; + if (!content) return next(); - const content = options[this.path]; - - // '/favicon.ico': 'https://eggjs.org/favicon.ico', + // '/favicon.ico': 'https://eggjs.org/favicon.ico' or '/favicon.ico': async (ctx) => 'https://eggjs.org/favicon.ico' + // content is function + if (typeof content === 'function') content = await content(ctx); // content is url - if (typeof content === 'string') return this.redirect(content); + if (typeof content === 'string') return ctx.redirect(content); // '/robots.txt': Buffer { - require('./koa'); - require('./toa'); -}; diff --git a/benchmarks/simple/app/controller/home.js b/benchmarks/simple/app/controller/home.js deleted file mode 100644 index 6db53282db..0000000000 --- a/benchmarks/simple/app/controller/home.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.body = 'Hello World, egg'; -}; diff --git a/benchmarks/simple/config/config.prod.js b/benchmarks/simple/config/config.prod.js deleted file mode 100644 index 027a8dc963..0000000000 --- a/benchmarks/simple/config/config.prod.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const path = require('path'); - -exports.keys = 'foo'; - -exports.logger = { - dir: path.join(__dirname, '../logs'), -}; - -exports.alinode = { - enable: !!process.env.ALINODE_ENABLE, - appid: process.env.ALINODE_APPID, - server: process.env.ALINODE_SERVER, - secret: process.env.ALINODE_SECRET, -}; diff --git a/benchmarks/simple/config/plugin.js b/benchmarks/simple/config/plugin.js deleted file mode 100644 index bffdc7df60..0000000000 --- a/benchmarks/simple/config/plugin.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -exports.alinode = { - enable: !!process.env.ALINODE_ENABLE, - package: 'egg-alinode', -}; diff --git a/benchmarks/simple/dispatch.js b/benchmarks/simple/dispatch.js deleted file mode 100644 index 80ddeeb385..0000000000 --- a/benchmarks/simple/dispatch.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - workers: Number(process.argv[2] || require('os').cpus().length), - baseDir: __dirname, -}); diff --git a/benchmarks/simple/koa.js b/benchmarks/simple/koa.js deleted file mode 100644 index 786dc5c21d..0000000000 --- a/benchmarks/simple/koa.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const koa = require('koa'); -const router = require('koa-router')(); - -const app = koa(); -let n = 15; - -while (n--) { - app.use(function* (next) { - yield next; - }); -} - -app.use(router.routes()); - -router.get('/', function* () { - this.body = 'Hello World, koa'; -}); - -console.log('koa app listen on 7002'); -app.listen(7002); diff --git a/benchmarks/simple/package.json b/benchmarks/simple/package.json deleted file mode 100644 index 6d41fe69cb..0000000000 --- a/benchmarks/simple/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "simple" -} diff --git a/benchmarks/simple/run.sh b/benchmarks/simple/run.sh deleted file mode 100755 index 5b1babf14b..0000000000 --- a/benchmarks/simple/run.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -echo -EGG_SERVER_ENV=prod node `dirname $0`/dispatch.js $1 & -pid=$! - -sleep 3 -echo "------- egg hello -------" -curl 'http://127.0.0.1:7001/' -echo "" -wrk 'http://127.0.0.1:7001/' \ - -d 10 \ - -c 50 \ - -t 8 - -sleep 3 -echo "------- koa hello -------" -curl 'http://127.0.0.1:7002/' -echo "" -wrk 'http://127.0.0.1:7002/' \ - -d 10 \ - -c 50 \ - -t 8 - -sleep 3 -echo "------- toa hello -------" -curl 'http://127.0.0.1:7003/' -echo "" -wrk 'http://127.0.0.1:7003/' \ - -d 10 \ - -c 50 \ - -t 8 - -kill $pid diff --git a/benchmarks/simple/toa.js b/benchmarks/simple/toa.js deleted file mode 100644 index df2e4d2fbd..0000000000 --- a/benchmarks/simple/toa.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const toa = require('toa'); -const Router = require('toa-router'); - -const router = new Router(); -const app = toa(); -let n = 15; - -while (n--) { - app.use(function* (next) { - yield next; - }); -} - -router.get('/', function* () { - this.body = 'Hello World, toa'; -}); - -app.use(router.toThunk()); -console.log('toa app listen on 7003'); -app.listen(7003); diff --git a/benchmarks/simple_view/app.js b/benchmarks/simple_view/app.js deleted file mode 100644 index 4879e20f3e..0000000000 --- a/benchmarks/simple_view/app.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = () => { - require('./koa'); - require('./toa'); -}; diff --git a/benchmarks/simple_view/app/controller/home.js b/benchmarks/simple_view/app/controller/home.js deleted file mode 100644 index 270f09b59a..0000000000 --- a/benchmarks/simple_view/app/controller/home.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = function* () { - yield this.render('home.html', { - user: { - name: 'foobar', - }, - title: 'egg view example', - }); -}; diff --git a/benchmarks/simple_view/app/view/component/nav.html b/benchmarks/simple_view/app/view/component/nav.html deleted file mode 100644 index c40a2d8c92..0000000000 --- a/benchmarks/simple_view/app/view/component/nav.html +++ /dev/null @@ -1,33 +0,0 @@ - - diff --git a/benchmarks/simple_view/app/view/home.html b/benchmarks/simple_view/app/view/home.html deleted file mode 100644 index 862b49de3e..0000000000 --- a/benchmarks/simple_view/app/view/home.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} - -
-

egg view example here, welcome {{ user.name }}

-
- -{% endblock %} diff --git a/benchmarks/simple_view/app/view/layout.html b/benchmarks/simple_view/app/view/layout.html deleted file mode 100644 index 821c565300..0000000000 --- a/benchmarks/simple_view/app/view/layout.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - {{title}} - - - - - - - - {% block nav %}{% include "component/nav.html" %}{% endblock %} - -
- {% block body %}{% endblock %} -
- -
-
-

Maintained by the eggjs team.

- -
-
- - - - - diff --git a/benchmarks/simple_view/config/config.prod.js b/benchmarks/simple_view/config/config.prod.js deleted file mode 100644 index 027a8dc963..0000000000 --- a/benchmarks/simple_view/config/config.prod.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const path = require('path'); - -exports.keys = 'foo'; - -exports.logger = { - dir: path.join(__dirname, '../logs'), -}; - -exports.alinode = { - enable: !!process.env.ALINODE_ENABLE, - appid: process.env.ALINODE_APPID, - server: process.env.ALINODE_SERVER, - secret: process.env.ALINODE_SECRET, -}; diff --git a/benchmarks/simple_view/config/plugin.js b/benchmarks/simple_view/config/plugin.js deleted file mode 100644 index 3f42be7aff..0000000000 --- a/benchmarks/simple_view/config/plugin.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -exports.view = { - enable: true, - package: 'egg-view-nunjucks', -}; - -exports.alinode = { - enable: !!process.env.ALINODE_ENABLE, - package: 'egg-alinode', -}; diff --git a/benchmarks/simple_view/dispatch.js b/benchmarks/simple_view/dispatch.js deleted file mode 100644 index 80ddeeb385..0000000000 --- a/benchmarks/simple_view/dispatch.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - workers: Number(process.argv[2] || require('os').cpus().length), - baseDir: __dirname, -}); diff --git a/benchmarks/simple_view/koa.js b/benchmarks/simple_view/koa.js deleted file mode 100644 index 89a1d557dc..0000000000 --- a/benchmarks/simple_view/koa.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const koa = require('koa'); -const nunjucks = require('nunjucks'); -const path = require('path'); -const router = require('koa-router')(); - -const app = koa(); -let n = 15; - -while (n--) { - app.use(function* (next) { - yield next; - }); -} - -app.use(router.routes()); - -const options = { - noCache: false, -}; -const viewPaths = path.join(__dirname, 'app/view'); -const engine = new nunjucks.Environment(new nunjucks.FileSystemLoader(viewPaths, options), options); - -function render(name, locals) { - return new Promise((resolve, reject) => { - engine.render(name, locals, function(err, result) { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); -} - -router.get('/', function* () { - this.body = yield render('home.html', { - user: { - name: 'fookoa', - }, - title: 'koa view example', - }); -}); - -console.log('koa app listen on 7002'); -app.listen(7002); diff --git a/benchmarks/simple_view/package.json b/benchmarks/simple_view/package.json deleted file mode 100644 index 03a115fb48..0000000000 --- a/benchmarks/simple_view/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "simple-view" -} diff --git a/benchmarks/simple_view/run.sh b/benchmarks/simple_view/run.sh deleted file mode 100755 index 9ed985ba65..0000000000 --- a/benchmarks/simple_view/run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -echo -EGG_SERVER_ENV=prod node `dirname $0`/dispatch.js $1 & -pid=$! - -sleep 3 -echo "------- egg view -------" -curl 'http://127.0.0.1:7001/' -s | grep 'title' -echo "" -wrk 'http://127.0.0.1:7001/' \ - -d 10 \ - -c 50 \ - -t 8 - -sleep 3 -echo "------- koa view -------" -curl 'http://127.0.0.1:7002/' -s | grep 'title' -echo "" -wrk 'http://127.0.0.1:7002/' \ - -d 10 \ - -c 50 \ - -t 8 - -sleep 3 -echo "------- toa view -------" -curl 'http://127.0.0.1:7003/' -s | grep 'title' -echo "" -wrk 'http://127.0.0.1:7003/' \ - -d 10 \ - -c 50 \ - -t 8 -kill $pid diff --git a/benchmarks/simple_view/toa.js b/benchmarks/simple_view/toa.js deleted file mode 100644 index e9974a2533..0000000000 --- a/benchmarks/simple_view/toa.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const toa = require('toa'); -const nunjucks = require('nunjucks'); -const path = require('path'); -const Router = require('toa-router'); - -const router = new Router(); -const app = toa(); -let n = 15; - -while (n--) { - app.use(function* (next) { - yield next; - }); -} - -const options = { - noCache: false, -}; -const viewPaths = path.join(__dirname, 'app/view'); -const engine = new nunjucks.Environment(new nunjucks.FileSystemLoader(viewPaths, options), options); - -function render(name, locals) { - return new Promise((resolve, reject) => { - engine.render(name, locals, function(err, result) { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); -} - -router.get('/', function* () { - this.body = yield render('home.html', { - user: { - name: 'footoa', - }, - title: 'toa view example', - }); -}); - -app.use(router.toThunk()); -console.log('toa app listen on 7003'); -app.listen(7003); diff --git a/config/config.default.js b/config/config.default.js index d0999ebaf8..534fba9891 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -3,122 +3,232 @@ const fs = require('fs'); const path = require('path'); -module.exports = appInfo => { - // appInfo contains name, baseDir, env, HOME, and pkg +/** + * The configuration of egg application, can be access by `app.config` + * @class Config + * @since 1.0.0 + */ - const appRoot = appInfo.env === 'local' || appInfo.env === 'unittest' ? appInfo.baseDir : appInfo.HOME; +module.exports = appInfo => { - const exports = { + const config = { /** - * runtime env + * The environment of egg * @member {String} Config#env + * @see {appInfo#env} * @since 1.0.0 */ env: appInfo.env, /** - * app name + * The name of the application * @member {String} Config#name + * @see {appInfo#name} * @since 1.0.0 */ name: appInfo.name, /** - * Should set by every app itself + * The key that signing cookies. It can contain multiple keys seperated by `,`. * @member {String} Config#keys + * @see http://eggjs.org/en/core/cookie-and-session.html#cookie-secret-key + * @default * @since 1.0.0 */ keys: '', /** - * Detect request, protocol header, case sensitive. - * If your app behind a proxy, like nginx, maybe you should set it to `x-forwarded-proto` + * default cookie options + * + * @member Config#cookies + * @property {String} sameSite - SameSite property, defaults is '' + * @property {Boolean} httpOnly - httpOnly property, defaults is true + */ + cookies: { + // httpOnly: true | false, + // sameSite: 'none|lax|strict', + }, + + /** + * Whether application deployed after a reverse proxy, + * when true proxy header fields will be trusted + * @member {Boolean} Config#proxy + * @default + * @since 1.0.0 + */ + proxy: false, + + /** + * + * max ips read from proxy ip header, default to 0 (means infinity) + * to prevent users from forging client ip addresses via x-forwarded-for + * @see https://github.com/koajs/koa/blob/master/docs/api/request.md#requestips + * @member {Integer} Config#maxIpsCount + * @default + * @since 2.25.0 + */ + maxIpsCount: 0, + + /** + * please use maxIpsCount instead + * @member {Integer} Config#maxProxyCount + * @default + * @since 2.21.0 + * @deprecated + */ + maxProxyCount: 0, + + /** + * Detect request's protocol from specified headers, not case-sensitive. + * Only worked when config.proxy set to true. * @member {String} Config#protocolHeaders + * @default * @since 1.0.0 */ - protocolHeaders: '', + protocolHeaders: 'x-forwarded-proto', /** - * package.json object + * Detect request' ip from specified headers, not case-sensitive. + * Only worked when config.proxy set to true. + * @member {String} Config#ipHeaders + * @default + * @since 1.0.0 + */ + ipHeaders: 'x-forwarded-for', + + /** + * Detect request' host from specified headers, not case-sensitive. + * Only worked when config.proxy set to true. + * @member {String} Config#hostHeaders + * @default + * @since 1.0.0 + */ + hostHeaders: '', + + /** + * package.json * @member {Object} Config#pkg + * @see {appInfo#pkg} * @since 1.0.0 */ pkg: appInfo.pkg, /** - * app base dir + * The current directory of the application * @member {String} Config#baseDir + * @see {appInfo#baseDir} * @since 1.0.0 */ baseDir: appInfo.baseDir, /** - * current user HOME dir + * The current HOME directory * @member {String} Config#HOME + * @see {appInfo#HOME} * @since 1.0.0 */ HOME: appInfo.HOME, /** - * store runtime info dir + * The directory of server running. You can find `application_config.json` under it that is dumpped from `app.config`. * @member {String} Config#rundir + * @default * @since 1.0.0 - * @private */ rundir: path.join(appInfo.baseDir, 'run'), + + /** + * dump config + * + * It will ignore special keys when dumpConfig + * + * @member Config#dump + * @property {Set} ignore - keys to ignore + */ + dump: { + ignore: new Set([ + 'pass', 'pwd', 'passd', 'passwd', 'password', 'keys', 'masterKey', 'accessKey', + // ignore any key contains "secret" keyword + /secret/i, + ]), + timing: { + // if boot action >= slowBootActionMinDuration, egg core will print it to warnning log + slowBootActionMinDuration: 5000, + }, + }, + + /** + * configurations are confused to users + * { + * [unexpectedKey]: [expectedKey], + * } + * @member Config#confusedConfigurations + * @type {Object} + */ + confusedConfigurations: { + bodyparser: 'bodyParser', + notFound: 'notfound', + sitefile: 'siteFile', + middlewares: 'middleware', + httpClient: 'httpclient', + }, }; /** - * notfound 中间件 options - * - * 指定应用 404 页面 + * The option of `notfound` middleware * - * 只有在 `enableRedirect === true && pageUrl` 才会真正 302 跳转到友好的404页面。 + * It will return page or json depend on negotiation when 404, + * If pageUrl is set, it will redirect to the page. * * @member Config#notfound - * @property {String} pageUrl - 默认为空,不设置全局统一 404 页面 - * @property {Boolean} enableRedirect - 是否跳转到 global404Url,默认在 local 为 false,其他为 true - * ``` + * @property {String} pageUrl - the 404 page url */ - exports.notfound = { + config.notfound = { pageUrl: '', - enableRedirect: appInfo.env === 'prod', }; /** - * siteFile options - * @member Config#siteFile - * key 值为 path,若 value 为 url,则 redirect,若 value 为文件 buffer,则直接返回此文件 - * @example - * - 指定应用 favicon, => '/favicon.ico': 'https://eggjs.org/favicon.ico', - * - 指定应用 crossdomain.xml, => '/crossdomain.xml': fs.readFileSync('path_to_file') - * - 指定应用 robots.txt, => '/robots.txt': fs.readFileSync('path_to_file') + * The option of `siteFile` middleware + * + * You can map some files using this options, it will response immdiately when matching. * - * ```js - * exports.siteFile = { + * @member {Object} Config#siteFile - key is path, and value is url or buffer. + * @property {String} cacheControl - files cache , default is public, max-age=2592000 + * @example + * // specific app's favicon, => '/favicon.ico': 'https://eggjs.org/favicon.ico', + * config.siteFile = { * '/favicon.ico': 'https://eggjs.org/favicon.ico', * }; */ - exports.siteFile = { + config.siteFile = { '/favicon.ico': fs.readFileSync(path.join(__dirname, 'favicon.png')), + // default cache in 30 days + cacheControl: 'public, max-age=2592000', }; /** - * bodyParser options + * The option of `bodyParser` middleware + * * @member Config#bodyParser - * @property {String} encoding - body 的编码格式,默认为 utf8 - * @property {String} formLimit - form body 的大小限制,默认为 100kb - * @property {String} jsonLimit - json body 的大小限制,默认为 100kb - * @property {Boolean} strict - json body 解析是否为严格模式,如果为严格模式则只接受 object 和 array - * @property {Number} queryString.arrayLimit - 表单元素数组长度限制,默认 100,否则会转换为 json 格式 - * @property {Number} queryString.depth - json 数值深度限制,默认 5 - * @property {Number} queryString.parameterLimit - 参数个数限制,默认 1000 + * @property {Boolean} enable - enable bodyParser or not, default is true + * @property {String | RegExp | Function | Array} ignore - won't parse request body when url path hit ignore pattern, can not set `ignore` when `match` presented + * @property {String | RegExp | Function | Array} match - will parse request body only when url path hit match pattern + * @property {String} encoding - body's encoding type,default is utf8 + * @property {String} formLimit - limit of the urlencoded body. If the body ends up being larger than this limit, a 413 error code is returned. Default is 1mb + * @property {String} jsonLimit - limit of the json body, default is 1mb + * @property {String} textLimit - limit of the text body, default is 1mb + * @property {Boolean} strict - when set to true, JSON parser will only accept arrays and objects. Default is true + * @property {Number} queryString.arrayLimit - urlencoded body array's max length, default is 100 + * @property {Number} queryString.depth - urlencoded body object's max depth, default is 5 + * @property {Number} queryString.parameterLimit - urlencoded body maximum parameters, default is 1000 */ - exports.bodyParser = { + config.bodyParser = { + enable: true, encoding: 'utf8', - formLimit: '100kb', - jsonLimit: '100kb', + formLimit: '1mb', + jsonLimit: '1mb', + textLimit: '1mb', strict: true, // @see https://github.com/hapijs/qs/blob/master/lib/parse.js#L8 for more options queryString: { @@ -126,31 +236,44 @@ module.exports = appInfo => { depth: 5, parameterLimit: 1000, }, + onerror(err, ctx) { + err.message += ', check bodyParser config'; + if (ctx.status === 404) { + // set default status to 400, meaning client bad request + ctx.status = 400; + if (!err.status) { + err.status = 400; + } + } + throw err; + }, }; /** * logger options * @member Config#logger - * @property {String} dir - 日志存储目录 - * @property {String} rotateLogDirs - 自动按日切割的目录 - * @property {String} encoding - 日志文件编码,预发和生产环境默认是 gbk,其他环境是 utf8 - * @property {String} level - 默认保存的日志级别,可选值: DEBUG, INFO, WARN, ERROR, NONE, 生产环境默认 INFO - * @property {String} env - 运行环境,等价于 antx.env - * @property {String} consoleLevel - 默认输出到标准输出的日志级别,本地开发环境默认是 INFO,单元测试 WARN,其他环境都是 NONE - * @property {Boolean} outputJSON - 是否输出 json 格式的日志,用于阿里监控。除非你明确知道自己想做什么,其他情况都不要配置 - * @property {Boolean} buffer - 是否开启磁盘写入缓存,默认 true - * @property {String} errorLogName - 异常日志文件名 - * @property {String} coreLogName - egg core 日志文件名 - * @property {String} agentLogName - agent worker 进程日志文件名 - * @property {Object} coreLogger - core logger 的自定义配置 + * @property {String} dir - directory of log files + * @property {String} encoding - log file encoding, defaults to utf8 + * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production + * @property {String} consoleLevel - log level of stdout, defaults to INFO in local serverEnv, defaults to WARN in unittest, defaults to NONE elsewise + * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. + * @property {Boolean} outputJSON - log as JSON or not, defaults to false + * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true + * @property {String} errorLogName - file name of errorLogger + * @property {String} coreLogName - file name of coreLogger + * @property {String} agentLogName - file name of agent worker log + * @property {Object} coreLogger - custom config of coreLogger + * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false + * @property {Boolean} enablePerformanceTimer - using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to false. e.g.: logger will set 1.456ms instead of 1ms. + * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false */ - exports.logger = { - dir: path.join(appRoot, 'logs', appInfo.name), - rotateLogDirs: [ path.join(appRoot, 'logs', appInfo.name) ], + config.logger = { + dir: path.join(appInfo.root, 'logs', appInfo.name), encoding: 'utf8', env: appInfo.env, level: 'INFO', consoleLevel: 'INFO', + disableConsoleAfterReady: appInfo.env !== 'local' && appInfo.env !== 'unittest', outputJSON: false, buffer: true, appLogName: `${appInfo.name}-web.log`, @@ -158,30 +281,72 @@ module.exports = appInfo => { agentLogName: 'egg-agent.log', errorLogName: 'common-error.log', coreLogger: {}, + allowDebugAtProd: false, + enablePerformanceTimer: false, + enableFastContextLogger: false, + }; + + /** + * The option for httpclient + * @member Config#httpclient + * @property {Boolean} enableDNSCache - Enable DNS lookup from local cache or not, default is false. + * @property {Boolean} dnsCacheLookupInterval - minimum interval of DNS query on the same hostname (default 10s). + * + * @property {Number} request.timeout - httpclient request default timeout, default is 5000 ms. + * + * @property {Boolean} httpAgent.keepAlive - Enable http agent keepalive or not, default is true + * @property {Number} httpAgent.freeSocketTimeout - http agent socket keepalive max free time, default is 4000 ms. + * @property {Number} httpAgent.maxSockets - http agent max socket number of one host, default is `Number.MAX_SAFE_INTEGER` @ses https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER + * @property {Number} httpAgent.maxFreeSockets - http agent max free socket number of one host, default is 256. + * + * @property {Boolean} httpsAgent.keepAlive - Enable https agent keepalive or not, default is true + * @property {Number} httpsAgent.freeSocketTimeout - httpss agent socket keepalive max free time, default is 4000 ms. + * @property {Number} httpsAgent.maxSockets - https agent max socket number of one host, default is `Number.MAX_SAFE_INTEGER` @ses https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER + * @property {Number} httpsAgent.maxFreeSockets - https agent max free socket number of one host, default is 256. + * @property {Boolean} useHttpClientNext - use urllib@3 HttpClient, default is false + * @property {Boolean} allowH2 - use urllib@4 HttpClient and enable H2, default is false. Only works on Node.js >= 18 + */ + config.httpclient = { + enableDNSCache: false, + dnsCacheLookupInterval: 10000, + dnsCacheMaxLength: 1000, + + request: { + timeout: 5000, + }, + httpAgent: { + keepAlive: true, + freeSocketTimeout: 4000, + maxSockets: Number.MAX_SAFE_INTEGER, + maxFreeSockets: 256, + }, + httpsAgent: { + keepAlive: true, + freeSocketTimeout: 4000, + maxSockets: Number.MAX_SAFE_INTEGER, + maxFreeSockets: 256, + }, + useHttpClientNext: false, + // allowH2: false, }; /** - * urllib options - * @member Config#urllib - * @property {Boolean} keepAlive - 是否开启 http keepalive, 默认是 true - * @property {Integer} keepAliveTimeout - socket 最长空闲时间, 单位毫秒, 默认是 30000 毫秒 - * @property {Integer} timeout - socket 最长不活跃时间, 单位毫秒, 默认是 30000 毫秒 - * @property {Integer} maxSockets - 对单个 host 的最大 socket 数, 默认是 Infinity 无限制 - * @property {Integer} maxFreeSockets - 对单个 host 的最大空闲 socket 数, 默认是 256 + * The option of `meta` middleware + * + * @member Config#meta + * @property {Boolean} enable - enable meta or not, default is true + * @property {Boolean} logging - enable logging start request, default is false */ - exports.urllib = { - keepAlive: true, - keepAliveTimeout: 30000, - timeout: 30000, - maxSockets: Infinity, - maxFreeSockets: 256, + config.meta = { + enable: true, + logging: false, }; /** - * 定义 core 中间件加载的顺序,中间件的名称会映射到 app.middlewares 上 + * core enable middlewares * @member {Array} Config#middleware */ - exports.coreMiddleware = [ + config.coreMiddleware = [ 'meta', 'siteFile', 'notfound', @@ -190,21 +355,70 @@ module.exports = appInfo => { ]; /** - * jsonp options - * @member Config#jsonp - * @property {String} callback - jsonp 的 callback 方法参数名,默认为 `_callback` - * @property {Number} limit - callback 方法名称最大长度,默认为 `50` + * emit `startTimeout` if worker don't ready after `workerStartTimeout` ms + * @member {Number} Config.workerStartTimeout + */ + config.workerStartTimeout = 10 * 60 * 1000; + + /** + * server timeout in milliseconds, default to 0 (no timeout). + * + * for special request, just use `ctx.req.setTimeout(ms)` + * + * @member {Number} Config#serverTimeout + * @see https://nodejs.org/api/http.html#http_server_timeout */ - exports.jsonp = { - callback: '_callback', - limit: 50, + config.serverTimeout = null; + + /** + * + * @member {Object} Config#cluster + * @property {Object} listen - listen options, see {@link https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback} + * @property {String} listen.path - set a unix sock path when server listen + * @property {Number} listen.port - set a port when server listen + * @property {String} listen.hostname - set a hostname binding server when server listen + */ + config.cluster = { + listen: { + path: '', + port: 7001, + hostname: '', + }, }; /** - * emit `startTimeout` if worker don't ready after `workerStartTimeout` ms - * @member {Number} Config.workerStartTimeout + * @property {Number} responseTimeout - response timeout, default is 60000 + */ + config.clusterClient = { + maxWaitTime: 60000, + responseTimeout: 60000, + }; + + /** + * This function / async function will be called when a client error occurred and return the response. + * + * The arguments are `err`, `socket` and `application` which indicate current client error object, current socket + * object and the application object. + * + * The response to be returned should include properties below: + * + * @member {Function} Config#onClientError + * @property [body] {String|Buffer} - the response body + * @property [status] {Number} - the response status code + * @property [headers] {Object} - the response header key-value pairs + * + * @example + * exports.onClientError = async (err, socket, app) => { + * return { + * body: 'error', + * status: 400, + * headers: { + * 'powered-by': 'Egg.js', + * } + * }; + * } */ - exports.workerStartTimeout = 10 * 60 * 1000; + config.onClientError = null; - return exports; + return config; }; diff --git a/config/config.local.js b/config/config.local.js index a3f32dff03..2924388133 100644 --- a/config/config.local.js +++ b/config/config.local.js @@ -1,14 +1,7 @@ 'use strict'; -module.exports = { - logger: { - // 开发环境,将 INFO 以上级别的应用日志和 WARN 以上级别的系统日志输出到 stdout - level: 'DEBUG', - consoleLevel: 'INFO', - coreLogger: { - consoleLevel: 'WARN', - }, - // 在 local 或者 unittest 环境下,默认不缓存日志,直接写入磁盘 - buffer: false, +exports.logger = { + coreLogger: { + consoleLevel: 'WARN', }, }; diff --git a/config/config.unittest.js b/config/config.unittest.js index 7f2b98f621..7c5c5e4c6d 100644 --- a/config/config.unittest.js +++ b/config/config.unittest.js @@ -2,7 +2,6 @@ module.exports = { logger: { - // 默认不缓存日志,直接写入磁盘 consoleLevel: 'WARN', buffer: false, }, diff --git a/config/favicon.png b/config/favicon.png index 7c47099a79..5b8a2587de 100644 Binary files a/config/favicon.png and b/config/favicon.png differ diff --git a/config/plugin.js b/config/plugin.js index 89427e31ee..da864347eb 100644 --- a/config/plugin.js +++ b/config/plugin.js @@ -4,7 +4,7 @@ module.exports = { // enable plugins /** - * app global error handler + * app global Error Handling * @member {Object} Plugin#onerror * @property {Boolean} enable - `true` by default */ @@ -13,28 +13,6 @@ module.exports = { package: 'egg-onerror', }, - /** - * userservice - * @member {Object} Plugin#userservice - * @property {Boolean} enable - `true` by default - * @since 1.0.0 - */ - userservice: { - enable: true, - package: 'egg-userservice', - }, - - /** - * userrole - * @member {Object} Plugin#userrole - * @property {Boolean} enable - `true` by default - * @since 1.0.0 - */ - userrole: { - enable: true, - package: 'egg-userrole', - }, - /** * session * @member {Object} Plugin#session @@ -57,17 +35,6 @@ module.exports = { package: 'egg-i18n', }, - /** - * Validate Plugin - * @member {Object} Plugin#validate - * @property {Boolean} enable - `true` by default - * @since 1.0.0 - */ - validate: { - enable: true, - package: 'egg-validate', - }, - /** * file and dir watcher * @member {Object} Plugin#watcher @@ -113,7 +80,7 @@ module.exports = { }, /** - * logger file rotater + * logger file rotator * @member {Object} Plugin#logrotator * @property {Boolean} enable - `true` by default * @since 1.0.0 @@ -134,38 +101,36 @@ module.exports = { package: 'egg-schedule', }, - // disable plugins - /** - * RESTful API - * @member {Object} Plugin#rest - * @property {Boolean} enable - 默认 false + * `app/public` dir static serve + * @member {Object} Plugin#static + * @property {Boolean} enable - `true` by default * @since 1.0.0 */ - rest: { - enable: false, - package: 'egg-rest', + static: { + enable: true, + package: 'egg-static', }, /** - * `app/public` dir static serve - * @member {Object} Plugin#static - * @property {Boolean} enable - `false` by default + * jsonp support for egg + * @member {Function} Plugin#jsonp + * @property {Boolean} enable - `true` by default * @since 1.0.0 */ - static: { - enable: false, - package: 'egg-static', + jsonp: { + enable: true, + package: 'egg-jsonp', }, /** - * CORS - * @member {Object} Plugin#cors - * @property {Boolean} enable - `false` by default + * view plugin + * @member {Function} Plugin#view + * @property {Boolean} enable - `true` by default * @since 1.0.0 */ - cors: { - enable: false, - package: 'egg-cors', + view: { + enable: true, + package: 'egg-view', }, }; diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index b6b2af6834..0000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,30 +0,0 @@ -title: egg -subtitle: "Born to build better enterprise frameworks and apps" -description: "Born to build better enterprise frameworks and apps" -language: -- en -- zh-cn -timezone: UTC -url: https://eggjs.org -root: / -archive_dir: news -permalink: news/:year/:month/:day/:title/ -new_post_name: :year-:month-:day-:title.md # File name of new posts -post_asset_folder: true -highlight: - enable: true - line_number: false -per_page: 0 - -# Extensions -## Plugins: https://hexo.io/plugins/ -## Themes: https://hexo.io/themes/ -theme: egg - -less: - compress: true - -# Deployment -## Docs: https://hexo.io/docs/deployment.html -deploy: - type: diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 7a04a8c5a1..0000000000 --- a/docs/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "egg", - "version": "0.0.0", - "private": true, - "hexo": { - "version": "3.2.2" - }, - "dependencies": { - "hexo": "^3.2.0", - "hexo-generator-index": "^0.2.0", - "hexo-generator-tag": "^0.2.0", - "hexo-renderer-marked": "^0.2.10", - "hexo-renderer-less": "^0.2.0", - "hexo-server": "^0.2.0" - } -} diff --git a/docs/plugins.puml b/docs/plugins.puml deleted file mode 100644 index a19b2ca8cd..0000000000 --- a/docs/plugins.puml +++ /dev/null @@ -1,7 +0,0 @@ - -@startuml -digraph world { - "onerror"; - "session"; -} -@enduml diff --git a/docs/scripts/helpers.js b/docs/scripts/helpers.js deleted file mode 100644 index 0bed8c536d..0000000000 --- a/docs/scripts/helpers.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -/* global hexo */ - -hexo.extend.helper.register('guide_toc', function() { - const toc = this.site.data.guide_toc; - let menu = '
'; - - for (const title in toc) { - const subMenu = toc[title]; - menu += `
${this.__('guide_toc.' + title)}
'; - } - - menu += '
'; - return menu; -}); - -hexo.extend.helper.register('menu_link', function() { - const menus = this.site.data.menu; - - let links = ''; - for (const menu in menus) { - let link = menus[menu]; - const content = this.__(`menu.${menu}`); - if (menu === 'guide' && this.page.lang !== 'en') { - link = '/' + this.page.lang + link; - } - links += `
  • ${content}
  • `; - } - - return links; -}); - -hexo.extend.helper.register('index_link', function() { - if (this.page.lang !== 'en') { - return `/${this.page.lang}/`; - } - return '/'; -}); diff --git a/docs/source/_data/guide_toc.yml b/docs/source/_data/guide_toc.yml deleted file mode 100644 index da4b79f3c9..0000000000 --- a/docs/source/_data/guide_toc.yml +++ /dev/null @@ -1,19 +0,0 @@ -Guide: - Overview: /guide/ - QuickStart: /guide/quickstart.html - "Write Application": /guide/write_application.html - "Core Plugins": /guide/plugins.html - Deployment: /guide/deployment.html - "Unit Test": /guide/unittest.html -Advanced: - "Write Framework": /advanced/write_framework.html - "Write Plugin": /advanced/write_plugin.html - Cluster: /advanced/cluster.html - Loader: /advanced/loader.html - Singleton: /advanced/singleton.html - "Best Practice": /advanced/best_practice.html -Community: - Contributing: /contributing.html - Member Guide: /member_guide.html - Frameworks: /frameworks.html - FAQ: /faq.html diff --git a/docs/source/_data/menu.yml b/docs/source/_data/menu.yml deleted file mode 100644 index ec3b2da9f7..0000000000 --- a/docs/source/_data/menu.yml +++ /dev/null @@ -1,4 +0,0 @@ -guide: /guide/ -# api: /api -plugins: https://eggjs.org/badgeboard/ -release: /release/ diff --git a/docs/source/_data/plugins.yml b/docs/source/_data/plugins.yml deleted file mode 100644 index 46dc59d281..0000000000 --- a/docs/source/_data/plugins.yml +++ /dev/null @@ -1 +0,0 @@ -- egg-logrotator diff --git a/docs/source/advanced/best_practice.md b/docs/source/advanced/best_practice.md deleted file mode 100644 index 0c20d64de1..0000000000 --- a/docs/source/advanced/best_practice.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Best Practice ---- - -# Best Practice - -TBD diff --git a/docs/source/advanced/cluster.md b/docs/source/advanced/cluster.md deleted file mode 100644 index e0a6f34bec..0000000000 --- a/docs/source/advanced/cluster.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Cluster ---- - -# Cluster - -TBD diff --git a/docs/source/advanced/loader.md b/docs/source/advanced/loader.md deleted file mode 100644 index 62c1ea6b61..0000000000 --- a/docs/source/advanced/loader.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Loader ---- - -# Loader - -TBD diff --git a/docs/source/advanced/singleton.md b/docs/source/advanced/singleton.md deleted file mode 100644 index 3fc7a18adc..0000000000 --- a/docs/source/advanced/singleton.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Singleton ---- - -# Singleton - -TBD diff --git a/docs/source/advanced/write_framework.md b/docs/source/advanced/write_framework.md deleted file mode 100644 index aeb1ff6183..0000000000 --- a/docs/source/advanced/write_framework.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Writing Framework ---- - -# Writing Framework - -TBD diff --git a/docs/source/advanced/write_plugin.md b/docs/source/advanced/write_plugin.md deleted file mode 100644 index 3d5f45a514..0000000000 --- a/docs/source/advanced/write_plugin.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Writing Plugin ---- - -# Writing Plugin - -TBD diff --git a/docs/source/api/index.md b/docs/source/api/index.md deleted file mode 100644 index dfd1de1284..0000000000 --- a/docs/source/api/index.md +++ /dev/null @@ -1,3 +0,0 @@ -layout: api -title: API ---- diff --git a/docs/source/faq.md b/docs/source/faq.md deleted file mode 100644 index e9877d8c0e..0000000000 --- a/docs/source/faq.md +++ /dev/null @@ -1,6 +0,0 @@ -title: FAQ ---- - -# FAQ - -If you have questions that is not contained below, please check [issue](https://gitlab.alibaba-inc.com/egg/egg/issues) diff --git a/docs/source/frameworks.md b/docs/source/frameworks.md deleted file mode 100644 index fd74e751f5..0000000000 --- a/docs/source/frameworks.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Frameworks ---- - -# Popular Frameworks - -- [aliyun-egg](https://github.com/eggjs/aliyun-egg) diff --git a/docs/source/guide/deployment.md b/docs/source/guide/deployment.md deleted file mode 100644 index 0ca61a1f9f..0000000000 --- a/docs/source/guide/deployment.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Deployment ---- - -# Deployment - -TBD diff --git a/docs/source/guide/index.md b/docs/source/guide/index.md deleted file mode 100644 index 8527f8222b..0000000000 --- a/docs/source/guide/index.md +++ /dev/null @@ -1,10 +0,0 @@ -title: Overview ---- - -# Overview - -## Why egg - -## What's egg - -## Benchmark diff --git a/docs/source/guide/installation.md b/docs/source/guide/installation.md deleted file mode 100644 index d6261ea268..0000000000 --- a/docs/source/guide/installation.md +++ /dev/null @@ -1,38 +0,0 @@ -title: Installation ---- - -# Installation - -The best way to install [node] is [nvm] in OS X and Linux, or [nvmw] in Windows. - -**Don't use sudo.** - -After [install nvm](https://github.com/creationix/nvm#install-script), you can install node. - -``` -$ nvm install 4 -``` - -You can switch version that is installed. - -```bash -$ nvm use 4 -$ node -v -$ nvm use 6 -$ node -v -``` - -## Global module - -Global module is the module that is installed with `-g` flag. You can share global modules between multi version. - -If you are using nvm, it will switch `prefix` when switch versions. But you can specify `prefix` to use one global module between versions. - -1. Edit `~/.npmrc`,append `prefix=~/.npm-global` -2. Edit `~/.zshrc` or `~/.bashrc`,,append ` export PATH=~/.npm-global/bin:$PATH` -3. Run `source ~/.zshrc` or `source ~/.bashrc` - - -[nvm]: https://github.com/creationix/nvm -[nvmw]: https://github.com/hakobera/nvmw -[node]: https://nodejs.org/ diff --git a/docs/source/guide/plugins.md b/docs/source/guide/plugins.md deleted file mode 100644 index 37e8bea7b6..0000000000 --- a/docs/source/guide/plugins.md +++ /dev/null @@ -1,4 +0,0 @@ -title: Core Plugins ---- - -# Core Plugins diff --git a/docs/source/guide/quickstart.md b/docs/source/guide/quickstart.md deleted file mode 100644 index e3cfb92c92..0000000000 --- a/docs/source/guide/quickstart.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Quick Start ---- - -# Quick Start - -TBD diff --git a/docs/source/guide/unittest.md b/docs/source/guide/unittest.md deleted file mode 100644 index 332dbd2f27..0000000000 --- a/docs/source/guide/unittest.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Unit Test ---- - -# Unit Test - -TBD diff --git a/docs/source/guide/write_application.md b/docs/source/guide/write_application.md deleted file mode 100644 index 042e354254..0000000000 --- a/docs/source/guide/write_application.md +++ /dev/null @@ -1,6 +0,0 @@ -title: Writing Application ---- - -# Writing Application - -TBD diff --git a/docs/source/member_guide.md b/docs/source/member_guide.md deleted file mode 100644 index 269a8cf24c..0000000000 --- a/docs/source/member_guide.md +++ /dev/null @@ -1 +0,0 @@ -# Member Guide diff --git a/docs/source/plugins.puml b/docs/source/plugins.puml deleted file mode 100644 index a19b2ca8cd..0000000000 --- a/docs/source/plugins.puml +++ /dev/null @@ -1,7 +0,0 @@ - -@startuml -digraph world { - "onerror"; - "session"; -} -@enduml diff --git a/docs/source/plugins/index.md b/docs/source/plugins/index.md deleted file mode 100644 index 02b43c3b1a..0000000000 --- a/docs/source/plugins/index.md +++ /dev/null @@ -1,3 +0,0 @@ -layout: plugins -title: Plugins ---- diff --git a/docs/source/zh-cn/guide/index.md b/docs/source/zh-cn/guide/index.md deleted file mode 100644 index 2f2ba8fe6c..0000000000 --- a/docs/source/zh-cn/guide/index.md +++ /dev/null @@ -1 +0,0 @@ -# 中文 overview diff --git a/docs/source/zh-cn/index.md b/docs/source/zh-cn/index.md deleted file mode 100644 index b1a0f0cf00..0000000000 --- a/docs/source/zh-cn/index.md +++ /dev/null @@ -1,2 +0,0 @@ -layout: index ---- diff --git a/docs/themes/egg/languages/en.yml b/docs/themes/egg/languages/en.yml deleted file mode 100644 index d6e3ce0e86..0000000000 --- a/docs/themes/egg/languages/en.yml +++ /dev/null @@ -1,37 +0,0 @@ -menu: - guide: Guide - api: API - plugins: Plugins - release: Release - -index: - slogan: "Born to build better enterprise frameworks and apps" - feature11: "Process Management" - feature12: "Built-in production process manager and load balancer" - feature21: "Customization" - feature22: "Customize framework for your team based on egg and plugins" - feature31: "Powerful Plugin System" - feature32: "Do what you like in egg with the pluggable and extendable system" - getstart: "Get Started" - readmore: "Read More" - -guide_toc: - Guide: Guide - Overview: Overview - QuickStart: QuickStart - "Write Application": "Write Application" - "Core Plugins": "Core Plugins" - Deployment: Deployment - "Unit Test": "Unit Test" - Advanced: Advanced - "Write Framework": "Write Framework" - "Write Plugin": "Write Plugin" - Cluster: Cluster - Loader: Loader - Singleton: Singleton - "Best Practice": "Best Practice" - Community: Community - Contributing: Contributing - "Member Guide": "Member Guide" - Frameworks: Frameworks - FAQ: FAQ diff --git a/docs/themes/egg/languages/zh-cn.yml b/docs/themes/egg/languages/zh-cn.yml deleted file mode 100644 index a6494ed339..0000000000 --- a/docs/themes/egg/languages/zh-cn.yml +++ /dev/null @@ -1,37 +0,0 @@ -menu: - guide: 新手指南 - api: API - plugins: 插件 - release: 发布记录 - -index: - slogan: "为企业级框架和应用而生" - feature11: "进程管理" - feature12: "内置的进程管理和负载均衡" - feature21: "自定义" - feature22: "基于 egg 和插件系统可以为你的团队自定义框架" - feature31: "强大的插件系统" - feature32: "插件系统可插拔及可扩展随心所欲" - getstart: "开始使用" - readmore: "查看更多" - -guide_toc: - Guide: 新手指南 - Overview: 总览 - QuickStart: 快速入门 - "Write Application": 创建一个应用 - "Core Plugins": 核心插件 - Deployment: 部署应用 - "Unit Test": 单元测试 - Advanced: 高阶 - "Write Framework": 编写一个框架 - "Write Plugin": 编写一个插件 - Cluster: 进程管理 - Loader: 加载器 - Singleton: 多实例模式 - "Best Practice": 最佳实践 - Community: 社区 - Contributing: 如何共享 - "Member Guide": 成员指南 - Frameworks: 框架 - FAQ: 常见问题 diff --git a/docs/themes/egg/layout/index.swig b/docs/themes/egg/layout/index.swig deleted file mode 100644 index b2f43d1ae9..0000000000 --- a/docs/themes/egg/layout/index.swig +++ /dev/null @@ -1,58 +0,0 @@ -
    - - -
    - Latest: 0.2.0 - Node.js >= 4.0.0 -
    - -
    -
      -
    • -
      - -
      -

      {{ __('index.feature11') }}

      -

      {{ __('index.feature12') }}

      -
    • -
    • -
      - -
      -

      {{ __('index.feature21') }}

      -

      {{ __('index.feature22') }}

      -
    • -
    • -
      - -
      -

      {{ __('index.feature31') }}

      -

      {{ __('index.feature32') }}

      -
    • -
    -
    - -
    -

    {{ __('index.getstart') }}

    -
    -
      -
    • $npm install egg-init -g
    • -
    • $egg-init --type simple showcase && cd showcase
    • -
    • $npm install
    • -
    • $npm run dev
    • -
    • $open http://localhost:7001
    • -
    -
    -

    - {{ __('index.readmore') }} -

    -
    -
    - - diff --git a/docs/themes/egg/layout/layout.swig b/docs/themes/egg/layout/layout.swig deleted file mode 100644 index b84a3a5610..0000000000 --- a/docs/themes/egg/layout/layout.swig +++ /dev/null @@ -1,13 +0,0 @@ - - - - {{ partial('partial/head') }} - - -
    - {{ partial('partial/header') }} - {{ body }} - {{ partial('partial/footer') }} -
    - - diff --git a/docs/themes/egg/layout/page.swig b/docs/themes/egg/layout/page.swig deleted file mode 100644 index ba364039c1..0000000000 --- a/docs/themes/egg/layout/page.swig +++ /dev/null @@ -1,35 +0,0 @@ -
    -

    GUIDE

    -

    The best way to know egg

    -
    - -
    -
    - {{ page.content }} -
    - -
    - - diff --git a/docs/themes/egg/layout/partial/footer.swig b/docs/themes/egg/layout/partial/footer.swig deleted file mode 100644 index 36d9c41d44..0000000000 --- a/docs/themes/egg/layout/partial/footer.swig +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/docs/themes/egg/layout/partial/head.swig b/docs/themes/egg/layout/partial/head.swig deleted file mode 100644 index ca48f1474a..0000000000 --- a/docs/themes/egg/layout/partial/head.swig +++ /dev/null @@ -1,7 +0,0 @@ -{{ page.title }} - - - - - -{{ css('css/index') }} diff --git a/docs/themes/egg/layout/partial/header.swig b/docs/themes/egg/layout/partial/header.swig deleted file mode 100644 index e236138cc3..0000000000 --- a/docs/themes/egg/layout/partial/header.swig +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/docs/themes/egg/layout/plugins.swig b/docs/themes/egg/layout/plugins.swig deleted file mode 100644 index 9702b386ab..0000000000 --- a/docs/themes/egg/layout/plugins.swig +++ /dev/null @@ -1,8 +0,0 @@ -
    -

    PLUGINS

    -

    dhdh

    -
    - -
    -plugins -
    diff --git a/docs/themes/egg/layout/release.swig b/docs/themes/egg/layout/release.swig deleted file mode 100644 index db60bd472f..0000000000 --- a/docs/themes/egg/layout/release.swig +++ /dev/null @@ -1,8 +0,0 @@ -
    -

    RELEASE

    -

    Knowing about the past,Looking to the future

    -
    - -
    -
    {{ page.content }}
    -
    diff --git a/docs/themes/egg/source/css/index.less b/docs/themes/egg/source/css/index.less deleted file mode 100644 index 7b3a313396..0000000000 --- a/docs/themes/egg/source/css/index.less +++ /dev/null @@ -1,19 +0,0 @@ -@import "vendor/normalize"; -@import "vendor/github-markdown"; - -@import "partial/var"; -@import "partial/main"; -@import "partial/nav"; -@import "partial/toc"; -@import "partial/footer"; - -@import "page/index"; -@import "page/page"; -@import "partial/mobile"; - -.release { - padding: 40px; - h1 { - font-size: 1.5em; - } -} diff --git a/docs/themes/egg/source/css/page/index.less b/docs/themes/egg/source/css/page/index.less deleted file mode 100644 index efde2640df..0000000000 --- a/docs/themes/egg/source/css/page/index.less +++ /dev/null @@ -1,126 +0,0 @@ -.btn { - display: inline-block; - height: 38px; - width: 120px; - border: 1px solid #fff; - border-radius: 19px; - color: #fff; - line-height: 38px; - letter-spacing: 0.5px; - font-weight: normal; -} - -.btn-primary { - background: #006FE5; - border-color: #006FE5; - &:hover{ - box-shadow: 0px 4px 8px 0px #0E3D6F; - color: #fff; - transition: box-shadow 0.3s; - } -} - -.index { - text-align: center; -} - -.index-bg-dark { - background: @bg_dark; - color: #dbdde6; -} - -.index-bg-light { - background: @bg_light; -} - -.block { - padding: 100px 0; -} - -.banner { - h1{ - font-size: 32px; - font-weight: 200; - letter-spacing: 1px; - margin-bottom: 20px; - } - - .banner-logo { - height: 280px; - } - - .banner-button { - padding-top: 50px; - } -} - -.version { - background: #F6F8F8; - height: 60px; - line-height: 60px; - box-shadow: 0px 1px 0px 0px rgba(225,225,225,0.50); - span { - padding: 0 30px; - color: #71747B; - } - strong { - color: #131926; - font-weight: bold; - } -} - -.info { - h3 { - font-weight: 500; - color: #333333; - font-size: 24px; - margin-bottom: 20px; - } - ul { - padding-top: 60px; - } - li { - vertical-align: top; - display: inline-block; - width: 270px; - padding: 0 20px; - margin-bottom: 70px; - font-size: 16px; - } - .info-img { - height: 100px; - } -} - - -.guide-code { - font-size: 20px; - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-weight: 200; - color: #FFFFFF; - text-align: left; - padding: 40px 0; - - ul { - background: #1F2532; - max-width: 650px; - margin: 0 auto; - padding: 40px; - } - - li { - list-style: none; - line-height: 60px; - } - - strong { - color: #d4d4d4; - margin-right: 30px; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } -} diff --git a/docs/themes/egg/source/css/page/page.less b/docs/themes/egg/source/css/page/page.less deleted file mode 100644 index 9642118011..0000000000 --- a/docs/themes/egg/source/css/page/page.less +++ /dev/null @@ -1,37 +0,0 @@ -.page-title { - padding: 40px 0 160px; - background: @bg_dark; - text-align: center; - h1{ - text-shadow: 3px 4px 0px rgba(255,255,255,0.10); - letter-spacing: 2px; - font-size: 64px; - color: #fff; - font-weight: 500; - } - h2{ - font-size: 16px; - color: #4E5668; - padding-top: 5px; - font-weight: 300; - letter-spacing: 0.5px; - } -} - -.page-main { - margin: -100px auto 50px; - max-width: @max_width; - min-height: 200px; - overflow: hidden; - background: #FFFFFF; - box-shadow: 0px 1px 1px 0px #E3E3E3; - border-radius: 2px; -} - -.page-main article { - width: 760px; - float: right; - min-height: 400px; - padding: 20px; - border-left: 1px solid #EFEFEF; -} diff --git a/docs/themes/egg/source/css/partial/footer.less b/docs/themes/egg/source/css/partial/footer.less deleted file mode 100644 index e0f0e4201d..0000000000 --- a/docs/themes/egg/source/css/partial/footer.less +++ /dev/null @@ -1,33 +0,0 @@ -.footer { - background: @bg_light; - - footer { - overflow: hidden; - height: 60px; - line-height: 60px; - max-width: @max_width; - margin: 0 auto; - } - - ul { - float: left; - } - - li { - display: inline-block; - margin-right: 30px; - } - - &, & a { - color: #6E717C; - } - - .license { - float: right; - - img { - vertical-align: -5px; - margin-left: 4px; - } - } -} diff --git a/docs/themes/egg/source/css/partial/main.less b/docs/themes/egg/source/css/partial/main.less deleted file mode 100644 index 72cf065e01..0000000000 --- a/docs/themes/egg/source/css/partial/main.less +++ /dev/null @@ -1,34 +0,0 @@ -body { - min-width: 320px; - background: @bg_default; - font-size: 14px; - font-family: 'Helvetica Neue', 'Helvetica', tahoma, 'Hiragino Sans GB', 'PingFang SC', 'STHeitiSC-Light', 'Microsoft YaHei', Arial, sans-serif; - & a:hover { - color: #006BE8; - } -} - -h1, h2, h3, h4, h5, h6 { - margin: 0; -} - -ul, li, dl, dt, dd { - margin: 0; - padding: 0; -} - -a { - text-decoration: none; -} - -p { - margin: 0; -} - -/* big font */ -.ft-b { - font-size: 32px; - font-weight: 500; - letter-spacing: 1px; - line-height: 2.5em; -} diff --git a/docs/themes/egg/source/css/partial/mobile.less b/docs/themes/egg/source/css/partial/mobile.less deleted file mode 100644 index 102382b4cd..0000000000 --- a/docs/themes/egg/source/css/partial/mobile.less +++ /dev/null @@ -1,74 +0,0 @@ -.mobile-menu { - display: none; - ul { - padding-left: 20px; - margin-bottom: 40px; - li a { - color: #000000; - } - } -} - -@media screen and (max-width: 1004px) { - .nav, footer { - padding: 0 20px; - } - - .nav { - ul { - display: none; - } - header { - text-align: center; - } - } - - .mobile-menu { - display: block; - } - - .page-main aside { - display: block; - position: absolute; - background: #fff; - top: 0px; - left: -210px; - width: 200px; - // height: 100%; - border-right: 1px solid #eee; - box-shadow: 0px 0px 4px 0px #ddd; - transition: left 1s; - } - - .mobile-trigger { - display: inline-block; - top: 15px; - right: -50px; - transition: right 1s; - position: absolute; - li { - background: #fff; - width: 30px; - height: 6px; - margin-bottom: 4px; - } - } - - .page-main aside.mobile-show { - left: 0; - transition: left 1s; - .mobile-trigger { - right: 15px; - transition: right 1s; - li { - background: #000; - } - } - } - - .page-main article { - float: none; - width: auto; - } - -} diff --git a/docs/themes/egg/source/css/partial/nav.less b/docs/themes/egg/source/css/partial/nav.less deleted file mode 100644 index bc017ea591..0000000000 --- a/docs/themes/egg/source/css/partial/nav.less +++ /dev/null @@ -1,41 +0,0 @@ -.nav { - background: @bg_dark; - - header { - overflow: hidden; - padding: 30px 0 10px; - max-width: @max_width; - margin: 0 auto; - } - - ul { - height: 32px; - float: right; - line-height: 32px; - } - - li { - display: inline-block; - margin-left: 40px; - } - - a { - display: inline-block; - color: #6E717C; - letter-spacing:0.5px; - font-weight: 300; - &:hover{ - color:#FFF; - transition: 0.6s all; - } - } - - iframe { - vertical-align: -4px; - } -} - -.nav-logo { - margin: 0; - display: inline-block; -} diff --git a/docs/themes/egg/source/css/partial/toc.less b/docs/themes/egg/source/css/partial/toc.less deleted file mode 100644 index 550c967f6e..0000000000 --- a/docs/themes/egg/source/css/partial/toc.less +++ /dev/null @@ -1,36 +0,0 @@ -.toc { - position: relative; - left: 3px; - float: left; - width: 200px; - padding: 30px 0; - border-right: 1px solid #EFEFEF; - - dl { - padding: 0 20px; - } - - dt { - padding-bottom: 10px; - border-bottom: 1px solid #eee; - } - - dd { - margin-bottom: 30px; - } - - ul li a { - font-size: 14px; - color: #71747B; - height: 40px; - line-height: 40px; - &:hover{ - color: #006BE8; - transition: 0.3s all; - } - } - - li { - list-style-type: none; - } -} diff --git a/docs/themes/egg/source/css/partial/var.less b/docs/themes/egg/source/css/partial/var.less deleted file mode 100644 index 6cb134ac8b..0000000000 --- a/docs/themes/egg/source/css/partial/var.less +++ /dev/null @@ -1,5 +0,0 @@ -@bg_default: #F6F8F8; -@bg_dark: #121724; -@bg_light: #FFFFFF; - -@max_width: 1004px; diff --git a/docs/themes/egg/source/css/vendor/github-markdown.less b/docs/themes/egg/source/css/vendor/github-markdown.less deleted file mode 100644 index 00d48488db..0000000000 --- a/docs/themes/egg/source/css/vendor/github-markdown.less +++ /dev/null @@ -1,694 +0,0 @@ -@font-face { - font-family: octicons-link; - src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); -} - -.markdown-body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - line-height: 1.5; - color: #333; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .pl-c { - color: #969896; -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: #0086b3; -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: #795da3; -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: #333; -} - -.markdown-body .pl-ent { - color: #63a35c; -} - -.markdown-body .pl-k { - color: #a71d5d; -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: #183691; -} - -.markdown-body .pl-v { - color: #ed6a43; -} - -.markdown-body .pl-id { - color: #b52a1d; -} - -.markdown-body .pl-ii { - color: #f8f8f8; - background-color: #b52a1d; -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: #63a35c; -} - -.markdown-body .pl-ml { - color: #693a17; -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: #1d3e81; -} - -.markdown-body .pl-mq { - color: #008080; -} - -.markdown-body .pl-mi { - font-style: italic; - color: #333; -} - -.markdown-body .pl-mb { - font-weight: bold; - color: #333; -} - -.markdown-body .pl-md { - color: #bd2c00; - background-color: #ffecec; -} - -.markdown-body .pl-mi1 { - color: #55a532; - background-color: #eaffea; -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: #795da3; -} - -.markdown-body .pl-mo { - color: #1d3e81; -} - -.markdown-body .octicon { - display: inline-block; - vertical-align: text-top; - fill: currentColor; -} - -.markdown-body a { - background-color: transparent; - -webkit-text-decoration-skip: objects; -} - -.markdown-body a:active, -.markdown-body a:hover { - outline-width: 0; -} - -.markdown-body strong { - font-weight: inherit; -} - -.markdown-body strong { - font-weight: bolder; -} - -.markdown-body h1 { - font-size: 2em; - margin: 0.67em 0; -} - -.markdown-body img { - border-style: none; -} - -.markdown-body svg:not(:root) { - overflow: hidden; -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre { - font-family: monospace, monospace; - font-size: 1em; -} - -.markdown-body hr { - box-sizing: content-box; - height: 0; - overflow: visible; -} - -.markdown-body input { - font: inherit; - margin: 0; -} - -.markdown-body input { - overflow: visible; -} - -.markdown-body button:-moz-focusring, -.markdown-body [type="button"]:-moz-focusring, -.markdown-body [type="reset"]:-moz-focusring, -.markdown-body [type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -.markdown-body [type="checkbox"] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body * { - box-sizing: border-box; -} - -.markdown-body input { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body a { - color: #4078c0; - text-decoration: none; -} - -.markdown-body a:hover, -.markdown-body a:active { - text-decoration: underline; -} - -.markdown-body strong { - font-weight: 600; -} - -.markdown-body hr { - height: 0; - margin: 15px 0; - overflow: hidden; - background: transparent; - border: 0; - border-bottom: 1px solid #ddd; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body h1 { - font-size: 32px; - font-weight: 600; -} - -.markdown-body h2 { - font-size: 24px; - font-weight: 600; -} - -.markdown-body h3 { - font-size: 20px; - font-weight: 600; -} - -.markdown-body h4 { - font-size: 16px; - font-weight: 600; -} - -.markdown-body h5 { - font-size: 14px; - font-weight: 600; -} - -.markdown-body h6 { - font-size: 12px; - font-weight: 600; -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; -} - -.markdown-body ul, -.markdown-body ol { - padding-left: 0; - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body code { - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; -} - -.markdown-body .octicon { - vertical-align: text-bottom; -} - -.markdown-body input { - -webkit-font-feature-settings: "liga" 0; - font-feature-settings: "liga" 0; -} - -.markdown-body .form-select::-ms-expand { - opacity: 0; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body>*:first-child { - margin-top: 0 !important; -} - -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre { - margin-top: 0; - margin-bottom: 16px; -} - -.markdown-body hr { - height: 0.25em; - padding: 0; - margin: 24px 0; - background-color: #e7e7e7; - border: 0; -} - -.markdown-body blockquote { - padding: 0 1em; - color: #777; - border-left: 0.25em solid #ddd; -} - -.markdown-body blockquote>:first-child { - margin-top: 0; -} - -.markdown-body blockquote>:last-child { - margin-bottom: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #555; - vertical-align: middle; - background-color: #fcfcfc; - border: solid 1px #ccc; - border-bottom-color: #bbb; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #bbb; -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: #000; - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 { - padding-bottom: 0.3em; - font-size: 2em; - border-bottom: 1px solid #eee; -} - -.markdown-body h2 { - padding-bottom: 0.3em; - font-size: 1.5em; - border-bottom: 1px solid #eee; -} - -.markdown-body h3 { - font-size: 1.25em; -} - -.markdown-body h4 { - font-size: 1em; -} - -.markdown-body h5 { - font-size: 0.875em; -} - -.markdown-body h6 { - font-size: 0.85em; - color: #777; -} - -.markdown-body ul, -.markdown-body ol { - padding-left: 2em; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li>p { - margin-top: 16px; -} - -.markdown-body li+li { - margin-top: 0.25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: bold; -} - -.markdown-body dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -.markdown-body table { - display: block; - width: 100%; - overflow: auto; - word-break: normal; - word-break: keep-all; -} - -.markdown-body table th { - font-weight: bold; -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid #ddd; -} - -.markdown-body table tr { - background-color: #fff; - border-top: 1px solid #ccc; -} - -.markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; -} - -.markdown-body img { - max-width: 100%; - box-sizing: content-box; - background-color: #fff; -} - -.markdown-body code { - padding: 0; - padding-top: 0.2em; - padding-bottom: 0.2em; - margin: 0; - font-size: 85%; - background-color: rgba(0,0,0,0.04); - border-radius: 3px; -} - -.markdown-body code::before, -.markdown-body code::after { - letter-spacing: -0.2em; - content: "\00a0"; -} - -.markdown-body pre { - word-wrap: normal; -} - -.markdown-body pre>code { - padding: 0; - margin: 0; - font-size: 100%; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: 16px; -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f7f7f7; - border-radius: 3px; -} - -.markdown-body pre code { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body pre code::before, -.markdown-body pre code::after { - content: normal; -} - -.markdown-body .pl-0 { - padding-left: 0 !important; -} - -.markdown-body .pl-1 { - padding-left: 3px !important; -} - -.markdown-body .pl-2 { - padding-left: 6px !important; -} - -.markdown-body .pl-3 { - padding-left: 12px !important; -} - -.markdown-body .pl-4 { - padding-left: 24px !important; -} - -.markdown-body .pl-5 { - padding-left: 36px !important; -} - -.markdown-body .pl-6 { - padding-left: 48px !important; -} - -.markdown-body .full-commit .btn-outline:not(:disabled):hover { - color: #4078c0; - border: 1px solid #4078c0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; - line-height: 10px; - color: #555; - vertical-align: middle; - background-color: #fcfcfc; - border: solid 1px #ccc; - border-bottom-color: #bbb; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #bbb; -} - -.markdown-body :checked+.radio-label { - position: relative; - z-index: 1; - border-color: #4078c0; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item+.task-list-item { - margin-top: 3px; -} - -.markdown-body .task-list-item input { - margin: 0 0.2em 0.25em -1.6em; - vertical-align: middle; -} - -.markdown-body hr { - border-bottom-color: #eee; -} diff --git a/docs/themes/egg/source/css/vendor/normalize.less b/docs/themes/egg/source/css/vendor/normalize.less deleted file mode 100644 index 18ddf7fede..0000000000 --- a/docs/themes/egg/source/css/vendor/normalize.less +++ /dev/null @@ -1,419 +0,0 @@ -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Prevent adjustments of font size after orientation changes in IE and iOS. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - * 2. Add the correct display in IE. - */ - -article, -aside, -details, /* 1 */ -figcaption, -figure, -footer, -header, -main, /* 2 */ -menu, -nav, -section, -summary { /* 1 */ - display: block; -} - -/** - * Add the correct display in IE 9-. - */ - -audio, -canvas, -progress, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Add the correct display in IE 10-. - * 1. Add the correct display in IE. - */ - -template, /* 1 */ -[hidden] { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change font properties to `inherit` in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -select, -textarea { - font: inherit; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Restore the font weight unset by the previous rule. - */ - -optgroup { - font-weight: bold; -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Correct the text style of placeholders in Chrome, Edge, and Safari. - */ - -::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} diff --git a/docs/themes/egg/source/images/feature1.svg b/docs/themes/egg/source/images/feature1.svg deleted file mode 100644 index 11b346a445..0000000000 --- a/docs/themes/egg/source/images/feature1.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - Page 1 - Created with Sketch Beta. - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/themes/egg/source/images/feature2.svg b/docs/themes/egg/source/images/feature2.svg deleted file mode 100644 index cd0c9c1e86..0000000000 --- a/docs/themes/egg/source/images/feature2.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - Page 1 - Created with Sketch Beta. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/themes/egg/source/images/feature3.svg b/docs/themes/egg/source/images/feature3.svg deleted file mode 100644 index 7a376cf446..0000000000 --- a/docs/themes/egg/source/images/feature3.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - Page 1 - Created with Sketch Beta. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/themes/egg/source/images/github.svg b/docs/themes/egg/source/images/github.svg deleted file mode 100644 index 3239e4f9f2..0000000000 --- a/docs/themes/egg/source/images/github.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Group - Created with Sketch Beta. - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/themes/egg/source/images/logo-animate.js b/docs/themes/egg/source/images/logo-animate.js deleted file mode 100644 index 775b41346d..0000000000 --- a/docs/themes/egg/source/images/logo-animate.js +++ /dev/null @@ -1,4 +0,0 @@ -!function(t){function e(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return t[i].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}(function(t){for(var e in t)if(Object.prototype.hasOwnProperty.call(t,e))switch(typeof t[e]){case"function":break;case"object":t[e]=function(e){var n=e.slice(1),i=t[e[0]];return function(t,e,r){i.apply(this,[t,e,r].concat(n))}}(t[e]);break;default:t[e]=t[t[e]]}return t}([function(t,e,n){"use strict";function i(t){return t&&t.__esModule?t:{"default":t}}var r=n(45),a=i(r),s=n(77),o=i(s),c=n(33),u=i(c),h=n(70),l=i(h);n(72),window.logo=function(t){var e=new a["default"],n=new o["default"]({containerId:t,width:320,height:250}),i=n.addGroup(u["default"],{svgson:l["default"]}),r=i.find("points"),s=r.get("children"),c=i.find("lines"),h=c.get("children"),f=void 0,x=void 0,g=void 0;n.translate(20,20);for(var d=0;d0&&e.stroke()},isPointInPath:function(t,e){return!1},isHit:function(t,e){var n=this,i=n.get("canvas"),r=new o(t,e,1);n.invert(r,i);var a=n.getBBox();if(a&&s.box(a.minX,a.maxX,a.minY,a.maxY,r.x,r.y)){var c=n.__attrs.clip;if(!c)return n.isPointInPath(r.x,r.y);if(c.inside(t,e))return n.isPointInPath(r.x,r.y)}return!1},getBBox:function(){return this.get("box")}}),t.exports=c},function(t,e,n){var i=n(8),r={Canvas:n(52),Group:n(23),Shape:n(6),Rect:n(67),Circle:n(56),Ellipse:n(58),Path:n(63),Text:n(68),Line:n(61),Image:n(60),Polygon:n(64),Polyline:n(65),Arc:n(55),Fan:n(59),Cubic:n(57),Quadratic:n(66),debug:function(t){i.debug=t}};t.exports=r},function(t,e){"use strict";var n={prefix:"g",backupContext:function(){return document.createElement("canvas").getContext("2d")}(),debug:!1,warn:function(t){}};t.exports=n},function(t,e,n){function i(t,e,n,i){s(t,a(e,n,i))}function r(t,e,n){var i=n/Math.sin(u);return t.setLength(i/2),e.sub(t),e}function a(t,e,n){var i=new c(1,0).angleTo(t),r=i-u,a=i+u,s=6+3*n;return[{x:e.x-s*Math.cos(r),y:e.y-s*Math.sin(r)},e,{x:e.x-s*Math.cos(a),y:e.y-s*Math.sin(a)}]}function s(t,e){t.moveTo(e[0].x,e[0].y),t.lineTo(e[1].x,e[1].y),t.lineTo(e[2].x,e[2].y)}var o=n(1),c=(n(3),n(4),o.Vector2),u=Math.PI/6;t.exports={makeArrow:i,getEndPoint:r}},function(t,e,n){"use strict";var i=n(7),r=n(4),a=i.Shape.superclass.constructor,s=document.createElement("table"),o=document.createElement("tr"),c=/^\s*<(\w+|!)[^>]*>/,u={tr:document.createElement("tbody"),tbody:s,thead:s,tfoot:s,td:o,th:o,"*":document.createElement("div")};r.mix(r,{getBoundingClientRect:function(t){var e=t.getBoundingClientRect(),n=document.documentElement.clientTop,i=document.documentElement.clientLeft;return{top:e.top-n,bottom:e.bottom-n,left:e.left-i,right:e.right-i}},upperFirst:function(t){return t.replace(/(\w)/,function(t){return t.toUpperCase()})},getStyle:function(t,e){return window.getComputedStyle?window.getComputedStyle(t,null)[e]:t.currentStyle[e]},modiCSS:function(t,e){var n;for(n in e)t.style[n]=e[n];return t},getRatio:function(){return window.devicePixelRatio?window.devicePixelRatio:1},initClassCfgs:function(t){if(!t.__cfg&&t!==a){var e=t.superclass.constructor;e&&!e.__cfg&&r.initClassCfgs(e),t.__cfg={},r.mix(!0,t.__cfg,e.__cfg),r.mix(!0,t.__cfg,t.CFG)}},mixin:function(t,e){var n=t.CFG?"CFG":"ATTRS";if(t&&e){t._mixins=e,t[n]=t[n]||{};var i={};r.each(e,function(e){r.augment(t,e);var a=e[n];a&&r.mix(i,a)}),t[n]=r.mix(i,t[n])}},createDom:function(t){var e=c.test(t)&&RegExp.$1;e in u||(e="*");var n=u[e];return t=t.replace(/(^\s*)|(\s*$)/g,""),n.innerHTML=""+t,n.childNodes[0]}}),t.exports=r},10,function(t,e,n){"use strict";function i(t,e){if(a.isNumeric(t)&&a.isNumeric(e))return s.number(t,e);if(a.isString(t)&&a.isString(e)){var n=new c(t),i=new c(e);if(n.getType()&&i.getType())return o.color(n,i)}}function r(t,e){if(a.isNumeric(t)&&a.isNumeric(e))return s.unNumber(t,e);if(a.isString(t)&&a.isString(e)){var n=new c(t),i=new c(e);if(n.getType()&&i.getType())return o.unColor(n,i)}}var a=n(4),s=n(39),o=n(36),c=n(13);t.exports={singular:i,unSingular:r}},function(t,e,n){t.exports=n(41)},function(t,e,n){"use strict";var i=n(4),r="\t\n\x0B\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029",a=new RegExp("([a-z])["+r+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+r+"]*,?["+r+"]*)+)","ig"),s=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+r+"]*,?["+r+"]*","ig"),o=function(t){if(!t)return null;if(typeof t==typeof[])return t;var e={a:7,c:6,o:2,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,u:3,z:0},n=[];return String(t).replace(a,function(t,i,r){var a=[],o=i.toLowerCase();if(r.replace(s,function(t,e){e&&a.push(+e)}),"m"==o&&a.length>2&&(n.push([i].concat(a.splice(0,2))),o="l",i="m"==i?"l":"L"),"o"==o&&1==a.length&&n.push([i,a[0]]),"r"==o)n.push([i].concat(a));else for(;a.length>=e[o]&&(n.push([i].concat(a.splice(0,e[o]))),e[o]););}),n},c=function(t,e){for(var n=[],i=0,r=t.length;r-2*!e>i;i+=2){var a=[{x:+t[i-2],y:+t[i-1]},{x:+t[i],y:+t[i+1]},{x:+t[i+2],y:+t[i+3]},{x:+t[i+4],y:+t[i+5]}];e?i?r-4==i?a[3]={x:+t[0],y:+t[1]}:r-2==i&&(a[2]={x:+t[0],y:+t[1]},a[3]={x:+t[2],y:+t[3]}):a[0]={x:+t[r-2],y:+t[r-1]}:r-4==i?a[3]=a[2]:i||(a[0]={x:+t[i],y:+t[i+1]}),n.push(["C",(-a[0].x+6*a[1].x+a[2].x)/6,(-a[0].y+6*a[1].y+a[2].y)/6,(a[1].x+6*a[2].x-a[3].x)/6,(a[1].y+6*a[2].y-a[3].y)/6,a[2].x,a[2].y])}return n},u=function(t,e,n,i,r){if(null==r&&null==i&&(i=n),t=+t,e=+e,n=+n,i=+i,null!=r)var a=Math.PI/180,s=t+n*Math.cos(-i*a),o=t+n*Math.cos(-r*a),c=e+n*Math.sin(-i*a),u=e+n*Math.sin(-r*a),h=[["M",s,c],["A",n,n,0,+(r-i>180),0,o,u]];else h=[["M",t,e],["m",0,-i],["a",n,i,0,1,1,0,2*i],["a",n,i,0,1,1,0,-2*i],["z"]];return h},h=function(t){if(t=o(t),!t||!t.length)return[["M",0,0]];var e,n=[],i=0,r=0,a=0,s=0,h=0;"M"==t[0][0]&&(i=+t[0][1],r=+t[0][2],a=i,s=r,h++,n[0]=["M",i,r]);for(var l,f,x=3==t.length&&"M"==t[0][0]&&"R"==t[1][0].toUpperCase()&&"Z"==t[2][0].toUpperCase(),g=h,d=t.length;g1&&(_=Math.sqrt(_),n=_*n,i=_*i);var v=n*n,y=i*i,M=(a==s?-1:1)*Math.sqrt(Math.abs((v*y-v*m*m-y*p*p)/(v*m*m+y*p*p))),S=M*n*m/i+(t+o)/2,b=M*-i*p/n+(e+c)/2,w=Math.asin(((e-b)/i).toFixed(9)),A=Math.asin(((c-b)/i).toFixed(9));w=tA&&(w-=2*Math.PI),!s&&A>w&&(A-=2*Math.PI)}var P=A-w;if(Math.abs(P)>l){var C=A,I=o,T=c;A=w+l*(s&&A>w?1:-1),o=S+n*Math.cos(A),c=b+i*Math.sin(A),g=x(o,c,n,i,r,0,s,I,T,[A,C,S,b])}P=A-w;var B=Math.cos(w),k=Math.sin(w),F=Math.cos(A),L=Math.sin(A),R=Math.tan(P/4),O=4/3*n*R,X=4/3*i*R,Y=[t,e],z=[t+O*k,e-X*B],q=[o+O*L,c-X*F],D=[o,c];if(z[0]=2*Y[0]-z[0],z[1]=2*Y[1]-z[1],u)return[z,q,D].concat(g);g=[z,q,D].concat(g).join().split(",");for(var N=[],W=0,G=g.length;W7){t[e].shift();for(var r=t[e];r.length;)u[e]="A",i&&(g[e]="A"),t.splice(e++,0,["C"].concat(r.splice(0,6)));t.splice(e,1),_=Math.max(n.length,i&&i.length||0)}},c=function(t,e,r,a,s){t&&e&&"M"==t[s][0]&&"M"!=e[s][0]&&(e.splice(s,0,["M",a.x,a.y]),r.bx=0,r.by=0,r.x=t[s][1],r.y=t[s][2],_=Math.max(n.length,i&&i.length||0))},u=[],g=[],d="",p="",m=0,_=Math.max(n.length,i&&i.length||0);m<_;m++){n[m]&&(d=n[m][0]),"C"!=d&&(u[m]=d,m&&(p=u[m-1])),n[m]=s(n[m],r,p),"A"!=u[m]&&"C"==d&&(u[m]="C"),o(n,m),i&&(i[m]&&(d=i[m][0]),"C"!=d&&(g[m]=d,m&&(p=g[m-1])),i[m]=s(i[m],a,p),"A"!=g[m]&&"C"==d&&(g[m]="C"),o(i,m)),c(n,i,r,a,m),c(i,n,a,r,m);var v=n[m],y=i&&i[m],M=v.length,S=i&&y.length;r.x=v[M-2],r.y=v[M-1],r.bx=parseFloat(v[M-4])||r.x,r.by=parseFloat(v[M-3])||r.y,a.bx=i&&(parseFloat(y[S-4])||a.x),a.by=i&&(parseFloat(y[S-3])||a.y),a.x=i&&y[S-2],a.y=i&&y[S-1]}return i?[n,i]:n},d=function(t,e,n,i){return null==t&&(t=e=n=i=0),null==e&&(e=t.y,n=t.width,i=t.height,t=t.x),{x:t,y:e,w:n,h:i,cx:t+n/2,cy:e+i/2}},p=function(t,e,n,i,r,a,s,o){for(var c,u,h,l,f,x,g,d,p=[],m=[[],[]],_=0;_<2;++_)if(0==_?(u=6*t-12*n+6*r,c=-3*t+9*n-9*r+3*s,h=3*n-3*t):(u=6*e-12*i+6*a,c=-3*e+9*i-9*a+3*o,h=3*i-3*e),Math.abs(c)<1e-12){if(Math.abs(u)<1e-12)continue;l=-h/u,0n)var r=2*Math.PI-t+e,a=t-n}else var r=t-e,a=n-t;return r>a?n:e}function a(t,e,n,i){var a=0;return n-e>=2*Math.PI&&(a=2*Math.PI),e=h.mod(e,2*Math.PI),n=h.mod(n,2*Math.PI)+a,t=h.mod(t,2*Math.PI),i?e>=n?t>n&&tn?t:r(t,e,n):e<=n?ee||tt.x&&(d=t.x),pt.y&&(m=t.y),_=0&&y=0&&c<=1&&o.push(c)}}else{var h=a*a-4*r*s;if(u.equal(h,0))o.push(-a/(2*r));else if(h>0){var l=Math.sqrt(h),c=(-a+l)/(2*r),f=(-a-l)/(2*r);c>=0&&c<=1&&o.push(c),f>=0&&f<=1&&o.push(f)}}return o}var o=n(1),c=o.Vector2,u=n(3);t.exports={at:i,derivativeAt:r,projectPoint:function(t,e,n,i,r,s,o,c,u,h){var l={};return a(t,e,n,i,r,s,o,c,u,h,l),l},pointDistance:a,extrema:s}},function(t,e,n){"use strict";function i(t,e,n,i){var r=1-i;return r*(r*t+2*i*e)+i*i*n}function r(t,e,n,r,a,s,o,u,h){for(var l,f=.005,x=1/0,g=1e-4,d=new c(o,u),p=0;p<1;p+=.05){var m=new c(i(t,n,a,p),i(e,r,s,p)),_=m.distanceToSquared(d);_=0&&_=0?[r]:[]}var s=n(1),o=n(3),c=s.Vector2;t.exports={at:i,projectPoint:function(t,e,n,i,a,s,o,c){var u={};return r(t,e,n,i,a,s,o,c,u),u},pointDistance:r,extrema:a}},function(t,e,n){var i=n(71);t.exports=i},function(t,e){var n={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1==(t/=1)?1:(n||(n=.3),i=1?h.attrs[u]=n.attrs[u]:e.pathStash&&n.pathStash?h.attrs[u]=s.pathInterpolation(t,e.pathStash,n.pathStash):(c=i.path2curve(e.attrs[u],n.attrs[u]),e.pathStash=c[0],n.pathStash=c[1]):(o=s.interpolation(e.attrs[u],n.attrs[u]),h.attrs[u]=o(t)));return n.matrix&&e.matrix&&(a=e.matrix.clone(),"matrix3"===n.matrix.type?(o=s.interpolation(e.matrix,n.matrix),h.matrix=o(t)):i.isFunction(n.matrix)&&(h.matrix=n.matrix.call(r,a,t))),h},pathInterpolation:function(t,e,n){for(var r,a,o,c,u=[],h=0;h=r&&(i.attrs||i.matrix||this._autoSetStartKeyFrame(),void this.step(t))},step:function(t){var e,n,i=this.get("target"),a=this.get("startTime"),o=this.get("delay"),c=t-a-o,u=this.get("duration"),h=this.get("startKeyFrame"),l=this.get("endKeyFrame"),f=this.get("easing");f=s[f]?f:"linear",n=c/u,n=n<=0?0:n>=1?1:n,n=s[f](n),e=r.getFrame(n,h,l,i),l.attrs&&i.attr(e.attrs),l.matrix&&i.setMatrix(e.matrix),this.set("ratio",n),this.set("currentFrame",e),this.updateStatus()},updateStatus:function(){var t=this.get("ratio"),e=this.get("callBack"),n=this.get("destroyTarget"),i=this.get("target"),r=this.get("repeat");if(t>=1)if(r){var a=this.get("startTime"),s=this.get("endTime"),o=this.get("duration");this.set("startTime",a+o),this.set("endTime",s+o),this.reset()}else this.set("needsDestroy",!0),n&&i.remove(!0),e&&e.call(i)},reset:function(){this.set("ratio",0),this.set("needsDestroy",!1)},play:function(){var t=this,e=(t.get("target"),t.get("canvas")),n=t.get("available"),i=t.get("ratio"),r=t.get("callBack"),s=+new Date;return n?(t.step(s),e&&e.get("destroyed")!==!0&&e.draw(),void a.requestAnimationFrame(function(){return i>=1?(r&&r(),void t.destroy()):void(i>=0&&t.play())})):void t.destroy()},animate:function(t,e,n,i,a){var s=r.getKeyFrameByProps(t,e),o=this.get("canvas");o=o?o:t.get("canvas"),this.set("target",t),this.set("startTime",+new Date),this.set("duration",n),this.set("startKeyFrame",s[0]),this.set("endKeyFrame",s[1]),this.set("easing",i),this.set("callBack",a),this.set("canvas",o),this.play()}}),t.exports=o},function(t,e,n){"use strict";function i(t){if(!t.__attrs&&t!==u){var e=t.superclass.constructor;e&&!e.__attrs&&i(e),t.__attrs={},r.mix(!0,t.__attrs,e.__attrs),r.mix(!0,t.__attrs,t.ATTRS)}}var r=n(2),a=n(8),s=n(48),o=n(54),c=n(53),u=function(t){this.__cfg={};var e=this.getDefaultCfg();r.mix(this.__cfg,u.CFG,e,t),this.__cfg.uuid=r.guid(a.prefix),i(this.constructor),this.initAttrs(this.__cfg.attrs),this.initTransform(),this.initEventDispatcher(),this.init()};u.CFG={id:null,zIndex:0,canvas:null,parent:null,capture:!0,context:null,visible:!0,destroyed:!1},r.augment(u,s,o,c,{init:function(){},getDefaultCfg:function(){return{}},set:function(t,e){var n=this,i="__set"+r.ucfirst(t);return n[i]&&(e=n[i](e)),n.__cfg[t]=e,this},get:function(t){return this.__cfg[t]},beforeDraw:function(){},draw:function(){var t=this,e=t.get("context"),n=t.__attrs.clip;t.beforeDraw(),t.get("visible")&&(e.save(),n&&(e.save(),n.resetTransform(),n.createPath(),e.restore(),e.clip()),t.resetAttrs(),t.resetTransform(),t.drawInner(),e.restore())},drawInner:function(){},show:function(){return this.set("visible",!0),this},hide:function(){return this.set("visible",!1),this},remove:function(t){var e=this;if(void 0===t&&(t=!0),e.get("parent")){var n=e.get("parent"),i=n.get("children");r.remove(i,e),e.set("parent",null)}return t&&e.destroy(),e},destroy:function(){var t=this,e=t.get("destroyed");if(!e)return t.__cfg={},t.__attrs=null,t.__listeners=null,t.__m=null,t.set("destroyed",!0),t},__setZIndex:function(t){var e=this;return this.__cfg.zIndex=t,r.notNull(e.get("parent"))&&e.get("parent").sort(),t},__setAttrs:function(t){var e=this;return e.attr(t),t},clone:function(){return r.clone(this)},getBBox:function(){return{minX:0,maxX:0,minY:0,maxY:0}}}),t.exports=u},function(t,e,n){"use strict";function i(t){i.superclass.constructor.call(this,t),this.set("children",[])}var r=n(2),a=n(22),s=(n(5),n(1)),o=s.Vector3;r.extend(i,a),r.augment(i,{isGroup:!0,canFill:!0,canStroke:!0,remove:function(t,e){var n=this;if(arguments.length>=2)n.contain(t)&&t.remove(e);else{if(1===arguments.length){if(!r.isBoolean(t))return n.contain(t)&&t.remove(!0),n;e=t}0===arguments.length&&(e=!0),i.superclass.remove.call(n,e)}return n},add:function(t){var e=this,n=e.get("children");if(r.isArray(t))r.each(t,function(t){t.get("parent")&&t.get("parent").remove(t,!1),e.__setEvn(t)}),n.push.apply(n,t);else{var i=t;i.get("parent")&&i.get("parent").remove(i,!1),e.__setEvn(i),n.push(i)}return e},contain:function(t){for(var e=this,n=e.get("children"),i=0,r=n.length;in&&(n=f),xa&&(a=g)}}),{minX:e,minY:i,maxX:n,maxY:a}},drawInner:function(){var t=this,e=t.get("children");return r.each(e,function(t){t.draw()}),t},getCount:function(){var t=this;return t.get("children").length},sort:function(){var t=this,e=t.get("children");return e.sort(function(t,e){return t.get("zIndex")-e.get("zIndex")}),t},find:function(t){var e=this;return e.findBy(function(e){return e.get("id")===t})},findBy:function(t){var e=this,n=e.get("children"),i=null;return r.each(n,function(e){if(t(e)?i=e:e.findBy&&(i=e.findBy(t)),i)return!1}),i},getShape:function(t,e){function n(){for(var n=s.length-1;n>=0;n--){var r=s[n];if(r.get("visible")&&r.get("capture")&&(r.isGroup?i=r.getShape(t,e):r.isHit(t,e)&&(i=r)),i)break}}var i,r=this,a=r.__attrs.clip,s=r.get("children");return a?a.inside(t,e)&&n():n(),i},clear:function(){for(var t=this,e=t.get("children");0!==e.length;)e[e.length-1].remove();return t},destroy:function(){var t=this;t.get("destroyed")||(t.clear(),i.superclass.destroy.call(t))}}),t.exports=i},function(t,e,n){"use strict";function i(t,e,n){var i=x.exec(t),r=u.mod(u.degreeToRad(parseFloat(i[1])),2*Math.PI),a=i[2],o=e.getBBox();if(0<=r&&r<.5*Math.PI)var c={x:o.minX,y:o.minY},h={x:o.maxX,y:o.maxY};else if(.5*Math.PI<=r&&r1){var i=e[0].charAt(0);e.splice(1,0,e[0].substr(1)),e[0]=i}c.each(e,function(t,n){isNaN(t)||(e[n]=+t)}),t[n]=e}),t):void 0},parseStyle:function(t,e,n){if(c.isString(t))return x.test(t)?i(t,e,n):g.test(t)?r(t,e,n):d.test(t)?a(t,e):o(t,n)}};t.exports=m},function(t,e,n){"use strict";var i=n(1),r=i.Vector2;t.exports={at:function(t,e,n){return(e-t)*n+t},pointDistance:function(t,e,n,i,a,s){var o=new r(n-t,i-e);if(o.isZero())return NaN;var c=o.vertical();c.normalize();var u=new r(a-t,s-e);return Math.abs(u.dot(c))},box:function(t,e,n,i,r){var a=r/2,s=Math.min(t,n),o=Math.max(t,n),c=Math.min(e,i),u=Math.max(e,i);return{minX:s-a,minY:c-a,maxX:o+a,maxY:u+a}}}},[88,10],[90,75,26,76],[88,11],[90,79,28,80],function(t,e,n){"use strict";function i(){"undefined"!=typeof Float32Array?this.elements=new Float32Array([1,0,0,0,1,0,0,0,1]):this.elements=[1,0,0,0,1,0,0,0,1]}var r=n(4),a=n(3);i.multiply=function(t,e){var n=t.elements,r=e.elements,a=new i;return a.set(n[0]*r[0]+n[3]*r[1]+n[6]*r[2],n[0]*r[3]+n[3]*r[4]+n[6]*r[5],n[0]*r[6]+n[3]*r[7]+n[6]*r[8],n[1]*r[0]+n[4]*r[1]+n[7]*r[2],n[1]*r[3]+n[4]*r[4]+n[7]*r[5],n[1]*r[6]+n[4]*r[7]+n[7]*r[8],n[2]*r[0]+n[5]*r[1]+n[8]*r[2],n[2]*r[3]+n[5]*r[4]+n[8]*r[5],n[2]*r[6]+n[5]*r[7]+n[8]*r[8])},i.equal=function(t,e){for(var n=t.elements,i=e.elements,r=!0,s=0,o=n.length;st.x&&(this.x=t.x),this.y>t.y&&(this.y=t.y),this},max:function(t){return this.xe.x&&(this.x=e.x),this.ye.y&&(this.y=e.y),this},clampScale:function(){var t,e;return function(n,r){return void 0===t&&(t=new i,e=new i),t.set(n,n),e.set(r,r),this.clamp(t,e)}}(),floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},roundToZero:function(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this},negate:function(){return this.x=-this.x,this.y=-this.y,this},dot:function(t){return this.x*t.x+this.y*t.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.lengthSq())},normalize:function(){return this.divideScaler(this.length())},distanceToSquared:function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},distanceTo:function(t){return Math.sqrt(this.distanceToSquared(t))},angleTo:function(t,e){var n=this.angle(t),r=i.direction(this,t)>=0;return e?r?2*Math.PI-n:n:r?n:2*Math.PI-n},vertical:function(t){return t?new i(this.y,(-this.x)):new i((-this.y),this.x)},angle:function(t){return i.angle(this,t)},setLength:function(t){var e=this.length();return 0!==e&&t!==e&&this.multiplyScaler(t/e),this},isZero:function(){return 0===this.x&&0===this.y},lerp:function(t,e){return this.copy(i.lerp(this,t,e))},equal:function(t){return a.equal(this.x,t.x)&&a.equal(this.y,t.y)},clone:function(){return new i(this.x,this.y)}}),t.exports=i},function(t,e,n){"use strict";function i(t,e,n){if(1===arguments.length)if(r.isArray(t)){var i=t;t=i[0],e=i[1],n=i[2]}else if("vector2"===t.type){var a=t;t=a.x,e=a.y,n=1}this.x=t||0,this.y=e||0,this.z=n||0}var r=n(4),a=n(3);i.add=function(t,e){return new i(t.x+e.x,t.y+e.y,t.z+e.z)},i.sub=function(t,e){return new i(t.x-e.x,t.y-e.y,t.z-e.z)},i.lerp=function(t,e,n){return new i(t.x+(e.x-t.x)*n,t.y+(e.y-t.y)*n,t.z+(e.z-t.z)*n)},i.cross=function(t,e){var n=t.x,r=t.y,a=t.z,s=e.x,o=e.y,c=e.z;return new i(r*c-a*o,a*s-n*c,n*o-r*s)},i.angle=function(t,e){var n=t.dot(e)/(t.length()*e.length());return Math.acos(a.clamp(n,-1,1))},r.augment(i,{type:"vector3",set:function(t,e,n){return this.x=t,this.y=e,this.z=n,this},setComponent:function(t,e){switch(t){case 0:return this.x=e,this;case 1:return this.y=e,this;case 2:return this.z=e,this;default:throw new Error("index is out of range:"+t)}},getComponent:function(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range:"+t)}},add:function(t){return this.copy(i.add(this,t))},sub:function(t){return this.copy(i.sub(this,t))},subBy:function(t){return this.copy(i.sub(t,this))},multiplyScaler:function(t){return this.x*=t,this.y*=t,this.z*=t,this},divideScaler:function(t){if(0!==t){var e=1/t;this.x*=e,this.y*=e,this.z*=e}else this.x=0,this.y=0,this.z=0;return this},min:function(t){return this.x>t.x&&(this.x=t.x),this.y>t.y&&(this.y=t.y),this.z>t.z&&(this.z=t.z),this},max:function(t){return this.xe.x&&(this.x=e.x),this.ye.y&&(this.y=e.y),this.ze.z&&(this.z=e.z),this},clampScale:function(){var t,e;return function(n,r){return void 0===t&&(t=new i,e=new i),t.set(n,n,n),e.set(r,r,r),this.clamp(t,e)}}(),floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this},ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this},round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this},roundToZero:function(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this.z=this.z<0?Math.ceil(this.z):Math.floor(this.z),this},negate:function(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this},dot:function(t){return this.x*t.x+this.y*t.y+this.z*t.z},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},length:function(){return Math.sqrt(this.lengthSq())},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScaler(this.length())},setLength:function(t){var e=this.length();return 0!==e&&t!==e&&this.multiplyScaler(t/e),this},lerp:function(t,e){return this.copy(i.lerp(this,t,e))},cross:function(t){return this.copy(i.cross(this,t))},angle:function(t){return i.angle(this,t)},distanceToSquared:function(t){var e=this.x-t.x,n=this.y-t.y,i=this.z-t.z;return e*e+n*n+i*i},distanceTo:function(t){return Math.sqrt(this.distanceToSquared(t))},applyMatrix:function(t){var e=t.elements,n=e[0]*this.x+e[3]*this.y+e[6]*this.z,i=e[1]*this.x+e[4]*this.y+e[7]*this.z,r=e[2]*this.x+e[5]*this.y+e[8]*this.z;return this.x=n,this.y=i,this.z=r,this},copy:function(t){return this.x=t.x,this.y=t.y,this.z=void 0!==t.z?t.z:1,this},equal:function(t){return a.equal(this.x,t.x)&&a.equal(this.y,t.y)&&a.equal(this.z,t.z)},clone:function(){return new i(this.x,this.y,this.z)}}),t.exports=i},function(t,e,n){"use strict";function i(t,e){e=e.replace(" ","");var n=/([a-z]+)\((\S+),(\S+)\)/gi,i=n.exec(e);t[i[1]](i[2],i[3])}function r(t){for(var e=t.split(" "),n=[],i=0;i>16&255)/255,this.space.g=(t>>8&255)/255,this.space.b=(255&t)/255,this},setStyle:function(t){var e;if(e=c.hex.exec(t)){var n=e[1],i=n.length;if(3===i)return this.setRGB(parseInt(n.charAt(0)+n.charAt(0),16)/255,parseInt(n.charAt(1)+n.charAt(1),16)/255,parseInt(n.charAt(2)+n.charAt(2),16)/255),this;if(6===i)return this.setRGB(parseInt(n.charAt(0)+n.charAt(1),16)/255,parseInt(n.charAt(2)+n.charAt(3),16)/255,parseInt(n.charAt(4)+n.charAt(5),16)/255),this}else if(e=c.space.exec(t)){var r,a=e[1],s=e[2];switch(a){case"rgb":if(r=c.rgbNum.exec(s))return this.setRGB(parseInt(r[1],10)/255,parseInt(r[2],10)/255,parseInt(r[3],10)/255),this;if(r=c.rgbPre.exec(s))return this.setRGB(parseInt(r[1],10)/100,parseInt(r[2],10)/100,parseInt(r[3],10)/100),this;break;case"rgba":if(r=c.rgbaNum.exec(s))return this.setRGB(parseInt(r[1],10)/255,parseInt(r[2],10)/255,parseInt(r[3],10)/255,parseFloat(r[4])),this;if(r=c.rgbaPre.exec(s))return this.setRGB(parseInt(r[1],10)/100,parseInt(r[2],10)/100,parseInt(r[3],10)/100,parseFloat(r[4])),this;break;case"hsl":if(r=c.hsl.exec(s))return this.setHSL(parseInt(r[1],10)/360,parseInt(r[2],10)/100,parseInt(r[3],10)/100),this;break;case"hsla":if(r=c.hsla.exec(s))return this.setHSL(parseInt(r[1],10)/360,parseInt(r[2],10)/100,parseInt(r[3],10)/100,parseFloat(r[4])),this}}else t=t.toLowerCase(),void 0!==o[t]?this.setHex(o[t]):this.setHex(o.black)},copy:function(t){this.space=t.space.clone()},clone:function(){return new i(this)}}),t.exports=i},function(t,e){t.exports={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074}},function(t,e,n){"use strict";var i=n(4),r=n(3),a=function(){this.h=0,this.s=0,this.l=0};i.augment(a,{type:"hsl",setHSL:function(t,e,n,i){this.h=r.mod(t,1),this.s=r.clamp(e,0,1),this.l=r.clamp(n,0,1),void 0!==i?this.a=r.clamp(i,0,1):this.a=void 0},toRGB:function(){function t(t,e,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?t+6*(e-t)*n:n<.5?e:n<2/3?t+6*(e-t)*(2/3-n):t}return function(){var e=this,n=e.h,i=e.s,r=e.l;if(0===i)return{r:r,g:r,b:r,a:e.a};var a=r<=.5?r*(1+i):r+i-r*i,s=2*r-a;return{r:t(s,a,n+1/3),g:t(s,a,n),b:t(s,a,n-1/3),a:e.a}}}(),clone:function(){var t=new a;return t.h=this.h,t.s=this.s,t.l=this.l,t.a=this.a,t},copy:function(t){return this.h=t.h,this.s=t.s,this.l=t.l,this.a=t.a,this},getStyle:function(){var t=this;return void 0===t.a?"hsl("+Math.round(360*t.h)+", "+Math.round(100*t.s)+"%, "+Math.round(100*t.l)+"%)":"hsla("+Math.round(360*t.h)+", "+Math.round(100*t.s)+"%, "+Math.round(100*t.l)+"%, "+t.a+")"}}),t.exports=a},function(t,e,n){"use strict";var i=n(4),r=n(3),a=function(){this.r=0,this.g=0,this.b=0,this.type="rgb"};i.augment(a,{type:"rgb",setRGB:function(t,e,n,i){this.r=r.clamp(t,0,1),this.g=r.clamp(e,0,1),this.b=r.clamp(n,0,1),void 0!==i?this.a=r.clamp(i,0,1):this.a=void 0},toHSL:function(){var t,e,n=this.r,i=this.g,r=this.b,a=Math.max(n,i,r),s=Math.min(n,i,r),o=(s+a)/2;if(s===a)t=0,e=0;else{var c=a-s;switch(e=o<=.5?c/(a+s):c/(2-a-s),a){case n:t=(i-r)/c+(ie?n:e)},_setCanvases:function(t){var e=t.get("canvas"),n=this.get("canvases");n.indexOf(e)===-1&&n.push(e)},_resetTweens:function(){var t=this.get("tweens");i.each(t,function(t){t.reset()})},_getTime:function(){var t=this.get("playTime"),e=this.get("pauseTimeSpace");return+new Date-t+e},_refresh:function(t){for(var e,n,r=this.get("tweens"),a=this.get("canvases"),s=this.get("autoDraw"),o=this.get("autoDestroy"),c=this.get("removeCanvas"),u=[],h=[],l=0;l=n&&r&&(t.set("pauseTimeSpace",0),t._resetTweens(),t.play())})}},animate:function(t,e){var n=new a({target:t,timeline:this,startTime:e?e:0});return n},add:function(t){var e,n=this.get("tweens");return i.isArray(t)?e=n.concat(t):i.isObject(t)&&"tween"===t.get("type")?(n.push(t),e=n):console.error("Timeline not Support this type"),this.set("tweens",e),this._trySetCanvases(t),this._trySetEndTime(t),this},getNow:function(){var t=this.get("playTime");return t?+new Date-t:0},getTime:function(){var t=this.get("playTime");return t?+new Date-t:0},play:function(){var t=this.get("available");return this.set("playTime",+new Date),t||(this.set("available",!0),this._update()),this},loop:function(t){return t||void 0===t?(this.set("infinite",!0),this.set("autoDestroy",!1),this.set("removeCanvas",!1),this.set("loop",!0)):this.set("loop",!1),this},stop:function(){this.set("available",!1),this.set("pauseTimeSpace",0),this._resetTweens(),this.reset(),this._refresh(0),this.draw()},pause:function(){var t=this.get("available");return t&&this.set("pauseTimeSpace",+new Date-this.get("playTime")),this.set("available",!1),this},reset:function(){var t=this.get("autoDestroy");this.set("time",0),t&&(this.set("tweens",[]),this.set("canvases",[]))},draw:function(){for(var t=this.get("canvases"),e=0;e=0?t:e||1},__setAttrClip:function(t){var e=this;if(t&&t.type in o)return null===t.get("canvas")&&(t=i.clone(t)),t.set("parent",e.get("parent")),t.set("context",e.get("context")),t.inside=function(n,i){var r=new u(n,i,1);return t.invert(r,e.get("canvas")),t.__isPointInFill(r.x,r.y)},t}}),t.exports=h},function(t,e){"use strict";t.exports={circle:1,ellipse:1,fan:1,polygon:1,rect:1,path:1}},function(t,e){"use strict";t.exports={fillStyle:1,strokeStyle:1,globalAlpha:1,shadowBlur:1,shadowColor:1,shadowOffsetX:1,shadowOffsetY:1,lineDash:1}},function(t,e){"use strict";t.exports={fillStyle:1,font:1,globalAlpha:1,lineCap:1,lineWidth:1,lineJoin:1,miterLimit:1,shadowBlur:1,shadowColor:1,shadowOffsetX:1,shadowOffsetY:1,strokeStyle:1,textAlign:1,textBaseline:1,lineDash:1}},function(t,e,n){var i=n(2),r=n(23),a=function(t){a.superclass.constructor.call(this,t)};i.extend(a,r),i.augment(a,{init:function(){a.superclass.init.call(this);var t=this,e=t.get("canvasId"),n=document.getElementById(e);t.set("el",n),t.set("context",n.getContext("2d")),t.set("canvas",t),t.__events()},__events:function(){},getPointByClient:function(t,e){var n=this,i=n.get("el"),r=i.getBoundingClientRect(),a=r.right-r.left,s=r.bottom-r.top;return{x:(t-r.left)*(i.width/a),y:(e-r.top)*(i.height/s)}},getClientByPoint:function(t,e){var n=this,i=n.get("el"),r=i.getBoundingClientRect(),a=r.right-r.left,s=r.bottom-r.top;return{clientX:t/(i.width/a)+r.left,clientY:e/(i.height/s)+r.top}},beforeDraw:function(){var t=this,e=t.get("context"),n=t.get("el");e.clearRect(0,0,n.width,n.height)},draw:function(){function t(){e.set("animateHandler",i.requestAnimationFrame(function(){e.set("animateHandler",void 0),e.get("toDraw")&&t()})),a.superclass.draw.call(e),e.set("toDraw",!1)}var e=this;e.get("animateHandler")?e.set("toDraw",!0):t()}}),t.exports=a},function(t,e,n){"use strict";var i=n(2),r=function(){};i.augment(r,{initEventDispatcher:function(){this.__listeners={}},on:function(t,e){var n=this.__listeners;return i.isNull(n[t])&&(n[t]=[]),n[t].indexOf(e)===-1&&n[t].push(e),this},off:function(t,e){var n=this.__listeners;return 0===arguments.length?(this.__listeners={},this):1===arguments.length&&i.isString(t)?(n[t]=[],this):2===arguments.length&&i.isString(t)&&i.isFunction(e)?(i.remove(n[t],e),this):void 0},has:function(t,e){var n=this.__listeners;return 0===arguments.length&&!i.isBlank(n)||(!(1!==arguments.length||!n[t]||i.isBlank(n[t]))||!(2!==arguments.length||!n[t]||n[t].indexOf(e)===-1))},trigger:function(t){var e=this,n=e.__listeners,r=n[t.type];if(t.target=e,i.notNull(r)&&i.each(r,function(n){n.call(e,t)}),t.bubbles){var a=e.get("parent");a&&!t.propagationStopped&&a.trigger(t)}return e}}),t.exports=r},function(t,e,n){"use strict";var i=n(2),r=n(1),a=r.Matrix3,s=(r.Vector3,n(3)),o=function(){};i.augment(o,{initTransform:function(){this.__m=new a},translate:function(t,e){return this.__m.translate(t,e),this},rotate:function(t){return this.__m.rotate(s.degreeToRad(t)),this},scale:function(t,e){return this.__m.scale(t,e),this},transform:function(t){var e=this;return i.each(t,function(t){switch(t[0]){case"t":e.translate(t[1],t[2]);break;case"s":e.scale(t[1],t[2]);break;case"r":e.rotate(t[1]);break;case"m":e.__m=a.multiply(t[1],e.__m)}}),this},setTransform:function(t){return this.__m.identity(),this.transform(t)},getMatrix:function(){return this.__m},setMatrix:function(t){return this.__m=t,this},apply:function(t,e){var n=this;e=e||n;for(var r=n,s=[];r!==e;)s.unshift(r),r=r.get("parent");s.unshift(r);var o=new a;return i.each(s,function(t){o.multiply(t.__m)}),t.applyMatrix(o),this},invert:function(t,e){var n=this;e=e||n;for(var r=n,s=[];r!==e;)s.unshift(r),r=r.get("parent");s.unshift(r);var o=new a;i.each(s,function(t){o.multiply(t.__m)});var c=o.getInverse();return t.applyMatrix(c),this},resetTransform:function(){var t=this,e=t.get("context"),n=t.__m.to2DObject();e.transform(n.a,n.b,n.c,n.d,n.e,n.f)}}),t.exports=o},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(15),o=n(3),c=n(9),u=n(1),h=u.Vector2,l=n(8),f=function(t){f.superclass.constructor.call(this,t)};f.ATTRS={x:0,y:0,r:0,startAngle:0,endAngle:0,clockwise:!1,lineWidth:1,arrow:!1},i.extend(f,r),i.augment(f,{canStroke:!0,type:"arc",__setAttrR:function(t,e){return t>=0?t:(l.warn("r \u5fc5\u987b\u5927\u4e8e0"),e)},__setAttrClockwise:function(t,e){return i.isBoolean(t)?t:(l.warn("clockwise \u5fc5\u987b\u662fboolean\u503c"),e)},__setAttrStartAngle:function(t){return o.degreeToRad(t)},__getAttrStartAngle:function(t){return o.radToDegree(t)},__setAttrEndAngle:function(t){return o.degreeToRad(t)},__getAttrEndAngle:function(t){return o.radToDegree(t)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrR:function(){this.__calculateBox()},__afterSetAttrStartAngle:function(){this.__calculateBox()},__afterSetAttrEndAngle:function(){this.__calculateBox()},__afterSetAttrClockwise:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.r,a=e.startAngle,o=e.endAngle,c=e.clockwise,u=e.lineWidth,h=s.box(n,i,r,a,o,c),l=u/2;h.minX-=l,h.minY-=l,h.maxX+=l,h.maxY+=l,this.set("box",h)},isPointInPath:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,o=i.r,c=i.startAngle,u=i.endAngle,h=i.clockwise,l=i.lineWidth;return!!n.hasStroke()&&a.arcline(r,s,o,c,u,h,l,t,e)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x,r=n.y,a=n.r,s=n.startAngle,o=n.endAngle,u=n.clockwise,l=n.lineWidth,f=n.arrow;if(e.beginPath(),e.arc(i,r,a,s,o,u),f){var x={x:i+a*Math.cos(o),y:r+a*Math.sin(o)},g=new h(-a*Math.sin(o),a*Math.cos(o));u&&g.multiplyScaler(-1),c.makeArrow(e,g,x,l)}}}),t.exports=f},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(8),o=function(t){o.superclass.constructor.call(this,t)};o.ATTRS={x:0,y:0,r:0,lineWidth:1},i.extend(o,r),i.augment(o,{canFill:!0,canStroke:!0,type:"circle",__setAttrR:function(t,e){return t>=0?t:(s.warn("r \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrR:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){ -this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.r,a=e.lineWidth,s=a/2+r;this.set("box",{minX:n-s,minY:i-s,maxX:n+s,maxY:i+s})},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,o=i.r;return a.circle(r,s,o,t,e)},__isPointInStroke:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,o=i.r,c=i.lineWidth;return a.arcline(r,s,o,0,2*Math.PI,!1,c,t,e)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x,r=n.y,a=n.r;e.beginPath(),e.arc(i,r,a,0,2*Math.PI,!1),e.closePath()}}),t.exports=o},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=(n(3),n(9)),o=n(16),c=n(1),u=c.Vector2,h=function(t){h.superclass.constructor.call(this,t)};h.ATTRS={p1:null,p2:null,p3:null,p4:null,lineWidth:1,arrow:!1},i.extend(h,r),i.augment(h,{canStroke:!0,type:"cubic",__afterSetAttrP1:function(){this.__calculateBox()},__afterSetAttrP2:function(){this.__calculateBox()},__afterSetAttrP3:function(){this.__calculateBox()},__afterSetAttrP4:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.p1,r=e.p2,a=e.p3,s=e.p4;if(!(i.isNull(n)||i.isNull(r)||i.isNull(a)||i.isNull(s))){for(var c=e.lineWidth/2,u=o.extrema(n[0],r[0],a[0],s[0]),h=0,l=u.length;h0?t:(u.warn("rx \u5927\u4e8e\u7b49\u4e8e0"),e)},__setAttrRy:function(t,e){return t>0?t:(u.warn("ry \u5927\u4e8e\u7b49\u4e8e0"),e)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrRx:function(){this.__calculateBox()},__afterSetAttrRy:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.rx,a=e.ry,s=e.lineWidth,o=r+s/2,c=a+s/2;this.set("box",{minX:n-o,minY:i-c,maxX:n+o,maxY:i+c})},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,u=i.rx,h=i.ry,l=u>h?u:h,f=u>h?1:u/h,x=u>h?h/u:1,g=new c(t,e,1),d=new o;d.scale(f,x),d.translate(r,s);var p=d.getInverse();return g.applyMatrix(p),a.circle(0,0,l,g.x,g.y)},__isPointInStroke:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,u=i.rx,h=i.ry,l=i.lineWidth,f=u>h?u:h,x=u>h?1:u/h,g=u>h?h/u:1,d=new c(t,e,1),p=new o;p.scale(x,g),p.translate(r,s);var m=p.getInverse();return d.applyMatrix(m),a.arcline(0,0,f,0,2*Math.PI,!1,l,d.x,d.y)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x,r=n.y,a=n.rx,s=n.ry,c=a>s?a:s,u=a>s?1:a/s,h=a>s?s/a:1,l=new o;l.scale(u,h),l.translate(i,r);var f=l.to2DObject();e.beginPath(),e.save(),e.transform(f.a,f.b,f.c,f.d,f.e,f.f),e.arc(0,0,c,0,2*Math.PI),e.restore(),e.closePath()}}),t.exports=h},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(3),o=n(15),c=n(1),u=c.Vector2,h=n(8),l=function(t){l.superclass.constructor.call(this,t)};l.ATTRS={x:0,y:0,rs:0,re:0,startAngle:0,endAngle:0,clockwise:!1,lineWidth:1},i.extend(l,r),i.augment(l,{canFill:!0,canStroke:!0,type:"fan",__setAttrRs:function(t,e){return t>=0?t:(h.warn("rs \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__setAttrRe:function(t,e){return t>=0?t:(h.warn("re \u5fc5\u987b\u5927\u4e8e\u7b49\u4f600"),e)},__setAttrClockwise:function(t,e){return i.isBoolean(t)?t:(h.warn("clockwise \u5fc5\u987b\u4e3aboolean\u503c"),e)},__setAttrStartAngle:function(t){return s.degreeToRad(t)},__getAttrStartAngle:function(t){return s.radToDegree(t)},__setAttrEndAngle:function(t){return s.degreeToRad(t)},__getAttrEndAngle:function(t){return s.radToDegree(t)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrRs:function(){this.__calculateBox()},__afterSetAttrRe:function(){this.__calculateBox()},__afterSetAttrStartAngle:function(){this.__calculateBox()},__afterSetAttrEndAngle:function(){this.__calculateBox()},__afterSetAttrClockwise:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.rs,a=e.re,s=e.startAngle,c=e.endAngle,u=e.clockwise,h=e.lineWidth,l=o.box(n,i,r,s,c,u),f=o.box(n,i,a,s,c,u),x=Math.min(l.minX,f.minX),g=Math.min(l.minY,f.minY),d=Math.max(l.maxX,f.maxX),p=Math.max(l.maxY,f.maxY),m=h/2;this.set("box",{minX:x-m,minY:g-m,maxX:d+m,maxY:p+m})},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.__attrs,r=i.x,a=i.y,c=i.rs,h=i.re,l=i.startAngle,f=i.endAngle,x=i.clockwise,g=new u(1,0),d=new u(t-r,e-a),p=g.angleTo(d),m=o.nearAngle(p,l,f,x);if(s.equal(p,m)){var _=d.lengthSq();if(c*c<=_&&_<=h*h)return!0}return!1},__isPointInStroke:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,o=i.rs,c=i.re,u=i.startAngle,h=i.endAngle,l=i.clockwise,f=i.lineWidth,x={x:Math.cos(u)*o+r,y:Math.sin(u)*o+s},g={x:Math.cos(u)*c+r,y:Math.sin(u)*c+s},d={x:Math.cos(h)*o+r,y:Math.sin(h)*o+s},p={x:Math.cos(h)*c+r,y:Math.sin(h)*c+s};return!!a.line(x.x,x.y,g.x,g.y,f,t,e)||(!!a.line(d.x,d.y,p.x,p.y,f,t,e)||(!!a.arcline(r,s,o,u,h,l,f,t,e)||!!a.arcline(r,s,c,u,h,l,f,t,e)))},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x,r=n.y,a=n.rs,s=n.re,o=n.startAngle,c=n.endAngle,u=n.clockwise,h={x:Math.cos(o)*a+i,y:Math.sin(o)*a+r},l={x:Math.cos(o)*s+i,y:Math.sin(o)*s+r},f={x:Math.cos(c)*a+i,y:Math.sin(c)*a+r};({x:Math.cos(c)*s+i,y:Math.sin(c)*s+r});e.beginPath(),e.moveTo(h.x,h.y),e.lineTo(l.x,l.y),e.arc(i,r,s,o,c,u),e.lineTo(f.x,f.y),e.arc(i,r,a,c,o,!u),e.closePath()}}),t.exports=l},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(8),o=function(t){o.superclass.constructor.call(this,t)};o.ATTRS={x:0,y:0,img:void 0,width:0,height:0,sx:null,sy:null,swidth:null,sheight:null},i.extend(o,r),i.augment(o,{type:"image",getDefaultAttrs:function(){return o.ATTRS},__setAttrWidth:function(t,e){return t>=0?t:(s.warn("width \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__setAttrHeight:function(t,e){return t>=0?t:(s.warn("height \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrWidth:function(){this.__calculateBox()},__afterSetAttrHeight:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.width,a=e.height;this.set("box",{minX:n,minY:i,maxX:n+r,maxY:i+a})},isPointInPath:function(t,e){var n=this,i=n.__attrs;if(n.get("toDraw")||!i.img)return!1;var r=i.x,s=i.y,o=i.width,c=i.height;return a.rect(r,s,o,c,t,e)},__setLoading:function(t){var e=this,n=e.get("canvas");return t===!1&&e.get("toDraw")===!0&&(e.__cfg.loading=!1,n.draw()),t},__setAttrImg:function(t){var e=this,n=e.__attrs;e.get("context");if(!i.isString(t))return t instanceof Image?(n.width||e.attr("width",t.width),n.height||e.attr("height",t.height),t):t instanceof HTMLElement&&i.isString(t.nodeName)&&"CANVAS"===t.nodeName.toUpperCase()?(n.width||e.attr("width",Number(t.getAttribute("width"))),n.height||e.attr("height",Number(t.getAttribute("height"))),t):t instanceof ImageData?(n.width||e.attr("width",t.width),n.height||e.attr("height",t.height),t):void 0;var r=new Image;r.onload=function(){return!e.get("destroyed")&&(e.attr("imgSrc",t),e.attr("img",r),void e.set("loading",!1))},r.src=t,e.set("loading",!0)},drawInner:function(){var t=this;return t.get("loading")?void t.set("toDraw",!0):void t.__drawImage()},__drawImage:function(){var t=this,e=t.get("context"),n=t.__attrs,r=n.x,a=n.y,s=n.img,o=n.width,c=n.height,u=n.sx,h=n.sy,l=n.swidth,f=n.sheight;return t.set("toDraw",!1),s instanceof Image||s instanceof HTMLElement&&i.isString(s.nodeName)&&"CANVAS"===s.nodeName.toUpperCase()?i.isNull(u)||i.isNull(h)||i.isNull(l)||i.isNull(f)?void e.drawImage(s,r,a,o,c):i.notNull(u)&&i.notNull(h)&&i.notNull(l)&&i.notNull(f)?void e.drawImage(s,u,h,l,f,r,a,o,c):void 0:s instanceof ImageData?void e.putImageData(s,r,a,u||0,h||0,l||o,f||c):void 0}}),t.exports=o},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(1),o=s.Vector2,c=n(9),u=n(25),h=function(t){h.superclass.constructor.call(this,t)};h.ATTRS={x1:0,y1:0,x2:0,y2:0,lineWidth:1,arrow:!1},i.extend(h,r),i.augment(h,{canStroke:!0,type:"line",__afterSetAttrX1:function(){this.__calculateBox()},__afterSetAttrY1:function(){this.__calculateBox()},__afterSetAttrX2:function(){this.__calculateBox()},__afterSetAttrY2:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x1,i=e.y1,r=e.x2,a=e.y2,s=e.lineWidth;this.set("box",u.box(n,i,r,a,s))},isPointInPath:function(t,e){var n=this,i=n.__attrs,r=i.x1,s=i.y1,o=i.x2,c=i.y2,u=i.lineWidth;return!!n.hasStroke()&&a.line(r,s,o,c,u,t,e)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x1,r=n.y1,a=n.x2,s=n.y2,u=n.arrow,h=n.lineWidth;if(e.beginPath(),e.moveTo(i,r),u){var l=new o(a-i,s-r),f=c.getEndPoint(l,new o(a,s),h);e.lineTo(f.x,f.y),c.makeArrow(e,l,f,h)}else e.lineTo(a,s)},getPoint:function(t){var e=this.__attrs;return{x:u.at(e.x1,e.x2,t),y:u.at(e.y1,e.y2,t)}}}),t.exports=h},function(t,e,n){"use strict";function i(t,e,n,i,r){return e*Math.cos(t)*Math.cos(r)-n*Math.sin(t)*Math.sin(r)+i}function r(t,e,n,i,r){return e*Math.sin(t)*Math.cos(r)+n*Math.cos(t)*Math.sin(r)+i}function a(t,e,n){return Math.atan(-n/e*Math.tan(t))}function s(t,e,n){return Math.atan(n/(e*Math.tan(t)))}var o=n(1);o.Vector2,n(3);t.exports={xAt:i,yAt:r,xExtrema:a,yExtrema:s}},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(69),s=n(24),o=function(t){o.superclass.constructor.call(this,t)};o.ATTRS={path:null,lineWidth:1},i.extend(o,r),i.augment(o,{canFill:!0,canStroke:!0,type:"path",__afterSetAttrPath:function(t){var e=this;if(i.isNull(t))return e.set("segments",null),void e.set("box",void 0);var n,r=s.parsePath(t),o=[];!i.isArray(r)||0===r.length||"M"!==r[0][0]&&"m"!==r[0][0]||(i.each(r,function(t){n=new a(t,n),o.push(n)}),e.set("segments",o),e.__calculateBox())},__afterSetAttrLineWidth:function(t){var e=this;e.get("segments");e.__calculateBox()},__afterSetAttrAll:function(t){var e=this;t.path?e.__afterSetAttrPath(t.path):e.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.lineWidth,r=t.get("segments");if(r){var a=1/0,s=-(1/0),o=1/0,c=-(1/0);i.each(r,function(t,e){t.getBBox(n);var i=t.box;i&&(i.minXs&&(s=i.maxX),i.minYc&&(c=i.maxY))}),this.set("box",{minX:a,minY:o,maxX:s,maxY:c})}},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.get("context");if(i)return n.createPath(),i.isPointInPath(t,e)},__isPointInStroke:function(t,e){for(var n=this,i=n.get("segments"),r=n.__attrs,a=r.lineWidth,s=0,o=i.length;so&&(o=e),nc&&(c=n)});var u=r/2;t.set("box",{minX:a-u,minY:s-u,maxX:o+u,maxY:c+u})}},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.get("context");return n.createPath(),i.isPointInPath(t,e)},__isPointInStroke:function(t,e){var n=this,i=n.__attrs,r=i.points;if(r.length<2)return!1;var s=i.lineWidth,o=r.slice(0);return r.length>=3&&o.push(r[0]),a.polyline(o,s,t,e)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,r=n.points;r.length<2||(e.beginPath(),i.each(r,function(t,n){0===n?e.moveTo(t[0],t[1]):e.lineTo(t[0],t[1])}),e.closePath())}}),t.exports=s},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(1),o=s.Vector2,c=n(9),u=function(t){u.superclass.constructor.call(this,t)};u.ATTRS={points:null,lineWidth:1,arrow:!1},i.extend(u,r),i.augment(u,{canStroke:!0,type:"polyline",__afterSetAttrPoints:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.lineWidth,r=e.points;if(r&&0!==r.length){var a=1/0,s=1/0,o=-(1/0),c=-(1/0);i.each(r,function(t){var e=t[0],n=t[1];eo&&(o=e),nc&&(c=n)});var u=n/2;this.set("box",{minX:a-u,minY:s-u,maxX:o+u,maxY:c+u})}},isPointInPath:function(t,e){var n=this,i=n.__attrs;if(n.hasStroke()){var r=i.points;if(r.length<2)return!1;var s=i.lineWidth;return a.polyline(r,s,t,e)}return!1},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.points,r=n.arrow,a=n.lineWidth;if(!(i.length<2)){e.beginPath(),e.moveTo(i[0][0],i[0][1]);for(var s=1,u=i.length-1;s=0?t:(s.warn("width \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__setAttrHeight:function(t,e){return t>=0?t:(s.warn("height \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__setAttrRadius:function(t,e){return t>=0?t:(s.warn("radius \u5fc5\u987b\u5927\u4e8e\u7b49\u4e8e0"),e)},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrWidth:function(){this.__calculateBox()},__afterSetAttrHeight:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(){this.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.width,a=e.height,s=e.lineWidth,o=s/2;this.set("box",{minX:n-o,minY:i-o,maxX:n+r+o,maxY:i+a+o})},isPointInPath:function(t,e){var n=this,i=n.hasFill(),r=n.hasStroke();return i&&r?n.__isPointInFill(t,e)||n.__isPointInStroke(t,e):i?n.__isPointInFill(t,e):!!r&&n.__isPointInStroke(t,e)},__isPointInFill:function(t,e){var n=this,i=n.__attrs,r=(i.x,i.y,i.width,i.height,i.radius,n.get("context"));return!!r&&(n.createPath(),r.isPointInPath(t,e))},__isPointInStroke:function(t,e){var n=this,i=n.__attrs,r=i.x,s=i.y,o=i.width,c=i.height,u=i.radius,h=i.lineWidth;if(0===u){var l=h/2;return a.line(r-l,s,r+o+l,s,h,t,e)||a.line(r+o,s-l,r+o,s+c+l,h,t,e)||a.line(r+o+l,s+c,r-l,s+c,h,t,e)||a.line(r,s+c+l,r,s-l,h,t,e)}return a.line(r+u,s,r+o-u,s,h,t,e)||a.line(r+o,s+u,r+o,s+c-u,h,t,e)||a.line(r+o-u,s+c,r+u,s+c,h,t,e)||a.line(r,s+c-u,r,s+u,h,t,e)||a.arcline(r+o-u,s+u,u,1.5*Math.PI,2*Math.PI,!1,h,t,e)||a.arcline(r+o-u,s+c-u,u,0,.5*Math.PI,!1,h,t,e)||a.arcline(r+u,s+c-u,u,.5*Math.PI,Math.PI,!1,h,t,e)||a.arcline(r+u,s+u,u,Math.PI,1.5*Math.PI,!1,h,t,e)},createPath:function(){var t=this,e=t.get("context"),n=t.__attrs,i=n.x,r=n.y,a=n.width,s=n.height,o=n.radius;e.beginPath(),0===o?(e.moveTo(i,r),e.lineTo(i+a,r),e.lineTo(i+a,r+s),e.lineTo(i,r+s),e.lineTo(i,r)):(e.moveTo(i+o,r),e.lineTo(i+a-o,r),e.arc(i+a-o,r+o,o,-Math.PI/2,0,!1),e.lineTo(i+a,r+s-o),e.arc(i+a-o,r+s-o,o,0,Math.PI/2,!1),e.lineTo(i+o,r+s),e.arc(i+o,r+s-o,o,Math.PI/2,Math.PI,!1),e.lineTo(i,r+o),e.arc(i+o,r+o,o,Math.PI,3*Math.PI/2,!1)),e.closePath()}}),t.exports=o},function(t,e,n){"use strict";var i=n(2),r=n(6),a=n(5),s=n(8),o=function(t){o.superclass.constructor.call(this,t)};o.ATTRS={x:0,y:0,text:null,fontSize:12,fontFamily:"sans-serif",fontStyle:"normal",fontWeight:"normal",fontVariant:"normal",textAlign:"start",textBaseline:"bottom",lineWidth:1};var c={start:1,right:1,left:1,end:1,center:1},u={top:1,middle:1,bottom:1},h={normal:1,italic:1,oblique:1},l={normal:1,"small-caps":1},f={normal:1,bold:1,bolder:1,lighter:1,100:1,200:1,300:1,400:1,500:1,600:1,700:1,800:1,900:1};i.extend(o,r),i.augment(o,{canFill:!0,canStroke:!0,type:"text",__setAttrTextAlign:function(t,e){return t in c?t:e},__setAttrTextBaseline:function(t,e){return t in u?t:e},__setAttrFontSize:function(t,e){return t>=12?t:e},__setAttrFontStyle:function(t,e){return t in h?t:e},__setAttrFontVariant:function(t,e){return t in l?t:e},__setAttrFontWeight:function(t,e){return t in f?t:e},__assembleFont:function(){var t=this,e=t.attr("fontSize"),n=t.attr("fontFamily"),i=t.attr("fontWeight"),r=t.attr("fontStyle"),a=t.attr("fontVariant");t.attr("font",[r,a,i,e+"px",n].join(" "))},__afterSetAttrFontSize:function(t){var e=this;e.attr({height:t}),e.__assembleFont()},__afterSetAttrFontFamily:function(){this.__assembleFont()},__afterSetAttrFontWeight:function(){this.__assembleFont()},__afterSetAttrFontStyle:function(){this.__assembleFont()},__afterSetAttrFontVariant:function(){this.__assembleFont()},__afterSetAttrFont:function(){this.attr("width",this.measureText())},__afterSetAttrText:function(){this.attr("width",this.measureText())},__afterSetAttrTextAlign:function(){this.__calculateBox()},__afterSetAttrTextBaseline:function(){this.__calculateBox()},__afterSetAttrX:function(){this.__calculateBox()},__afterSetAttrY:function(){this.__calculateBox()},__afterSetAttrWidth:function(){this.__calculateBox()},__afterSetAttrLineWidth:function(){this.__calculateBox()},__afterSetAttrAll:function(t){var e=this;"fontSize"in t&&e.attr("height",t.fontSize),("fontSize"in t||"fontWeight"in t||"fontStyle"in t||"fontVariant"in t||"fontFamily"in t)&&e.__assembleFont(),"text"in t&&e.__afterSetAttrText(t.text),e.__calculateBox()},__calculateBox:function(){var t=this,e=t.__attrs,n=e.x,i=e.y,r=e.width;if(r){var a=e.height,s=e.textAlign,o=e.textBaseline,c=e.lineWidth,u={x:n,y:i-a};s&&("end"===s||"right"===s?u.x-=r:"center"===s&&(u.x-=r/2)),o&&("top"===o?u.y+=a:"middle"===o&&(u.y+=a/2)),this.set("startPoint",u);var h=c/2;this.set("box",{minX:u.x-h,minY:u.y-h,maxX:u.x+r+h,maxY:u.y+a+h})}},isPointInPath:function(t,e){var n=this,i=n.getBBox();if(n.hasFill()||n.hasStroke())return a.box(i.minX,i.maxX,i.minY,i.maxY,t,e)},drawInner:function(){var t=this,e=t.get("context"),n=t.__attrs,r=n.text,a=n.x,s=n.y;i.isNull(r)||(e.beginPath(),t.hasFill()&&e.fillText(r,a,s),t.hasStroke()&&e.strokeText(r,a,s))},measureText:function(){var t=this,e=t.__attrs,n=e.text,r=e.font;if(!i.isNull(n)){var a=s.backupContext;a.save(),a.font=r;var o=a.measureText(n).width;return a.restore(),o}}}),t.exports=o},function(t,e,n){"use strict";function i(t,e){this.preSegment=e,this.init(t,e)}function r(t,e,n){return{x:n.x+t,y:n.y+e}}function a(t,e){return{x:e.x+(e.x-t.x),y:e.y+(e.y-t.y)}}function s(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1])}function o(t,e){return(t[0]*e[0]+t[1]*e[1])/(s(t)*s(e))}function c(t,e){return(t[0]*e[1]1&&(r*=Math.sqrt(m),a*=Math.sqrt(m));var _=Math.sqrt((r*r*(a*a)-r*r*(p*p)-a*a*(d*d))/(r*r*(p*p)+a*a*(d*d)));n===i&&(_*=-1),isNaN(_)&&(_=0);var v=_*r*p/a,y=_*-a*d/r,M=(h+x)/2+Math.cos(u)*v-Math.sin(u)*y,S=(f+g)/2+Math.sin(u)*v+Math.cos(u)*y,b=c([1,0],[(d-v)/r,(p-y)/a]),w=[(d-v)/r,(p-y)/a],A=[(-1*d-v)/r,(-1*p-y)/a],P=c(w,A);return o(w,A)<=-1&&(P=Math.PI),o(w,A)>=1&&(P=0),0===i&&P>0&&(P-=2*Math.PI),1===i&&P<0&&(P+=2*Math.PI),[t,M,S,r,a,b,P,u,i]}var h=n(2),l=n(3),f=n(1),x=n(5),g=n(16),d=n(17),p=n(62),m=f.Vector3,_=f.Matrix3;h.augment(i,{init:function(t,e){var n=t[0];e=e||{endPoint:{x:0,y:0}};var i=/[a-z]/.test(n),s=n.toUpperCase(),o=t;switch(s){case"M":if(i)var c=r(o[1],o[2],e.endPoint);else var c={x:o[1],y:o[2]};this.command="M",this.params=[e.endPoint,c],this.subStart=c,this.endPoint=c;break;case"L":if(i)var c=r(o[1],o[2],e.endPoint);else var c={x:o[1],y:o[2]};this.command="L",this.params=[e.endPoint,c],this.subStart=e.subStart,this.endPoint=c;break;case"H":if(i)var c=r(o[1],0,e.endPoint);else var c={x:o[1],y:e.endPoint.y};this.command="L",this.params=[e.endPoint,c],this.subStart=e.subStart,this.endPoint=c;break;case"V":if(i)var c=r(0,o[1],e.endPoint);else var c={x:e.endPoint.x,y:o[1]};this.command="L",this.params=[e.endPoint,c],this.subStart=e.subStart,this.endPoint=c;break;case"Q":if(i)var h=r(o[1],o[2],e.endPoint),l=r(o[3],o[4],e.endPoint);else var h={x:o[1],y:o[2]},l={x:o[3],y:o[4]};this.command="Q",this.params=[e.endPoint,h,l],this.subStart=e.subStart,this.endPoint=l;break;case"T":if(i)var l=r(o[1],o[2],e.endPoint);else var l={x:o[1],y:o[2]};if("Q"===e.command){var h=a(e.params[1],e.endPoint);this.command="Q",this.params=[e.endPoint,h,l],this.subStart=e.subStart,this.endPoint=l}else this.command="TL",this.params=[e.endPoint,l],this.subStart=e.subStart,this.endPoint=l;break;case"C":if(i)var h=r(o[1],o[2],e.endPoint),l=r(o[3],o[4],e.endPoint),f=r(o[5],o[6],e.endPoint);else var h={x:o[1],y:o[2]},l={x:o[3],y:o[4]},f={x:o[5],y:o[6]};this.command="C",this.params=[e.endPoint,h,l,f],this.subStart=e.subStart,this.endPoint=f;break;case"S":if(i)var l=r(o[1],o[2],e.endPoint),f=r(o[3],o[4],e.endPoint);else var l={x:o[1],y:o[2]},f={x:o[3],y:o[4]};if("C"===e.command){var h=a(e.params[2],e.endPoint);this.command="C",this.params=[e.endPoint,h,l,f],this.subStart=e.subStart,this.endPoint=f}else this.command="SQ",this.params=[e.endPoint,l,f],this.subStart=e.subStart,this.endPoint=f;break;case"A":var x=o[1],g=o[2],d=o[3],p=o[4],m=o[5];if(i)var c=r(o[6],o[7],e.endPoint);else var c={x:o[6],y:o[7]};this.command="A",this.params=u(e.endPoint,c,p,m,x,g,d),this.subStart=e.subStart,this.endPoint=c;break;case"Z":this.command="Z",this.params=[e.endPoint,e.subStart],this.subStart=e.subStart,this.endPoint=e.subStart}},isInside:function(t,e,n){var i=this,r=i.command,a=i.params,s=i.box;if(s&&!x.box(s.minX,s.maxX,s.minY,s.maxY,t,e))return!1;switch(r){case"M":return!1;case"TL":case"L":case"Z":return x.line(a[0].x,a[0].y,a[1].x,a[1].y,n,t,e);case"SQ":case"Q":return x.quadraticline(a[0].x,a[0].y,a[1].x,a[1].y,a[2].x,a[2].y,n,t,e);case"C":return x.cubicline(a[0].x,a[0].y,a[1].x,a[1].y,a[2].x,a[2].y,a[3].x,a[3].y,n,t,e);case"A":var o=a,c=o[1],u=o[2],h=o[3],l=o[4],f=o[5],g=o[6],d=o[7],p=o[8],v=h>l?h:l,y=h>l?1:h/l,M=h>l?l/h:1,o=new m(t,e,1),S=new _;return S.translate(-c,-u),S.rotate(-d),S.scale(1/y,1/M),o.applyMatrix(S),x.arcline(0,0,v,f,f+g,1-p,n,o.x,o.y)}return!1},draw:function(t){var e=this.command,n=this.params;switch(e){case"M":t.moveTo(n[1].x,n[1].y);break;case"TL":case"L":t.lineTo(n[1].x,n[1].y);break;case"SQ":case"Q":var i=n[1],r=n[2];t.quadraticCurveTo(i.x,i.y,r.x,r.y);break;case"C":var i=n[1],r=n[2],a=n[3];t.bezierCurveTo(i.x,i.y,r.x,r.y,a.x,a.y);break;case"A":var s=n,o=s[1],c=s[2],u=s[3],h=s[4],l=s[5],f=s[6],x=s[7],g=s[8],d=u>h?u:h,p=u>h?1:u/h,m=u>h?h/u:1;t.translate(o,c),t.rotate(x),t.scale(p,m),t.arc(0,0,d,l,l+f,1-g),t.scale(1/p,1/m),t.rotate(-x),t.translate(-o,-c);break;case"Z":t.closePath()}},getBBox:function(t){var e=t/2,n=this.params;switch(this.command){case"M":case"Z":break;case"TL":case"L":this.box={minX:Math.min(n[0].x,n[1].x)-e,maxX:Math.max(n[0].x,n[1].x)+e,minY:Math.min(n[0].y,n[1].y)-e,maxY:Math.max(n[0].y,n[1].y)+e};break;case"SQ":case"Q":for(var i=d.extrema(n[0].x,n[1].x,n[2].x),r=0,a=i.length;rb&&(b=P)}for(var C=p.yExtrema(m,h,l),I=1/0,T=-(1/0),B=[v,y],r=2*-Math.PI;r<=2*Math.PI;r+=Math.PI){var k=C+r;1===_?vT&&(T=F)}this.box={minX:S-e,maxX:b+e,minY:I-e,maxY:T+e}}}}),t.exports=i},function(t,e){t.exports={name:"g",attrs:{id:"Group-15",transform:"translate(596.000000, 166.000000)"},childs:[{name:"g",attrs:{id:"egg",transform:"translate(186.000000, 130.000000)",fill:"#FFFFFF"},childs:[{name:"path",attrs:{d:"M67.941389,0.759375 L67.2258742,6.90040761 C63.8434233,6.90040761 61.7294233,7.39564722 60.8838106,8.3861413 L60.8838106,8.81535326 C62.1630708,10.4661767 62.8026914,12.3150713 62.8026914,14.3620924 C62.8026914,17.5096625 61.6589631,20.1949617 59.3714723,22.4180707 C57.0839815,24.6411796 53.6744789,25.7527174 49.1428621,25.7527174 C46.9312596,25.7527174 45.1208083,25.5766322 43.7114537,25.2244565 C42.5622878,25.75272 41.9877134,26.4460555 41.9877134,27.3044837 C41.9877134,28.6471535 42.6761185,29.5881087 44.0529495,30.1273777 C45.4297804,30.6666467 48.6333036,30.9362772 53.6636151,30.9362772 C62.3582483,30.9362772 66.7054997,33.9407308 66.7054997,39.9497283 C66.7054997,44.1758363 64.8950484,47.5324604 61.2740914,50.0197011 C57.6531344,52.5069418 53.2516779,53.7505435 48.0695898,53.7505435 C37.9222373,53.7505435 32.8486371,50.514978 32.8486371,44.04375 C32.8486371,42.0407509 33.417791,40.3404282 34.5561158,38.942731 C35.6944406,37.5450338 37.0116252,36.6921211 38.5077093,36.3839674 L38.5077093,35.8887228 C36.6863896,34.7221409 35.7757434,32.9392783 35.7757434,30.5400815 C35.7757434,28.1188738 37.0224614,26.093894 39.5159347,24.4650815 L39.5159347,24.0028533 C36.0033897,22.2199639 34.2471434,19.4356168 34.2471434,15.6497283 C34.2471434,12.4141143 35.4559178,9.70680437 37.8735029,7.52771739 C40.2910879,5.34863041 44.0258214,4.25910326 49.0778153,4.25910326 C52.5253132,4.25910326 55.3222979,4.91942274 57.4688532,6.24008152 L57.9241809,6.24008152 C58.5529698,2.08000637 60.6561289,0 64.2337211,0 C65.6430756,0 66.8789526,0.253122469 67.941389,0.759375 L67.941389,0.759375 Z M58.6722192,42.3929348 C58.6722192,40.7641223 58.0597012,39.5865525 56.8346469,38.8601902 C55.6095926,38.1338279 53.501013,37.7706522 50.508845,37.7706522 C48.0804188,37.7706522 45.6303469,37.6605989 43.1585559,37.4404891 C41.2505067,38.5850601 40.2964964,40.2908854 40.2964964,42.5580163 C40.2964964,46.7841244 43.0717991,48.8971467 48.6224876,48.8971467 C51.7230675,48.8971467 54.1731394,48.3028592 55.9727767,47.1142663 C57.772414,45.9256734 58.6722192,44.351912 58.6722192,42.3929348 L58.6722192,42.3929348 Z M54.8344576,15.2205163 C54.8344576,13.2395281 54.2056781,11.7317987 52.9481002,10.6972826 C51.6905223,9.66276657 50.2486659,9.1455163 48.6224876,9.1455163 C46.9963093,9.1455163 45.5707144,9.64625858 44.3456601,10.6477582 C43.1206058,11.6492577 42.5080878,13.1184686 42.5080878,15.0554348 C42.5080878,17.014412 43.1368673,18.5001308 44.3944452,19.5126359 C45.6520231,20.5251409 47.1155616,21.0313859 48.7851046,21.0313859 C50.3679182,21.0313859 51.771831,20.5416489 52.9968853,19.5621603 C54.2219396,18.5826717 54.8344576,17.1354715 54.8344576,15.2205163 Z M29.6288202,17.6307065 C29.6288202,18.7532665 29.5637741,20.2169747 29.4336798,22.021875 L8.4886082,22.3850543 C8.79216148,24.872295 9.843741,26.9412961 11.6433783,28.5921196 C13.4430156,30.242943 15.8063593,31.0683424 18.7334802,31.0683424 C21.9858367,31.0683424 25.444124,30.5731028 29.1084458,29.5826087 L28.4579777,35.9877717 C25.4658097,37.0883207 21.7581788,37.638587 17.3349738,37.638587 C11.7409205,37.638587 7.45329478,36.0428149 4.47196792,32.8512228 C1.49064107,29.6596308 0,25.8187725 0,21.3285326 C0,16.6401939 1.41475389,12.5737265 4.2443041,9.12900815 C7.07385432,5.68428984 11.0037262,3.96195652 16.0340377,3.96195652 C20.6307017,3.96195652 24.0456248,5.22756887 26.2789097,7.75883152 C28.5121945,10.2900942 29.6288202,13.5806863 29.6288202,17.6307065 Z M22.0183439,17.3335598 C22.0183439,12.2710345 19.8935028,9.73980978 15.6437569,9.73980978 C11.5891524,9.73980978 9.1824446,12.3590771 8.4235614,17.5976902 L22.0183439,17.3335598 Z M104.627788,0.759375 L103.912273,6.90040761 C100.529822,6.90040761 98.415822,7.39564722 97.5702093,8.3861413 L97.5702093,8.81535326 C98.8494695,10.4661767 99.48909,12.3150713 99.48909,14.3620924 C99.48909,17.5096625 98.3453618,20.1949617 96.057871,22.4180707 C93.7703802,24.6411796 90.3608776,25.7527174 85.8292607,25.7527174 C83.6176583,25.7527174 81.8072069,25.5766322 80.3978524,25.2244565 C79.2486864,25.75272 78.6741121,26.4460555 78.6741121,27.3044837 C78.6741121,28.6471535 79.3625172,29.5881087 80.7393482,30.1273777 C82.1161791,30.6666467 85.3197023,30.9362772 90.3500138,30.9362772 C99.044647,30.9362772 103.391898,33.9407308 103.391898,39.9497283 C103.391898,44.1758363 101.581447,47.5324604 97.9604901,50.0197011 C94.3395331,52.5069418 89.9380766,53.7505435 84.7559884,53.7505435 C74.6086359,53.7505435 69.5350358,50.514978 69.5350358,44.04375 C69.5350358,42.0407509 70.1041897,40.3404282 71.2425145,38.942731 C72.3808393,37.5450338 73.6980239,36.6921211 75.1941079,36.3839674 L75.1941079,35.8887228 C73.3727883,34.7221409 72.4621421,32.9392783 72.4621421,30.5400815 C72.4621421,28.1188738 73.7088601,26.093894 76.2023334,24.4650815 L76.2023334,24.0028533 C72.6897883,22.2199639 70.9335421,19.4356168 70.9335421,15.6497283 C70.9335421,12.4141143 72.1423165,9.70680437 74.5599016,7.52771739 C76.9774866,5.34863041 80.7122201,4.25910326 85.7642139,4.25910326 C89.2117119,4.25910326 92.0086966,4.91942274 94.1552519,6.24008152 L94.6105796,6.24008152 C95.2393685,2.08000637 97.3425275,0 100.92012,0 C102.329474,0 103.565351,0.253122469 104.627788,0.759375 Z M91.5208563,15.2205163 C91.5208563,13.2395281 90.8920768,11.7317987 89.6344989,10.6972826 C88.376921,9.66276657 86.9350646,9.1455163 85.3088863,9.1455163 C83.682708,9.1455163 82.2571131,9.64625858 81.0320588,10.6477582 C79.8070045,11.6492577 79.1944865,13.1184686 79.1944865,15.0554348 C79.1944865,17.014412 79.823266,18.5001308 81.0808439,19.5126359 C82.3384218,20.5251409 83.8019603,21.0313859 85.4715033,21.0313859 C87.0543168,21.0313859 88.4582297,20.5416489 89.683284,19.5621603 C90.9083383,18.5826717 91.5208563,17.1354715 91.5208563,15.2205163 Z M95.3586178,42.3929348 C95.3586178,40.7641223 94.7460999,39.5865525 93.5210456,38.8601902 C92.2959913,38.1338279 90.1874117,37.7706522 87.1952437,37.7706522 C84.7668174,37.7706522 82.3167456,37.6605989 79.8449546,37.4404891 C77.9369054,38.5850601 76.9828951,40.2908854 76.9828951,42.5580163 C76.9828951,46.7841244 79.7581977,48.8971467 85.3088863,48.8971467 C88.4094662,48.8971467 90.8595381,48.3028592 92.6591754,47.1142663 C94.4588127,45.9256734 95.3586178,44.351912 95.3586178,42.3929348 Z", -id:"Combined-Shape"}}]},{name:"g",attrs:{id:"center",transform:"translate(90.000000, 81.000000)",fill:"#FFFFFF"},childs:[{name:"polygon",attrs:{id:"Fill-12",points:"15.7433 52.9007 0.7433 26.9197 15.7433 0.9387 45.7433 0.9387 60.7433 26.9197 45.7433 52.9007"}}]},{name:"g",attrs:{id:"lines",transform:"translate(1.000000, 2.000000)",stroke:"#5B6170"},childs:[{name:"path",attrs:{d:"M1.09756663,105.979963 L30.4010933,54.5622391",id:"Path-2"}},{name:"path",attrs:{d:"M30.5531474,54.6354443 L60.5923556,105.734546",id:"Path-3"}},{name:"path",attrs:{d:"M0.911617172,105.721093 L60.6777067,105.658745",id:"Path-4"}},{name:"path",attrs:{d:"M0.840924452,105.741293 L29.990168,158.062787",id:"Path-5"}},{name:"path",attrs:{d:"M30.0735293,158.348783 L60.5980606,104.94528",id:"Path-6"}},{name:"path",attrs:{d:"M29.9291683,157.823181 L90.598116,157.772853",id:"Path-7"}},{name:"path",attrs:{d:"M29.8450373,157.872759 L59.5410172,210.722784",id:"Path-8"}},{name:"path",attrs:{d:"M30.504042,54.5952791 L60.0609967,1.9976529",id:"Path-9"}},{name:"path",attrs:{d:"M30.5115911,54.4580219 L90.6947337,54.5266505",id:"Path-10"}},{name:"path",attrs:{d:"M60.5733881,105.592686 L90.7591339,54.5504998",id:"Path-11"}},{name:"path",attrs:{d:"M60.4287535,105.838508 L90.1041226,157.439288",id:"Path-12"}},{name:"path",attrs:{d:"M90.8011827,157.964689 L60.2158599,210.465687",id:"Path-13"}},{name:"path",attrs:{d:"M90.9230584,54.9434488 L60.0797104,1.97800556",id:"Path-14"}},{name:"path",attrs:{d:"M60.1768462,1.92633361 L120.628941,1.59414377",id:"Path-15"}},{name:"path",attrs:{d:"M90.5863676,54.5478847 L120.795781,1.51668247",id:"Path-16"}},{name:"path",attrs:{d:"M90.7134836,54.5250436 L149.707717,54.6566285",id:"Path-17"}},{name:"path",attrs:{d:"M120.605926,1.78087467 L149.620028,54.8444026",id:"Path-18"}},{name:"path",attrs:{d:"M149.636354,54.6739985 L180.613165,1.67271399",id:"Path-19"}},{name:"path",attrs:{d:"M120.398788,1.66710188 L180.633063,1.62730691",id:"Path-20"}},{name:"path",attrs:{d:"M180.634466,1.80101364 L210.651832,54.6050175",id:"Path-23"}},{name:"path",attrs:{d:"M210.732836,54.6442753 L179.723652,105.49417",id:"Path-24"}},{name:"path",attrs:{d:"M210.781915,54.5060656 L149.600138,54.9015146",id:"Path-25"}},{name:"path",attrs:{d:"M210.771197,54.3864719 L239.074291,106.183527",id:"Path-26"}},{name:"path",attrs:{d:"M239.358837,106.077684 L179.551961,105.829702",id:"Path-27"}},{name:"path",attrs:{d:"M179.614689,105.711281 L149.626538,54.470605",id:"Path-28"}},{name:"path",attrs:{d:"M59.9664127,210.661496 L120.055533,210.991735",id:"Path-29"}},{name:"path",attrs:{d:"M119.975274,210.672326 L90.7272677,157.686386",id:"Path-30"}},{name:"path",attrs:{d:"M120.13145,210.976393 L149.799102,157.672302",id:"Path-31"}},{name:"path",attrs:{d:"M120.284312,210.80986 L180.728176,210.676054",id:"Path-32"}},{name:"path",attrs:{d:"M149.646958,157.388457 L180.576238,210.799181",id:"Path-33"}},{name:"path",attrs:{d:"M149.572413,157.544888 L90.5533295,157.544888",id:"Path-34"}}]},{name:"g",attrs:{id:"points",fill:"#C5C9D5"},childs:[{name:"circle",attrs:{id:"Oval-Copy",cx:"150.5",cy:"56.5",r:"3.5"}},{name:"circle",attrs:{id:"Oval-999",cx:"92",cy:"55",r:"3"}},{name:"circle",attrs:{id:"Oval-Copy-6",cx:"180.5",cy:"107.5",r:"2.5"}},{name:"circle",attrs:{id:"Oval-Copy-16",cx:"211.5",cy:"56.5",r:"2.5"}},{name:"circle",attrs:{id:"Oval-Copy-11",cx:"240",cy:"108",r:"2"}},{name:"circle",attrs:{id:"Oval-Copy-12",cx:"61",cy:"4",r:"2"}},{name:"circle",attrs:{id:"Oval-Copy-13",cx:"2",cy:"108",r:"2"}},{name:"circle",attrs:{id:"Oval-Copy-14",cx:"31",cy:"160",r:"2"}},{name:"circle",attrs:{id:"Oval-Copy-15",cx:"121",cy:"213",r:"2"}},{name:"circle",attrs:{id:"Oval-Copy-7",cx:"121.5",cy:"3.5",r:"2.5"}},{name:"circle",attrs:{id:"Oval-Copy-17",cx:"181.5",cy:"3.5",r:"3.5"}},{name:"circle",attrs:{id:"Oval-Copy-9",cx:"91.5",cy:"159.5",r:"2.5"}},{name:"circle",attrs:{id:"Oval-Copy-10",cx:"181.5",cy:"212.5",r:"2.5"}},{name:"circle",attrs:{id:"Oval-Copy-2",cx:"61.5",cy:"107.5",r:"3.5"}},{name:"circle",attrs:{id:"Oval-Copy-3",cx:"150.5",cy:"159.5",r:"3.5"}},{name:"circle",attrs:{id:"Oval-Copy-4",cx:"60.5",cy:"212.5",r:"3.5"}},{name:"circle",attrs:{id:"Oval-Copy-5",cx:"31.5",cy:"56.5",r:"3.5"}}]}]}},function(t,e,n){"use strict";function i(t){if(!t._attrs&&t!==r){var e=t.superclass.constructor;e&&!e._attrs&&i(e),t._attrs={},a.mix(!0,t._attrs,e._attrs),a.mix(!0,t._attrs,t.ATTRS)}}var r,a=n(4);r=function(t){i(this.constructor),this._attrs={},this.events={};var e=this.getDefaultCfg();a.mix(this._attrs,e,t)},a.augment(r,{getDefaultCfg:function(){var t=this,e=t.constructor,n=e._attrs,i=a.mix(!0,{},n);return i},set:function(t,e){var n="_onRender"+a.ucfirst(t);return this[n]&&this[n](e,this._attrs[t]),this._attrs[t]=e,this},get:function(t){return this._attrs[t]},on:function(t,e){var n=this,i=n.events,r=i[t];return r||(r=i[t]=[]),r.push(e),n},fire:function(t,e){var n=this,i=n.events,r=i[t];r&&a.each(r,function(t){t(e)})},off:function(t,e){var n=this,i=n.events,r=i[t];return t?(r&&a.remove(r,e),n):(n.events={},n)},destroy:function(){var t=this,e=t.destroyed;return e?t:(t._attrs={},t.events={},void(t.destroyed=!0))}}),t.exports=r},function(t,e){},[85,74,27,10],[86,10,27],[87,10],[89,10,26],[85,78,29,11],[86,11,29],[87,11],[89,11,28],function(t,e,n){var i=n(82);t.exports=i},function(t,e){"use strict";function n(t,e,i){i=i||0;for(var r in e)if(e.hasOwnProperty(r)){var o=e[r];null!==o&&s.isObject(o)?(s.isObject(t[r])||(t[r]={}),i=t[n-1])return t[n-1];for(var r=1;rt[n-1])return NaN;if(en?n:t},snapTo:function(t,e){var r=n(t,e),a=i(t,e);if(isNaN(r)||isNaN(a)){if(t[0]>=e)return t[0];var s=t[t.length-1];if(s<=e)return s}return Math.abs(e-r)=0;e--)delete t[e];t.length=0},equalsArray:function(t,e){if(t===e)return!0;if(!t||!e)return!1;if(t.length!==e.length)return!1;for(var n=!0,i=0;i');t.appendChild(n),this.set("canvasDOM",n)}},_setInitSize:function(){this.get("widthStyle")?this.changeSizeByCss(this.get("widthStyle"),this.get("heightStyle")):this.get("width")&&this.changeSize(this.get("width"),this.get("height"))},_getPx:function(t,e){var n=this.get("canvasDOM");n.style[t]=e;var i=a.getBoundingClientRect(n);return"width"===t?i.right-i.left:"height"===t?i.bottom-i.top:void 0},_reSize:function(){var t=this.get("canvasDOM"),e=this.get("widthCanvas"),n=this.get("heightCanvas"),i=this.get("widthStyle"),r=this.get("heightStyle");t.style.width=i,t.style.height=r,t.setAttribute("width",e),t.setAttribute("height",n)},getWidth:function(){var t=this.get("pixelRatio"),e=this.get("width");return e*t},getHeight:function(){var t=this.get("pixelRatio"),e=this.get("height");return e*t},changeSizeByCss:function(t,e){var n=this.get("pixelRatio"),t=this._getPx("width",t),e=this._getPx("height",e),i=t*n,r=e*n;this.set("widthStyle",t),this.set("heightStyle",e),this.set("widthCanvas",i),this.set("heightCanvas",r),this.set("width",t),this.set("height",e),this._reSize()},changeSize:function(t,e){var n=this.get("pixelRatio"),i=t*n,r=e*n;this.set("widthCanvas",i),this.set("heightCanvas",r),this.set("widthStyle",t+"px"),this.set("heightStyle",e+"px"),this.set("width",t),this.set("height",e),this._reSize()}}),t.exports=h},function(t,e,n,i){var r=n(i),a=(n(7),function(){});r.augment(a,{getParent:function(){return this.get("parent")||this.get("father")},getDefaultCfg:function(){r.initClassCfgs(this.constructor);var t=r.mix(!0,{},this.constructor.__cfg);return t},getBBBox:function(){var t=this.getBBox();return t?(t.x=t.minX,t.y=t.minY,t.width=t.maxX-t.minX,t.height=t.maxY-t.minY,t.centerX=t.x+t.width/2,t.centerY=t.y+t.height/2):t={x:0,y:0,centerX:0,centerY:0,width:0,height:0},t},move:function(t,e){var n=this,i=n.get("x")||0,r=n.get("y")||0;n.translate(t-i,e-r),n.set("x",t),n.set("y",e)}}),t.exports=a},function(t,e,n,i){"use strict";var r=n(i),a=n(7),s=a.Group,o=function(t){o.superclass.constructor.call(this,t),this._beforeRenderUI(),this._renderUI(),this._bindUI()};o.CFG={},r.extend(o,s),r.augment(o,{_beforeRenderUI:function(){this._initCfg(),this._multiRatioCfg()},_renderUI:function(){},_multiRatioCfg:function(){},_initCfg:function(){},_bindUI:function(){}}),t.exports=o},function(t,e,n,i,r){var a=n(i),s=n(7),o=n(r),c=function(){};a.augment(c,{addShape:function(t,e){var n,i=this.get("canvas");return e=a.mix({},e),e?(e.type=t,e.canvas=i,e.father=this,t=a.upperFirst(t),n=new s[t](e)):n=new s[t],this.add(n),n},addGroup:function(t,e){var n,i=this.get("canvas");if(e=a.mix({},e),a.isFunction(t))e?(e.canvas=i,e.father=this,n=new t(e)):n=new t({canvas:i,father:this}),this.add(n);else if(a.isObject(t))t.canvas=i,n=new o(t),this.add(n);else{if(void 0!==t)return!1;n=new o,this.add(n)}return n},findByCFG:function(t,e){var n=this.get("children"),i=[];return a.each(n,function(n,r){n.get(t)===e&&i.push(n)}),i}}),t.exports=c},function(t,e,n,i,r,a){t.exports={GMixin:n(i),GroupBase:n(r),GroupMixin:n(a)}}])); diff --git a/docs/themes/egg/source/images/logo.svg b/docs/themes/egg/source/images/logo.svg deleted file mode 100644 index d1147bf8af..0000000000 --- a/docs/themes/egg/source/images/logo.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - Logo - Created with Sketch Beta. - - - - - - - - - \ No newline at end of file diff --git a/examples/cookie/app/controller/forget.js b/examples/cookie/app/controller/forget.js deleted file mode 100644 index 6c956c1086..0000000000 --- a/examples/cookie/app/controller/forget.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.deleteCookie('remember'); - this.redirect('/'); -}; diff --git a/examples/cookie/app/controller/home.js b/examples/cookie/app/controller/home.js deleted file mode 100644 index 72c69eb43f..0000000000 --- a/examples/cookie/app/controller/home.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -module.exports = function* () { - if (this.getCookie('remember')) { - this.body = '

    Remembered :). Click to forget!.

    '; - return; - } - - this.body = `

    Check to - .

    `; -}; diff --git a/examples/cookie/app/controller/remember.js b/examples/cookie/app/controller/remember.js deleted file mode 100644 index 8dc4d72fb0..0000000000 --- a/examples/cookie/app/controller/remember.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = function* () { - const minute = 60000; - if (this.request.body.remember) this.setCookie('remember', 1, { maxAge: minute }); - this.redirect('/'); -}; diff --git a/examples/cookie/app/router.js b/examples/cookie/app/router.js deleted file mode 100644 index da1d0f5ebf..0000000000 --- a/examples/cookie/app/router.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = app => { - app.get('/', app.controller.home); - app.get('/forget', app.controller.forget); - app.post('/remember', app.controller.remember); -}; diff --git a/examples/cookie/config/config.default.js b/examples/cookie/config/config.default.js deleted file mode 100644 index 587c2c0e25..0000000000 --- a/examples/cookie/config/config.default.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -exports.keys = 'my cooo00ooooool keys'; -exports.security = { - csrf: false, - ctoken: false, -}; diff --git a/examples/cookie/dispatch.js b/examples/cookie/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/cookie/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/cookie/package.json b/examples/cookie/package.json deleted file mode 100644 index 44da01b371..0000000000 --- a/examples/cookie/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-cookie-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/cookie/test/index.test.js b/examples/cookie/test/index.test.js deleted file mode 100644 index de506087fd..0000000000 --- a/examples/cookie/test/index.test.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); - -describe('example cookie test', () => { - let app; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - return app.ready(); - }); - - after(() => app.close()); - - it('should GET / show "remember me" checkbox when cookie.remember not exists', () => { - return request(app.callback()) - .get('/') - .expect(200) - .expect(/ remember me<\/label>/); - }); - - it('should POST /remember to set cookie.remember = 1', () => { - return request(app.callback()) - .post('/remember') - .send({ - remember: 'true', - }) - .expect(302) - .expect('Location', '/') - .expect('Set-Cookie', /^remember=1; path=\/; expires=[^;]+; httponly,remember\.sig=[^;]+; path=\/; expires=[^;]+; httponly$/); - }); - - it('should GET /forget to delete cookie.remember', () => { - return request(app.callback()) - .get('/forget') - .expect(302) - .expect('Location', '/') - .expect('Set-Cookie', /^remember=; path=\/; expires=[^;]+; httponly$/); - }); -}); diff --git a/examples/cookie_session/app/controller/home.js b/examples/cookie_session/app/controller/home.js deleted file mode 100644 index 21f121446c..0000000000 --- a/examples/cookie_session/app/controller/home.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.session.count = (this.session.count || 0) + 1; - this.body = `${this.session.count} times, now: ${Date()}`; -}; diff --git a/examples/cookie_session/config/config.default.js b/examples/cookie_session/config/config.default.js deleted file mode 100644 index 587c2c0e25..0000000000 --- a/examples/cookie_session/config/config.default.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -exports.keys = 'my cooo00ooooool keys'; -exports.security = { - csrf: false, - ctoken: false, -}; diff --git a/examples/cookie_session/dispatch.js b/examples/cookie_session/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/cookie_session/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/cookie_session/package.json b/examples/cookie_session/package.json deleted file mode 100644 index 9f953eb814..0000000000 --- a/examples/cookie_session/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-cookie-session-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/cookie_session/test/index.test.js b/examples/cookie_session/test/index.test.js deleted file mode 100644 index 2f746e9ad2..0000000000 --- a/examples/cookie_session/test/index.test.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); - -describe('example cookie_session test', () => { - let app; - let cookie; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - return app.ready(); - }); - - after(() => app.close()); - - it('should GET / first time', () => { - return request(app.callback()) - .get('/') - .expect(200) - .expect(/^1 times/) - .expect('Set-Cookie', /^EGG_SESS=[^;]+; path=\/; expires=[^;]+; httponly$/) - .expect(res => { - cookie = res.headers['set-cookie'][0].split(';')[0]; - }); - }); - - it('should GET / second time', () => { - return request(app.callback()) - .get('/') - .set('Cookie', cookie) - .expect(200) - .expect(/^2 times/) - // session.count change - .expect('Set-Cookie', /^EGG_SESS=[^;]+; path=\/; expires=[^;]+; httponly$/); - }); -}); diff --git a/examples/helloworld/app/controller/foo.js b/examples/helloworld/app/controller/foo.js deleted file mode 100644 index 88d5dc5b76..0000000000 --- a/examples/helloworld/app/controller/foo.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.body = 'Hello foo'; -}; diff --git a/examples/helloworld/app/controller/home.js b/examples/helloworld/app/controller/home.js deleted file mode 100644 index a43a3eecde..0000000000 --- a/examples/helloworld/app/controller/home.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.body = 'Hello World'; -}; diff --git a/examples/helloworld/app/router.js b/examples/helloworld/app/router.js deleted file mode 100644 index 26be3d5686..0000000000 --- a/examples/helloworld/app/router.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = app => { - app.get('/', app.controller.home); - app.get('/foo', app.controller.foo); -}; diff --git a/examples/helloworld/config/config.default.js b/examples/helloworld/config/config.default.js deleted file mode 100644 index af619a436a..0000000000 --- a/examples/helloworld/config/config.default.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.keys = 'my secret keys'; diff --git a/examples/helloworld/dispatch.js b/examples/helloworld/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/helloworld/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/helloworld/package.json b/examples/helloworld/package.json deleted file mode 100644 index ecfde7227f..0000000000 --- a/examples/helloworld/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-helloworld", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/helloworld/test/index.test.js b/examples/helloworld/test/index.test.js deleted file mode 100644 index 92834ecf1b..0000000000 --- a/examples/helloworld/test/index.test.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); - -describe('example helloworld test', () => { - let app; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - return app.ready(); - }); - - after(() => app.close()); - - it('should GET / 200', () => { - return request(app.callback()) - .get('/') - .expect(200) - .expect('Hello World'); - }); - - it('should GET /foo', () => { - return request(app.callback()) - .get('/foo') - .expect(200) - .expect('Hello foo'); - }); -}); diff --git a/examples/multipart/.gitignore b/examples/multipart/.gitignore deleted file mode 100644 index 4939210ed6..0000000000 --- a/examples/multipart/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -upload_dirs -!upload_dirs/keepit diff --git a/examples/multipart/app/controller/home.js b/examples/multipart/app/controller/home.js deleted file mode 100644 index 2456d5df0e..0000000000 --- a/examples/multipart/app/controller/home.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.body = `
    -

    Title:

    -

    Image:

    -

    -
    `; -}; diff --git a/examples/multipart/app/controller/upload.js b/examples/multipart/app/controller/upload.js deleted file mode 100644 index 1cb96e260e..0000000000 --- a/examples/multipart/app/controller/upload.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const sendToWormhole = require('stream-wormhole'); - -module.exports = function* () { - const stream = yield this.getFileStream(); - let filepath = path.join(this.app.config.baseDir, `logs/${stream.filename}`); - if (stream.fields.title === 'mock-error') { - filepath = path.join(this.app.config.baseDir, `logs/not-exists/dir/${stream.filename}`); - } else if (stream.fields.title === 'mock-read-error') { - filepath = path.join(this.app.config.baseDir, `logs/read-error-${stream.filename}`); - } - this.logger.warn('Saving %s to %s', stream.filename, filepath); - try { - yield saveStream(stream, filepath); - } catch (err) { - yield sendToWormhole(stream); - throw err; - } - - this.body = { - file: stream.filename, - fields: stream.fields, - }; -}; - -function saveStream(stream, filepath) { - return new Promise((resolve, reject) => { - if (filepath.indexOf('/read-error-') > 0) { - stream.once('readable', () => { - const buf = stream.read(10240); - console.log('read %d bytes', buf.length); - setTimeout(() => { - reject(new Error('mock read error')); - }, 1000); - }); - } else { - const ws = fs.createWriteStream(filepath); - stream.pipe(ws); - ws.on('error', reject); - ws.on('finish', resolve); - } - }); -} diff --git a/examples/multipart/app/router.js b/examples/multipart/app/router.js deleted file mode 100644 index 51342020cc..0000000000 --- a/examples/multipart/app/router.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = app => { - app.get('/', app.controller.home); - app.post('/upload', app.controller.upload); -}; diff --git a/examples/multipart/config/config.default.js b/examples/multipart/config/config.default.js deleted file mode 100644 index 6c4b8d15f5..0000000000 --- a/examples/multipart/config/config.default.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.keys = 'my keys'; diff --git a/examples/multipart/dispatch.js b/examples/multipart/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/multipart/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/multipart/package.json b/examples/multipart/package.json deleted file mode 100644 index c8ef6f7e5f..0000000000 --- a/examples/multipart/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-multipart-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/multipart/test/index.test.js b/examples/multipart/test/index.test.js deleted file mode 100644 index eb2a214b87..0000000000 --- a/examples/multipart/test/index.test.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); -const formstream = require('formstream'); -const urllib = require('urllib'); - -describe.skip('example multipart test', () => { - let app; - let csrfToken; - let cookies; - let host; - let server; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - server = app.listen(); - }); - - after(() => app.close()); - - it('should GET / show upload form', () => { - return request(server) - .get('/') - .expect(200) - .expect(/

    Image: <\/p>/) - .expect(res => { - console.log(res.headers, res.text); - csrfToken = res.headers['x-csrf']; - cookies = res.headers['set-cookie'].join(';'); - host = `http://127.0.0.1:${server.address().port}`; - }); - }); - - it('should POST /upload success', done => { - const form = formstream(); - form.file('file', __filename); - // other form fields - form.field('title', 'fengmk2 test title') - .field('love', 'egg'); - - const headers = form.headers(); - headers.Cookie = cookies; - urllib.request(`${host}/upload?_csrf=${csrfToken}`, { - method: 'POST', - headers, - stream: form, - dataType: 'json', - }, (err, data, res) => { - assert(!err, err && err.message); - assert.equal(res.statusCode, 200); - console.log(data); - done(); - }); - }); -}); diff --git a/examples/restful_api/app/apis/user.js b/examples/restful_api/app/apis/user.js deleted file mode 100644 index 046c592a7b..0000000000 --- a/examples/restful_api/app/apis/user.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -const users = [ - { - name: 'fengmk2', - url: 'https://fengmk2.com', - projects: [ - 'urllib', - 'egg', - ], - createdAt: new Date(), - modifiedAt: new Date(), - }, - { - name: 'dead-horse', - url: 'http://deadhorse.me', - projects: [ - 'koa', - 'egg', - ], - createdAt: new Date(), - modifiedAt: new Date(), - }, -]; - -// GET /api/users -exports.index = function* () { - this.meta = { - total: Object.keys(users).length, - }; - this.data = users; -}; - -// GET /api/users/:id -exports.show = function* () { - const user = users.find(user => user.name === this.params.id); - this.data = user; -}; - -// POST /api/users -exports.create = function* () { - const user = this.params.data; - if (!user.name) { - this.throw(400, 'missing name field'); - } - if (users.find(user => user.name === this.params.id)) { - this.throw(400, `${user.name} exists`); - } - - user.modifiedAt = user.createdAt = new Date(); - users.push(user); - this.data = this.params.data; -}; - -// PUT /api/users/:id -exports.update = function* () { - const user = this.params.data; - if (!user.name) { - this.throw(400, 'missing name field'); - } - const existsUser = users.find(user => user.name === this.params.id); - if (!existsUser) { - this.throw(400, `${user.name} not exists`); - } - - Object.assign(existsUser, user); - existsUser.modifiedAt = new Date(); - this.data = existsUser; -}; - -// DELETE /api/users/:id -exports.delete = function* () { - const name = this.params.id; - const index = users.findIndex(user => user.name === name); - if (index === -1) { - this.throw(400, `${name} not exists`); - } - - users.splice(index, 1); -}; diff --git a/examples/restful_api/config/config.default.js b/examples/restful_api/config/config.default.js deleted file mode 100644 index 8cc3eea162..0000000000 --- a/examples/restful_api/config/config.default.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -exports.rest = { - urlprefix: '/api/', -}; diff --git a/examples/restful_api/config/plugin.js b/examples/restful_api/config/plugin.js deleted file mode 100644 index cda84be894..0000000000 --- a/examples/restful_api/config/plugin.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.rest = true; diff --git a/examples/restful_api/dispatch.js b/examples/restful_api/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/restful_api/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/restful_api/package.json b/examples/restful_api/package.json deleted file mode 100644 index ca9ed31b5e..0000000000 --- a/examples/restful_api/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-restful-api-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/schedule/app/schedule/all_cron.js b/examples/schedule/app/schedule/all_cron.js deleted file mode 100644 index 6762773c4e..0000000000 --- a/examples/schedule/app/schedule/all_cron.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -module.exports = function() { - const exports = {}; - - const now = new Date(); - const start = (now.getSeconds() + 3) % 60; - exports.schedule = { - cron: `${start}/30 * * * * *`, - type: 'all', - }; - - exports.task = function* (ctx) { - ctx.logger.info('all&&cron'); - }; - return exports; -}; diff --git a/examples/schedule/app/schedule/all_interval.js b/examples/schedule/app/schedule/all_interval.js deleted file mode 100644 index c2f1a74e6b..0000000000 --- a/examples/schedule/app/schedule/all_interval.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -exports.schedule = { - interval: 3000, - type: 'all', -}; - -exports.task = function* (ctx) { - ctx.logger.info('all&&interval'); -}; diff --git a/examples/schedule/app/schedule/worker_cron.js b/examples/schedule/app/schedule/worker_cron.js deleted file mode 100644 index 0169bbfbf6..0000000000 --- a/examples/schedule/app/schedule/worker_cron.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -module.exports = function() { - const exports = {}; - - const now = new Date(); - const start = (now.getSeconds() + 3) % 60; - exports.schedule = { - cron: `${start}/30 * * * * *`, - type: 'worker', - }; - - exports.task = function* (ctx) { - ctx.logger.info('worker&&cron'); - }; - return exports; -}; diff --git a/examples/schedule/app/schedule/worker_interval.js b/examples/schedule/app/schedule/worker_interval.js deleted file mode 100644 index b6a2e0d364..0000000000 --- a/examples/schedule/app/schedule/worker_interval.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -exports.schedule = { - interval: 3000, - type: 'worker', -}; - -exports.task = function* (ctx) { - ctx.logger.info('worker&&interval'); -}; diff --git a/examples/schedule/dispatch.js b/examples/schedule/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/schedule/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/schedule/package.json b/examples/schedule/package.json deleted file mode 100644 index 7659640254..0000000000 --- a/examples/schedule/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-schedule-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/schedule/test/index.test.js b/examples/schedule/test/index.test.js deleted file mode 100644 index 1aa1121468..0000000000 --- a/examples/schedule/test/index.test.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const mm = require('egg-mock'); - -describe.skip('egg schedule example', () => { - it('should schedule run schedule success', function* () { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - - const app = mm.cluster({ - baseDir, - customEgg, - workers: 2, - }); - - yield app.ready(); - yield sleep(3000); - const log = fs.readFileSync(path.join(baseDir, 'logs/egg-schedule-demo/egg-schedule-demo-web.log'), 'utf8'); - console.log(log); - countLine(log, 'all&&cron').should.equal(2); - countLine(log, 'all&&interval').should.equal(2); - countLine(log, 'worker&&cron').should.equal(1); - countLine(log, 'worker&&interval').should.equal(1); - - app.close(); - }); -}); - -function sleep(time) { - return new Promise(resolve => setTimeout(resolve, time)); -} - -function countLine(content, key) { - return content.split('\n').filter(line => line.indexOf(key) >= 0).length; -} diff --git a/examples/start.js b/examples/start.js deleted file mode 100644 index 01c698e7a8..0000000000 --- a/examples/start.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const path = require('path'); -const egg = require('..'); - -const name = process.argv[2]; -console.log('Starting %s', name); - -egg.startCluster({ - baseDir: path.join(__dirname, name), -}); diff --git a/examples/static/app/controller/home.js b/examples/static/app/controller/home.js deleted file mode 100644 index 7684234502..0000000000 --- a/examples/static/app/controller/home.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = function* () { - this.body = `

    `; -}; diff --git a/examples/static/app/public/foo.js b/examples/static/app/public/foo.js deleted file mode 100644 index e7682bbdec..0000000000 --- a/examples/static/app/public/foo.js +++ /dev/null @@ -1 +0,0 @@ -alert('hi egg'); diff --git a/examples/static/app/public/hi.txt b/examples/static/app/public/hi.txt deleted file mode 100644 index 3b38ffbb78..0000000000 --- a/examples/static/app/public/hi.txt +++ /dev/null @@ -1,2 +0,0 @@ -hi egg. -你好,蛋蛋。 diff --git "a/examples/static/app/public/\350\233\213\350\233\213Web\346\241\206\346\236\266.txt" "b/examples/static/app/public/\350\233\213\350\233\213Web\346\241\206\346\236\266.txt" deleted file mode 100644 index 2ed6b1420b..0000000000 --- "a/examples/static/app/public/\350\233\213\350\233\213Web\346\241\206\346\236\266.txt" +++ /dev/null @@ -1,2 +0,0 @@ -test only. -测试专用。 diff --git a/examples/static/config/plugin.js b/examples/static/config/plugin.js deleted file mode 100644 index 2c02313f64..0000000000 --- a/examples/static/config/plugin.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.static = true; diff --git a/examples/static/dispatch.js b/examples/static/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/static/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/static/package.json b/examples/static/package.json deleted file mode 100644 index b6dfc94133..0000000000 --- a/examples/static/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-static-demo", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/static/test/index.test.js b/examples/static/test/index.test.js deleted file mode 100644 index f1167c4a65..0000000000 --- a/examples/static/test/index.test.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); - -describe('example static test', () => { - let app; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - return app.ready(); - }); - - after(() => app.close()); - - it('should GET / 200', () => { - return request(app.callback()) - .get('/') - .expect(200) - .expect(/
  • Download hi\.txt<\/a>\.<\/li>/); - }); - - it('should GET /public/hi.txt', () => { - return request(app.callback()) - .get('/public/hi.txt') - .expect(200) - .expect('hi egg.\n你好,蛋蛋。\n'); - }); -}); diff --git a/examples/view_nunjucks/app/controller/home.js b/examples/view_nunjucks/app/controller/home.js deleted file mode 100644 index 270f09b59a..0000000000 --- a/examples/view_nunjucks/app/controller/home.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = function* () { - yield this.render('home.html', { - user: { - name: 'foobar', - }, - title: 'egg view example', - }); -}; diff --git a/examples/view_nunjucks/app/public/css/home.css b/examples/view_nunjucks/app/public/css/home.css deleted file mode 100644 index 208d16d421..0000000000 --- a/examples/view_nunjucks/app/public/css/home.css +++ /dev/null @@ -1 +0,0 @@ -body {} diff --git a/examples/view_nunjucks/app/public/css/layout.css b/examples/view_nunjucks/app/public/css/layout.css deleted file mode 100644 index 208d16d421..0000000000 --- a/examples/view_nunjucks/app/public/css/layout.css +++ /dev/null @@ -1 +0,0 @@ -body {} diff --git a/examples/view_nunjucks/app/public/js/home.js b/examples/view_nunjucks/app/public/js/home.js deleted file mode 100644 index 3b1e38f9a4..0000000000 --- a/examples/view_nunjucks/app/public/js/home.js +++ /dev/null @@ -1 +0,0 @@ -console.log('hello egg view'); diff --git a/examples/view_nunjucks/app/view/component/nav.html b/examples/view_nunjucks/app/view/component/nav.html deleted file mode 100644 index c40a2d8c92..0000000000 --- a/examples/view_nunjucks/app/view/component/nav.html +++ /dev/null @@ -1,33 +0,0 @@ - - diff --git a/examples/view_nunjucks/app/view/home.html b/examples/view_nunjucks/app/view/home.html deleted file mode 100644 index 862b49de3e..0000000000 --- a/examples/view_nunjucks/app/view/home.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} - -
    -

    egg view example here, welcome {{ user.name }}

    -
    - -{% endblock %} diff --git a/examples/view_nunjucks/app/view/layout.html b/examples/view_nunjucks/app/view/layout.html deleted file mode 100644 index 821c565300..0000000000 --- a/examples/view_nunjucks/app/view/layout.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - {{title}} - - - - - - - - {% block nav %}{% include "component/nav.html" %}{% endblock %} - -
    - {% block body %}{% endblock %} -
    - -
    -
    -

    Maintained by the eggjs team.

    - -
    -
    - - - - - diff --git a/examples/view_nunjucks/config/config.default.js b/examples/view_nunjucks/config/config.default.js deleted file mode 100644 index 4b27c20fbd..0000000000 --- a/examples/view_nunjucks/config/config.default.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -exports.keys = 'my secret keys'; - -exports.view = { - // dir: 'path/to/template/dir', // default to `{app_root}/app/view` - cache: true, // local env is false -}; diff --git a/examples/view_nunjucks/config/plugin.js b/examples/view_nunjucks/config/plugin.js deleted file mode 100644 index d41f56e103..0000000000 --- a/examples/view_nunjucks/config/plugin.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -exports.view = { - enable: true, - package: 'egg-view-nunjucks', -}; diff --git a/examples/view_nunjucks/dispatch.js b/examples/view_nunjucks/dispatch.js deleted file mode 100644 index e9353d7351..0000000000 --- a/examples/view_nunjucks/dispatch.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const egg = require('../..'); - -egg.startCluster({ - baseDir: __dirname, -}); diff --git a/examples/view_nunjucks/package.json b/examples/view_nunjucks/package.json deleted file mode 100644 index 64ebf89d2f..0000000000 --- a/examples/view_nunjucks/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "egg-view-nunjucks", - "main": "dispatch.js", - "scripts": { - "dev": "egg-bin dev", - "start": "node dispatch.js" - } -} diff --git a/examples/view_nunjucks/test/index.test.js b/examples/view_nunjucks/test/index.test.js deleted file mode 100644 index 3a4b3cf8f3..0000000000 --- a/examples/view_nunjucks/test/index.test.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const path = require('path'); -const request = require('supertest'); -const mm = require('egg-mock'); - -describe('example view-nunjucks test', () => { - let app; - - before(() => { - const baseDir = path.dirname(__dirname); - const customEgg = path.join(baseDir, '../..'); - app = mm.app({ - baseDir, - customEgg, - }); - return app.ready(); - }); - - after(() => app.close()); - - it('should GET / 200', () => { - return request(app.callback()) - .get('/') - .expect(200) - .expect(/

    egg view example here, welcome foobar<\/h2>/) - .expect(/egg view example<\/title>/); - }); -}); diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..760f505e7a --- /dev/null +++ b/index.d.ts @@ -0,0 +1,1290 @@ +import accepts = require('accepts'); +import { AsyncLocalStorage } from 'async_hooks'; +import { EventEmitter } from 'events' +import { Readable } from 'stream'; +import { Socket } from 'net'; +import { IncomingMessage, ServerResponse } from 'http'; +import KoaApplication = require('koa'); +import KoaRouter = require('koa-router'); +import { + EggLogger as Logger, + EggLoggers, + LoggerLevel as EggLoggerLevel, + EggLoggersOptions, + EggLoggerOptions, + EggContextLogger, +} from 'egg-logger'; +import { + RequestOptions2 as RequestOptionsOld, + HttpClientResponse as HttpClientResponseOld, +} from 'urllib'; +import { + RequestURL, + RequestOptions, + HttpClientResponse as HttpClientResponseNext, +} from 'urllib-next'; +import { + EggCoreBase, + FileLoaderOption, + EggLoader as CoreLoader, + EggCoreOptions as CoreOptions, + EggLoaderOptions as CoreLoaderOptions, + BaseContextClass as CoreBaseContextClass, +} from 'egg-core'; +import EggCookies = require('egg-cookies'); +import 'egg-onerror'; +import 'egg-session'; +import 'egg-i18n'; +import 'egg-watcher'; +import 'egg-multipart'; +import 'egg-security'; +import 'egg-development'; +import 'egg-logrotator'; +import 'egg-schedule'; +import 'egg-static'; +import 'egg-jsonp'; +import 'egg-view'; + +declare module 'egg' { + export type EggLogger = Logger; + // plain object + type PlainObject<T = any> = { [key: string]: T }; + + // Remove specific property from the specific class + type RemoveSpecProp<T, P> = Pick<T, Exclude<keyof T, P>>; + + // Usage: + // ```ts + // import { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse } from 'egg'; + // async function request(url: HttpClientRequestURL, options: HttpClientRequestOptions): Promise<HttpClientResponse> { + // return await app.httpclient.request(url, options); + // } + // ``` + export type HttpClientRequestURL = RequestURL; + export type HttpClientRequestOptions = RequestOptions; + export type HttpClientResponse<T = any> = HttpClientResponseNext<T>; + // Compatible with both urllib@2 and urllib@3 RequestOptions to request + export interface EggHttpClient extends EventEmitter { + request<T = any>(url: HttpClientRequestURL): Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + request<T = any>(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): + Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + curl<T = any>(url: HttpClientRequestURL): Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + curl<T = any>(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): + Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + safeCurl<T = any>(url: HttpClientRequestURL): Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + safeCurl<T = any>(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): + Promise<HttpClientResponseOld<T> | HttpClientResponse<T>>; + } + + interface EggHttpConstructor { + new(app: Application): EggHttpClient; + } + + export interface EggContextHttpClient extends EggHttpClient { } + interface EggContextHttpClientConstructor { + new(ctx: Context): EggContextHttpClient; + } + + /** + * BaseContextClass is a base class that can be extended, + * it's instantiated in context level, + * {@link Helper}, {@link Service} is extending it. + */ + export class BaseContextClass extends CoreBaseContextClass<Context, Application, EggAppConfig, IService> { // tslint:disable-line + /** + * logger + */ + protected logger: EggLogger; + } + + export class Boot { + /** + * logger + * @member {EggLogger} + */ + protected logger: EggLogger; + + /** + * The configuration of application + * @member {EggAppConfig} + */ + protected config: EggAppConfig; + + /** + * The instance of agent + * @member {Agent} + */ + protected agent: Agent; + + /** + * The instance of app + * @member {Application} + */ + protected app: Application; + } + + export type RequestArrayBody = any[]; + export type RequestObjectBody = PlainObject; + export interface Request extends KoaApplication.Request { // tslint:disable-line + /** + * detect if response should be json + * 1. url path ends with `.json` + * 2. response type is set to json + * 3. detect by request accept header + * + * @member {Boolean} Request#acceptJSON + * @since 1.0.0 + */ + acceptJSON: boolean; + + /** + * Request remote IPv4 address + * @member {String} Request#ip + * @example + * ```js + * this.request.ip + * => '127.0.0.1' + * => '111.10.2.1' + * ``` + */ + ip: string; + + /** + * Get all pass through ip addresses from the request. + * Enable only on `app.config.proxy = true` + * + * @member {Array} Request#ips + * @example + * ```js + * this.request.ips + * => ['100.23.1.2', '201.10.10.2'] + * ``` + */ + ips: string[]; + + protocol: string; + + /** + * get params pass by querystring, all value are Array type. {@link Request#query} + * @member {Array} Request#queries + * @example + * ```js + * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val + * this.queries + * => + * { + * "a": ["b", "c"], + * "o[foo]": ["bar"], + * "b[]": ["1", "2"], + * "e": ["val"] + * } + * ``` + */ + queries: PlainObject<string[]>; + + /** + * get params pass by querystring, all value are String type. + * @member {Object} Request#query + * @example + * ```js + * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 + * this.query + * => { 'name': 'Foo', 'age': 20 } + * + * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val + * this.query + * => + * { + * "a": "b", + * "o[foo]": "bar", + * "b[]": "1", + * "e": "val" + * } + * ``` + */ + query: PlainObject<string>; + + body: any; + } + + export interface Response<ResponseBodyT = any> extends KoaApplication.Response { // tslint:disable-line + /** + * read response real status code. + * + * e.g.: Using 302 status redirect to the global error page + * instead of show current 500 status page. + * And access log should save 500 not 302, + * then the `realStatus` can help us find out the real status code. + * @member {Number} Context#realStatus + */ + realStatus: number; + body: ResponseBodyT; + } + + export type LoggerLevel = EggLoggerLevel; + + + /** + * egg app info + * @example + * ```js + * // config/config.default.ts + * import { EggAppInfo } from 'egg'; + * + * export default (appInfo: EggAppInfo) => { + * return { + * keys: appInfo.name + '123456', + * }; + * } + * ``` + */ + export interface EggAppInfo { + pkg: any; // package.json + name: string; // the application name from package.json + baseDir: string; // current directory of application + env: EggEnvType; // equals to serverEnv + HOME: string; // home directory of the OS + root: string; // baseDir when local and unittest, HOME when other environment + } + + type IgnoreItem = string | RegExp | ((ctx: Context) => boolean); + type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; + + /** logger config of egg */ + export interface EggLoggerConfig extends RemoveSpecProp<EggLoggersOptions, 'type'> { + /** custom config of coreLogger */ + coreLogger?: Partial<EggLoggerOptions>; + /** allow debug log at prod, defaults to `false` */ + allowDebugAtProd?: boolean; + /** disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. */ + disableConsoleAfterReady?: boolean; + /** using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to `false`. e.g.: logger will set 1.456ms instead of 1ms. */ + enablePerformanceTimer?: boolean; + /** using the app logger instead of EggContextLogger, defaults to `false` */ + enableFastContextLogger?: boolean; + } + + /** Custom Loader Configuration */ + export interface CustomLoaderConfig extends RemoveSpecProp<FileLoaderOption, 'inject' | 'target'> { + /** + * an object you wanner load to, value can only be 'ctx' or 'app'. default to app + */ + inject?: 'ctx' | 'app'; + /** + * whether need to load files in plugins or framework, default to false + */ + loadunit?: boolean; + } + + export interface HttpClientBaseConfig { + /** Whether use http keepalive */ + keepAlive?: boolean; + /** Free socket after keepalive timeout */ + freeSocketKeepAliveTimeout?: number; + /** Free socket after request timeout */ + freeSocketTimeout?: number; + /** Request timeout */ + timeout?: number; + /** Determines how many concurrent sockets the agent can have open per origin */ + maxSockets?: number; + /** The maximum number of sockets that will be left open in the free state */ + maxFreeSockets?: number; + } + + /** HttpClient config */ + export interface HttpClientConfig extends HttpClientBaseConfig { + /** http.Agent */ + httpAgent?: HttpClientBaseConfig; + /** https.Agent */ + httpsAgent?: HttpClientBaseConfig; + /** Default request args for httpclient */ + request?: HttpClientRequestOptions | RequestOptionsOld; + /** Whether enable dns cache */ + enableDNSCache?: boolean; + /** Enable proxy request. Default is `false`. */ + enableProxy?: boolean; + /** proxy agent uri or options. Default is `null`. */ + proxy?: string | { [key: string]: any }; + /** DNS cache lookup interval */ + dnsCacheLookupInterval?: number; + /** DNS cache max age */ + dnsCacheMaxLength?: number; + /** use urllib@3 HttpClient. Default is `false` */ + useHttpClientNext?: boolean; + /** Allow to use HTTP2 first, only work on `useHttpClientNext = true`. Default is `false` */ + allowH2?: boolean; + } + + export interface EggAppConfig { + workerStartTimeout: number; + baseDir: string; + middleware: string[]; + + /** + * The option of `bodyParser` middleware + * + * @member Config#bodyParser + * @property {Boolean} enable - enable bodyParser or not, default to true + * @property {String | RegExp | Function | Array} ignore - won't parse request body when url path hit ignore pattern, can not set `ignore` when `match` presented + * @property {String | RegExp | Function | Array} match - will parse request body only when url path hit match pattern + * @property {String} encoding - body encoding config, default utf8 + * @property {String} formLimit - form body size limit, default 1mb + * @property {String} jsonLimit - json body size limit, default 1mb + * @property {String} textLimit - json body size limit, default 1mb + * @property {Boolean} strict - json body strict mode, if set strict value true, then only receive object and array json body + * @property {Number} queryString.arrayLimit - from item array length limit, default 100 + * @property {Number} queryString.depth - json value deep length, default 5 + * @property {Number} queryString.parameterLimit - parameter number limit, default 1000 + * @property {String[]} enableTypes - parser will only parse when request type hits enableTypes, default is ['json', 'form'] + * @property {Object} extendTypes - support extend types + * @property {String} onProtoPoisoning - Defines what action must take when parsing a JSON object with `__proto__`. Possible values are `'error'`, `'remove'` and `'ignore'`. Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. + */ + bodyParser: { + enable: boolean; + encoding: string; + formLimit: string; + jsonLimit: string; + textLimit: string; + strict: boolean; + queryString: { + arrayLimit: number; + depth: number; + parameterLimit: number; + }; + ignore: IgnoreOrMatch; + match: IgnoreOrMatch; + enableTypes: string[]; + extendTypes: { + json: string[]; + form: string[]; + text: string[]; + }; + /** Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. */ + onProtoPoisoning: 'error' | 'remove' | 'ignore'; + }; + + /** + * logger options + * @member Config#logger + * @property {String} dir - directory of log files + * @property {String} encoding - log file encloding, defaults to utf8 + * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production + * @property {String} consoleLevel - log level of stdout, defaults to INFO in local serverEnv, defaults to WARN in unittest, defaults to NONE elsewise + * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. + * @property {Boolean} outputJSON - log as JSON or not, defaults to false + * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true + * @property {String} errorLogName - file name of errorLogger + * @property {String} coreLogName - file name of coreLogger + * @property {String} agentLogName - file name of agent worker log + * @property {Object} coreLogger - custom config of coreLogger + * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false + * @property {Boolean} enablePerformanceTimer - using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to false. e.g.: logger will set 1.456ms instead of 1ms. + * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false + */ + logger: EggLoggerConfig; + + /** custom logger of egg */ + customLogger: { + [key: string]: EggLoggerOptions; + }; + + /** Configuration of httpclient in egg. */ + httpclient: HttpClientConfig; + + development: { + /** + * dirs needed watch, when files under these change, application will reload, use relative path + */ + watchDirs: string[]; + /** + * dirs don't need watch, including subdirectories, use relative path + */ + ignoreDirs: string[]; + /** + * don't wait all plugins ready, default is true. + */ + fastReady: boolean; + /** + * whether reload on debug, default is true. + */ + reloadOnDebug: boolean; + /** + * whether override default watchDirs, default is false. + */ + overrideDefault: boolean; + /** + * whether override default ignoreDirs, default is false. + */ + overrideIgnore: boolean; + /** + * whether to reload, use https://github.com/sindresorhus/multimatch + */ + reloadPattern: string[] | string; + }; + + /** + * customLoader config + */ + customLoader: { + [key: string]: CustomLoaderConfig; + }; + + /** + * It will ignore special keys when dumpConfig + */ + dump: { + ignore: Set<string>; + }; + + /** + * The environment of egg + */ + env: EggEnvType; + + /** + * The current HOME directory + */ + HOME: string; + + hostHeaders: string; + + /** + * I18n options + */ + i18n: { + /** + * default value EN_US + */ + defaultLocale: string; + /** + * i18n resource file dir, not recommend to change default value + */ + dirs: string[]; + /** + * custom the locale value field, default `query.locale`, you can modify this config, such as `query.lang` + */ + queryField: string; + /** + * The locale value key in the cookie, default is locale. + */ + cookieField: string; + /** + * Locale cookie expire time, default `1y`, If pass number value, the unit will be ms + */ + cookieMaxAge: string | number; + }; + + /** + * Detect request' ip from specified headers, not case-sensitive. Only worked when config.proxy set to true. + */ + ipHeaders: string; + + /** + * jsonp options + * @member Config#jsonp + * @property {String} callback - jsonp callback method key, default to `_callback` + * @property {Number} limit - callback method name's max length, default to `50` + * @property {Boolean} csrf - enable csrf check or not. default to false + * @property {String|RegExp|Array} whiteList - referrer white list + */ + jsonp: { + limit: number; + callback: string; + csrf: boolean; + whiteList: string | RegExp | Array<string | RegExp>; + }; + + /** + * The key that signing cookies. It can contain multiple keys seperated by . + */ + keys: string; + + /** + * The name of the application + */ + name: string; + + /** + * package.json + */ + pkg: any; + + rundir: string; + + security: { + domainWhiteList: string[]; + protocolWhiteList: string[]; + defaultMiddleware: string; + csrf: any; + ssrf: { + ipBlackList: string[]; + ipExceptionList: string[]; + checkAddress?(ip: string): boolean; + }; + xframe: { + enable: boolean; + value: 'SAMEORIGIN' | 'DENY' | string; + }; + hsts: any; + methodnoallow: { enable: boolean }; + noopen: { enable: boolean; } + xssProtection: any; + csp: any; + }; + + siteFile: PlainObject<string | Buffer>; + + watcher: PlainObject; + + onClientError(err: Error, socket: Socket, app: EggApplication): ClientErrorResponse | Promise<ClientErrorResponse>; + + /** + * server timeout in milliseconds, default to 0 (no timeout). + * + * for special request, just use `ctx.req.setTimeout(ms)` + * + * @see https://nodejs.org/api/http.html#http_server_timeout + */ + serverTimeout: number | null; + + [prop: string]: any; + } + + export interface ClientErrorResponse { + body: string | Buffer; + status: number; + headers: { [key: string]: string }; + } + + export interface Router extends Omit<KoaRouter<any, Context>, 'url'> { + /** + * restful router api + */ + resources(name: string, prefix: string, ...middleware: any[]): Router; + + /** + * @param {String} name - Router name + * @param {Object} [params] - more parameters + * @example + * ```js + * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) + * => /posts/1/edit?name=foo&page=2 + * router.url('posts', { name: 'foo&1', page: 2 }) + * => /posts?name=foo%261&page=2 + * ``` + * @return {String} url by path name and query params. + * @since 1.0.0 + */ + url(name: string, params?: any): string; + /** + * Alias for the url method + */ + pathFor(name: string, params?: any): string; + methods: string[]; + } + + export interface EggApplication extends Omit<EggCoreBase<EggAppConfig>, 'ctxStorage' | 'currentContext'> { + /** + * HttpClient instance + */ + httpclient: EggHttpClient; + + /** + * Logger for Application, wrapping app.coreLogger with context infomation + * + * @member {ContextLogger} Context#logger + * @since 1.0.0 + * @example + * ```js + * this.logger.info('some request data: %j', this.request.body); + * this.logger.warn('WARNING!!!!'); + * ``` + */ + logger: EggLogger; + + /** + * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web + */ + coreLogger: EggLogger; + + /** + * All loggers contain logger, coreLogger and customLogger + */ + loggers: EggLoggers; + + /** + * messenger instance + */ + messenger: Messenger; + + /** + * get router + */ + router: Router; + + /** + * create a singleton instance + */ + addSingleton(name: string, create: any): void; + + runSchedule(schedulePath: string, ...args: any[]): Promise<any>; + + /** + * http request helper base on httpclient, it will auto save httpclient log. + * Keep the same api with httpclient.request(url, args). + * See https://github.com/node-modules/urllib#api-doc for more details. + */ + curl: EggHttpClient['request']; + + /** + * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical + */ + getLogger(name: string): EggLogger; + + /** + * print the infomation when console.log(app) + */ + inspect(): any; + + /** + * Alias to Router#url + */ + url(name: string, params: any): any; + + /** + * Create an anonymous context, the context isn't request level, so the request is mocked. + * then you can use context level API like `ctx.service` + * @member {String} EggApplication#createAnonymousContext + * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. + * @return {Context} context + */ + createAnonymousContext(req?: Request): Context; + + /** + * export context base classes, let framework can impl sub class and over context extend easily. + */ + ContextCookies: typeof EggCookies; + ContextLogger: typeof EggContextLogger; + ContextHttpClient: EggContextHttpClientConstructor; + HttpClient: EggHttpConstructor; + Subscription: typeof Subscription; + Controller: typeof Controller; + Service: typeof Service; + } + + // compatible + export class EggApplication { + constructor(options?: CoreOptions); + } + + export type RouterPath = string | RegExp; + + export class Application extends EggApplication { + /** + * global locals for view + * @see Context#locals + */ + locals: IApplicationLocals; + + /** + * HTTP get method + */ + get(path: RouterPath, fn: string): void; + get(path: RouterPath, ...middleware: any[]): void; + + /** + * HTTP post method + */ + post(path: RouterPath, fn: string): void; + post(path: RouterPath, ...middleware: any[]): void; + + /** + * HTTP put method + */ + put(path: RouterPath, fn: string): void; + put(path: RouterPath, ...middleware: any[]): void; + + /** + * HTTP patch method + */ + patch(path: RouterPath, fn: string): void; + patch(path: RouterPath, ...middleware: any[]): void; + + /** + * HTTP delete method + */ + delete(path: RouterPath, fn: string): void; + delete(path: RouterPath, ...middleware: any[]): void; + + /** + * restful router api + */ + resources(name: string, prefix: string, fn: string): Router; + resources(path: string, prefix: string, ...middleware: any[]): Router; + + redirect(path: string, redirectPath: string): void; + + controller: IController; + + middleware: KoaApplication.Middleware[] & IMiddleware; + + /** + * Run async function in the background + * @see Context#runInBackground + * @param {Function} scope - the first args is an anonymous ctx + */ + runInBackground(scope: (ctx: Context) => void): void; + + /** + * Run async function in the anonymous context scope + * @see Context#runInAnonymousContextScope + * @param {Function} scope - the first args is an anonymous ctx, scope should be async function + * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. + */ + runInAnonymousContextScope<R>(scope: (ctx: Context) => Promise<R>, req?: Request): Promise<R>; + + /** + * Get current execute ctx async local storage + * @returns {AsyncLocalStorage} localStorage - store current execute Context + */ + get ctxStorage(): AsyncLocalStorage<Context>; + + /** + * Get current execute ctx, maybe undefined + * @returns {Context} ctx - current execute Context + */ + get currentContext(): Context; + } + + export interface IApplicationLocals extends PlainObject { } + + export interface FileStream extends Readable { // tslint:disable-line + fields: any; + + filename: string; + + fieldname: string; + + mime: string; + + mimeType: string; + + transferEncoding: string; + + encoding: string; + + truncated: boolean; + } + + interface GetFileStreamOptions { + requireFile?: boolean; // required file submit, default is true + defCharset?: string; + limits?: { + fieldNameSize?: number; + fieldSize?: number; + fields?: number; + fileSize?: number; + files?: number; + parts?: number; + headerPairs?: number; + }; + checkFile?( + fieldname: string, + file: any, + filename: string, + encoding: string, + mimetype: string + ): void | Error; + } + + /** + * KoaApplication's Context will carry the default 'cookie' property in + * the egg's Context interface, which is wrong here because we have our own + * special properties (e.g: encrypted). So we must remove this property and + * create our own with the same name. + * @see https://github.com/eggjs/egg/pull/2958 + * + * However, the latest version of Koa has "[key: string]: any" on the + * context, and there'll be a type error for "keyof koa.Context". + * So we have to directly inherit from "KoaApplication.BaseContext" and + * rewrite all the properties to be compatible with types in Koa. + * @see https://github.com/eggjs/egg/pull/3329 + */ + export interface Context<ResponseBodyT = any> extends KoaApplication.BaseContext { + [key: string]: any; + body: ResponseBodyT; + + app: Application; + + // properties of koa.Context + req: IncomingMessage; + res: ServerResponse; + originalUrl: string; + respond?: boolean; + + service: IService; + + request: Request; + + response: Response<ResponseBodyT>; + + // The new 'cookies' instead of Koa's. + cookies: EggCookies; + + helper: IHelper; + + /** + * Resource Parameters + * @example + * ##### ctx.params.id {string} + * + * `GET /api/users/1` => `'1'` + * + * ##### ctx.params.ids {Array<String>} + * + * `GET /api/users/1,2,3` => `['1', '2', '3']` + * + * ##### ctx.params.fields {Array<String>} + * + * Expect request return data fields, for example + * `GET /api/users/1?fields=name,title` => `['name', 'title']`. + * + * ##### ctx.params.data {Object} + * + * Tht request data object + * + * ##### ctx.params.page {Number} + * + * Page number, `GET /api/users?page=10` => `10` + * + * ##### ctx.params.per_page {Number} + * + * The number of every page, `GET /api/users?per_page=20` => `20` + */ + params: any; + + /** + * @see Request#query + */ + query: PlainObject<string>; + + /** + * @see Request#queries + */ + queries: PlainObject<string[]>; + + /** + * @see Request#accept + */ + accept: accepts.Accepts; + + /** + * @see Request#acceptJSON + */ + acceptJSON: boolean; + + /** + * @see Request#ip + */ + ip: string; + + /** + * @see Response#realStatus + */ + realStatus: number; + + /** + * Set the ctx.body.data value + * + * @member {Object} Context#data= + * @example + * ```js + * ctx.data = { + * id: 1, + * name: 'fengmk2' + * }; + * ``` + * + * will get responce + * + * ```js + * HTTP/1.1 200 OK + * + * { + * "data": { + * "id": 1, + * "name": "fengmk2" + * } + * } + * ``` + */ + data: any; + + /** + * set ctx.body.meta value + * + * @example + * ```js + * ctx.meta = { + * count: 100 + * }; + * ``` + * will get responce + * + * ```js + * HTTP/1.1 200 OK + * + * { + * "meta": { + * "count": 100 + * } + * } + * ``` + */ + meta: any; + + /** + * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, + * which will be used as data when view is rendering. + * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. + * + * when you set locals, only object is available + * + * ```js + * this.locals = { + * a: 1 + * }; + * this.locals = { + * b: 1 + * }; + * this.locals.c = 1; + * console.log(this.locals); + * { + * a: 1, + * b: 1, + * c: 1, + * }; + * ``` + * + * `ctx.locals` has cache, it only merges `app.locals` once in one request. + * + * @member {Object} Context#locals + */ + locals: IApplicationLocals & IContextLocals; + + /** + * alias to {@link locals}, compatible with koa that use this variable + */ + state: any; + + /** + * Logger for Application, wrapping app.coreLogger with context infomation + * + * @member {ContextLogger} Context#logger + * @since 1.0.0 + * @example + * ```js + * this.logger.info('some request data: %j', this.request.body); + * this.logger.warn('WARNING!!!!'); + * ``` + */ + logger: EggLogger; + + /** + * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical + */ + getLogger(name: string): EggLogger; + + /** + * Request start time + */ + starttime: number; + + /** + * Request start timer using `performance.now()` + */ + performanceStarttime?: number; + + /** + * http request helper base on httpclient, it will auto save httpclient log. + * Keep the same api with httpclient.request(url, args). + * See https://github.com/node-modules/urllib#api-doc for more details. + */ + curl: EggHttpClient['request']; + + __(key: string, ...values: string[]): string; + gettext(key: string, ...values: string[]): string; + + /** + * get upload file stream + * @example + * ```js + * const stream = await this.getFileStream(); + * // get other fields + * console.log(stream.fields); + * ``` + * @method Context#getFileStream + * @param {Object} options + * @return {ReadStream} stream + * @since 1.0.0 + */ + getFileStream(options?: GetFileStreamOptions): Promise<FileStream>; + + /** + * @see Responce.redirect + */ + redirect(url: string, alt?: string): void; + + httpclient: EggContextHttpClient; + } + + export interface IContextLocals extends PlainObject { } + + export class Controller extends BaseContextClass { } + + export class Service extends BaseContextClass { } + + export class Subscription extends BaseContextClass { } + + /** + * The empty interface `IService` is a placeholder, for egg + * to auto injection service to ctx.service + * + * @example + * + * import { Service } from 'egg'; + * class FooService extends Service { + * async bar() {} + * } + * + * declare module 'egg' { + * export interface IService { + * foo: FooService; + * } + * } + * + * Now I can get ctx.service.foo at controller and other service file. + */ + export interface IService extends PlainObject { } // tslint:disable-line + + export interface IController extends PlainObject { } // tslint:disable-line + + export interface IMiddleware extends PlainObject { } // tslint:disable-line + + export interface IHelper extends PlainObject, BaseContextClass { + /** + * Generate URL path(without host) for route. Takes the route name and a map of named params. + * @method Helper#pathFor + * @param {String} name - Router Name + * @param {Object} params - Other params + * + * @example + * ```js + * app.get('home', '/index.htm', 'home.index'); + * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) + * => /index.htm?by=recent&limit=20 + * ``` + * @return {String} url path(without host) + */ + pathFor(name: string, params?: PlainObject): string; + + /** + * Generate full URL(with host) for route. Takes the route name and a map of named params. + * @method Helper#urlFor + * @param {String} name - Router name + * @param {Object} params - Other params + * @example + * ```js + * app.get('home', '/index.htm', 'home.index'); + * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) + * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 + * ``` + * @return {String} full url(with host) + */ + urlFor(name: string, params?: PlainObject): string; + } + + // egg env type + export type EggEnvType = 'local' | 'unittest' | 'prod' | string; + + /** + * plugin config item interface + */ + export interface IEggPluginItem { + env?: EggEnvType[]; + path?: string; + package?: string; + enable?: boolean; + } + + export type EggPluginItem = IEggPluginItem | boolean; + + /** + * build-in plugin list + */ + export interface EggPlugin { + [key: string]: EggPluginItem | undefined; + onerror?: EggPluginItem; + session?: EggPluginItem; + i18n?: EggPluginItem; + watcher?: EggPluginItem; + multipart?: EggPluginItem; + security?: EggPluginItem; + development?: EggPluginItem; + logrotator?: EggPluginItem; + schedule?: EggPluginItem; + static?: EggPluginItem; + jsonp?: EggPluginItem; + view?: EggPluginItem; + } + + /** + * Singleton instance in Agent Worker, extend {@link EggApplication} + */ + export class Agent extends EggApplication { + } + + export interface ClusterOptions { + /** specify framework that can be absolute path or npm package */ + framework?: string; + /** directory of application, default to `process.cwd()` */ + baseDir?: string; + /** customized plugins, for unittest */ + plugins?: object | null; + /** numbers of app workers, default to `os.cpus().length` */ + workers?: number; + /** listening port, default to 7001(http) or 8443(https) */ + port?: number; + /** https or not */ + https?: boolean; + /** ssl key */ + key?: string; + /** ssl cert */ + cert?: string; + [prop: string]: any; + } + + export function startCluster(options: ClusterOptions, callback: (...args: any[]) => any): void; + + export interface StartOptions { + /** specify framework that can be absolute path or npm package */ + framework?: string; + /** directory of application, default to `process.cwd()` */ + baseDir?: string; + /** ignore single process mode warning */ + ignoreWarning?: boolean; + } + + export function start(options?: StartOptions): Promise<Application> + + /** + * Powerful Partial, Support adding ? modifier to a mapped property in deep level + * @example + * import { PowerPartial, EggAppConfig } from 'egg'; + * + * // { view: { defaultEngines: string } } => { view?: { defaultEngines?: string } } + * type EggConfig = PowerPartial<EggAppConfig> + */ + export type PowerPartial<T> = { + [U in keyof T]?: T[U] extends object + ? PowerPartial<T[U]> + : T[U] + }; + + // send data can be number|string|boolean|object but not Set|Map + export interface Messenger extends EventEmitter { + /** + * broadcast to all agent/app processes including itself + */ + broadcast(action: string, data: any): void; + + /** + * send to agent from the app, + * send to an random app from the agent + */ + sendRandom(action: string, data: any): void; + + /** + * send to specified process + */ + sendTo(pid: number, action: string, data: any): void; + + /** + * send to agent from the app, + * send to itself from the agent + */ + sendToAgent(action: string, data: any): void; + + /** + * send to all app including itself from the app, + * send to all app from the agent + */ + sendToApp(action: string, data: any): void; + } + + // compatible + export interface EggLoaderOptions extends CoreLoaderOptions { } + export interface EggLoader extends CoreLoader { } + + /** + * App worker process Loader, will load plugins + * @see https://github.com/eggjs/egg-core + */ + export class AppWorkerLoader extends CoreLoader { + loadConfig(): void; + load(): void; + } + + /** + * Agent worker process loader + * @see https://github.com/eggjs/egg-loader + */ + export class AgentWorkerLoader extends CoreLoader { + loadConfig(): void; + load(): void; + } + + export interface IBoot { + /** + * Ready to call configDidLoad, + * Config, plugin files are referred, + * this is the last chance to modify the config. + */ + configWillLoad?(): void; + + /** + * Config, plugin files have loaded + */ + configDidLoad?(): void; + + /** + * All files have loaded, start plugin here + */ + didLoad?(): Promise<void>; + + /** + * All plugins have started, can do some thing before app ready + */ + willReady?(): Promise<void>; + + /** + * Worker is ready, can do some things, + * don't need to block the app boot + */ + didReady?(): Promise<void>; + + /** + * Server is listening + */ + serverDidReady?(): Promise<void>; + + /** + * Do some thing before app close + */ + beforeClose?(): Promise<void>; + } + + export interface Singleton<T> { + get(id: string): T; + } +} diff --git a/index.js b/index.js index 3705bfaadd..7e3269eda3 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,3 @@ -'use strict'; - /** * @namespace Egg */ @@ -8,7 +6,13 @@ * Start egg application with cluster mode * @since 1.0.0 */ -exports.startCluster = require('./lib/cluster'); +exports.startCluster = require('egg-cluster').startCluster; + +/** + * Start egg application with single process mode + * @since 1.0.0 + */ +exports.start = require('./lib/start'); /** * @member {Application} Egg#Application @@ -23,25 +27,42 @@ exports.Application = require('./lib/application'); exports.Agent = require('./lib/agent'); /** - * @member {AgentWorkerClient} Egg#AgentWorkerClient + * @member {AppWorkerLoader} Egg#AppWorkerLoader * @since 1.0.0 */ -exports.AgentWorkerClient = require('./lib/core/agent_worker_client'); +exports.AppWorkerLoader = require('./lib/loader').AppWorkerLoader; /** - * @member {AppWorkerClient} Egg#AppWorkerClient + * @member {AgentWorkerLoader} Egg#AgentWorkerLoader * @since 1.0.0 */ -exports.AppWorkerClient = require('./lib/core/app_worker_client'); +exports.AgentWorkerLoader = require('./lib/loader').AgentWorkerLoader; /** - * @member {AppWorkerLoader} Egg#AppWorkerLoader - * @since 1.0.0 + * @member {Controller} Egg#Controller + * @since 1.1.0 */ -exports.AppWorkerLoader = require('./lib/loader').AppWorkerLoader; +exports.Controller = require('./lib/core/base_context_class'); /** - * @member {AgentWorkerLoader} Egg#AgentWorkerLoader - * @since 1.0.0 + * @member {Service} Egg#Service + * @since 1.1.0 */ -exports.AgentWorkerLoader = require('./lib/loader').AgentWorkerLoader; +exports.Service = require('./lib/core/base_context_class'); + +/** + * @member {Subscription} Egg#Subscription + * @since 1.10.0 + */ +exports.Subscription = require('./lib/core/base_context_class'); + +/** + * @member {BaseContextClass} Egg#BaseContextClass + * @since 1.2.0 + */ +exports.BaseContextClass = require('./lib/core/base_context_class'); + +/** + * @member {Boot} Egg#Boot + */ +exports.Boot = require('./lib/core/base_hook_class'); diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000000..8f7bcd336e --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1 @@ +import '.'; diff --git a/lib/agent.js b/lib/agent.js index 2e9d7b80fb..bfb3edf59c 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,84 +1,48 @@ 'use strict'; const path = require('path'); +const ms = require('ms'); const EggApplication = require('./egg'); const AgentWorkerLoader = require('./loader').AgentWorkerLoader; -const AgentWorkerClient = require('./core/agent_worker_client'); -const AGENT_CLIENTS = Symbol('Agent#agentClients'); const EGG_LOADER = Symbol.for('egg#loader'); const EGG_PATH = Symbol.for('egg#eggPath'); /** - * Agent 对象,由 AgentWorker 实例化,和 {@link Application} 共用继承 {@link EggApplication} 的 API - * @extends EggApplication + * Singleton instance in Agent Worker, extend {@link EggApplication} + * @augments EggApplication */ class Agent extends EggApplication { - /** - * @constructor - * @param {Object} options - 同 {@link EggApplication} + * @class + * @param {Object} options - see {@link EggApplication} */ - constructor(options) { - options = options || {}; + constructor(options = {}) { options.type = 'agent'; super(options); this.loader.load(); - // 不让 agent 退出 - setInterval(() => {}, 24 * 60 * 60 * 1000); + // dump config after loaded, ensure all the dynamic modifications will be recorded + const dumpStartTime = Date.now(); + this.dumpConfig(); + this.coreLogger.info( + '[egg:core] dump config after load, %s', + ms(Date.now() - dumpStartTime) + ); + + // keep agent alive even it doesn't have any io tasks + this.agentAliveHandler = setInterval(() => {}, 24 * 60 * 60 * 1000); this._uncaughtExceptionHandler = this._uncaughtExceptionHandler.bind(this); process.on('uncaughtException', this._uncaughtExceptionHandler); } - /** - * 当前进程实例化的 AgentWorkerClient 集合,只在 mm.app 场景下才有用 - * @private - */ - get agentWorkerClients() { - if (!this[AGENT_CLIENTS]) { - this[AGENT_CLIENTS] = new Map(); - } - return this[AGENT_CLIENTS]; - } - - /** - * 启动一个 agent 任务 - * @param {Object} options - * - {String} name 唯一的名字,例如:diamond | configclient - * - {Object} client 任务的客户端 - * - {Function} subscribe 提供一个统一注册的方法 `subscribe(info, listener)` - * - {Function} formatKey 提供一个方法:将订阅信息格式化为一个唯一的键值 `formatKey(info)` - * @return {AgentWorkerClient} - - * @example - * ``` - * // 实际创建一个 configclient,然后启动 agent 任务 - * const client = new Configclient(); - * agent.startAgent({ - * name: 'configclient', - * client: client, - * subscribe: function(info, listener) { - * client.subscribe(info, listener); - * }, - * formatKey: function(info) { - * return info.dataId + '@' + info.groupId; - * }, - * }) - * ``` - */ - startAgent(options) { - options = options || {}; - options.agent = this; - - return new AgentWorkerClient(options); - } - _uncaughtExceptionHandler(err) { if (!(err instanceof Error)) { err = new Error(String(err)); } + /* istanbul ignore else */ if (err.name === 'Error') { err.name = 'unhandledExceptionError'; } @@ -93,11 +57,39 @@ class Agent extends EggApplication { return path.join(__dirname, '..'); } + _wrapMessenger() { + for (const methodName of [ + 'broadcast', + 'sendTo', + 'sendToApp', + 'sendToAgent', + 'sendRandom', + ]) { + wrapMethod(methodName, this.messenger, this.coreLogger); + } + + function wrapMethod(methodName, messenger, logger) { + const originMethod = messenger[methodName]; + messenger[methodName] = function() { + const stack = new Error().stack.split('\n').slice(1).join('\n'); + logger.warn( + "agent can't call %s before server started\n%s", + methodName, + stack + ); + originMethod.apply(this, arguments); + }; + messenger.prependOnceListener('egg-ready', () => { + messenger[methodName] = originMethod; + }); + } + } + close() { process.removeListener('uncaughtException', this._uncaughtExceptionHandler); - super.close(); + clearInterval(this.agentAliveHandler); + return super.close(); } - } module.exports = Agent; diff --git a/lib/application.js b/lib/application.js index 15d39c19de..eac56c9c0a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -1,35 +1,82 @@ -/** - * 对 koa application 的所有扩展,都放在此文件统一维护。 - * - * - koa application: https://github.com/koajs/koa/blob/master/lib/application.js - */ - 'use strict'; const path = require('path'); +const fs = require('fs'); +const ms = require('ms'); +const is = require('is-type-of'); const graceful = require('graceful'); +const http = require('http'); +const cluster = require('cluster-client'); +const onFinished = require('on-finished'); +const { assign } = require('utility'); +const eggUtils = require('egg-core').utils; const EggApplication = require('./egg'); const AppWorkerLoader = require('./loader').AppWorkerLoader; +const KEYS = Symbol('Application#keys'); +const HELPER = Symbol('Application#Helper'); +const LOCALS = Symbol('Application#locals'); +const BIND_EVENTS = Symbol('Application#bindEvents'); +const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig'); const EGG_LOADER = Symbol.for('egg#loader'); const EGG_PATH = Symbol.for('egg#eggPath'); +const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); +const RESPONSE_RAW = Symbol('Application#responseRaw'); +// client error => 400 Bad Request +// Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror +const DEFAULT_BAD_REQUEST_HTML = `<html> + <head><title>400 Bad Request + +

    400 Bad Request

    +
    + + `; +const DEFAULT_BAD_REQUEST_HTML_LENGTH = Buffer.byteLength(DEFAULT_BAD_REQUEST_HTML); +const DEFAULT_BAD_REQUEST_RESPONSE = + `HTTP/1.1 400 Bad Request\r\nContent-Length: ${DEFAULT_BAD_REQUEST_HTML_LENGTH}` + + `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`; + +// Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 +function escapeHeaderValue(value) { + // Protect against response splitting. The regex test is there to + // minimize the performance impact in the common case. + return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value; +} + +// Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 /** - * Application 对象,由 AppWorker 实例化,和 {@link Agent} 共用继承 {@link EggApplication} 的 API - * @extends EggApplication + * Singleton instance in App Worker, extend {@link EggApplication} + * @augments EggApplication */ class Application extends EggApplication { /** - * @constructor - * @param {Object} options - 同 {@link EggApplication} + * @class + * @param {Object} options - see {@link EggApplication} */ - constructor(options) { - options = options || {}; + constructor(options = {}) { options.type = 'application'; super(options); - this.loader.load(); - this.on('server', server => this.onServer(server)); + + // will auto set after 'server' event emit + this.server = null; + + try { + this.loader.load(); + } catch (e) { + // close gracefully + this[CLUSTER_CLIENTS].forEach(cluster.close); + throw e; + } + + // dump config after loaded, ensure all the dynamic modifications will be recorded + const dumpStartTime = Date.now(); + this.dumpConfig(); + this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime)); + + this[WARN_CONFUSED_CONFIG](); + this[BIND_EVENTS](); } get [EGG_LOADER]() { @@ -40,15 +87,260 @@ class Application extends EggApplication { return path.join(__dirname, '..'); } + [RESPONSE_RAW](socket, raw) { + /* istanbul ignore next */ + if (!socket.writable) return; + if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + + const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body; + const headers = raw.headers || {}; + const status = raw.status || 400; + + let responseHeaderLines = ''; + const firstLine = `HTTP/1.1 ${status} ${http.STATUS_CODES[status] || 'Unknown'}`; + + // Not that safe because no validation for header keys. + // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L451 + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === 'content-length') { + delete headers[key]; + continue; + } + responseHeaderLines += `${key}: ${escapeHeaderValue(headers[key])}\r\n`; + } + + responseHeaderLines += `Content-Length: ${Buffer.byteLength(body)}\r\n`; + + socket.end(`${firstLine}\r\n${responseHeaderLines}\r\n${body.toString()}`); + } + + onClientError(err, socket) { + // ignore when there is no http body, it almost like an ECONNRESET + if (err.rawPacket) { + this.logger.warn('A client (%s:%d) error [%s] occurred: %s', + socket.remoteAddress, + socket.remotePort, + err.code, + err.message); + } + + if (typeof this.config.onClientError === 'function') { + const p = eggUtils.callFn(this.config.onClientError, [ err, socket, this ]); + + // the returned object should be something like: + // + // { + // body: '...', + // headers: { + // ... + // }, + // status: 400 + // } + // + // default values: + // + // + body: '' + // + headers: {} + // + status: 400 + p.then(ret => { + this[RESPONSE_RAW](socket, ret || {}); + }).catch(err => { + this.logger.error(err); + this[RESPONSE_RAW](socket); + }); + } else { + // because it's a raw socket object, we should return the raw HTTP response + // packet. + this[RESPONSE_RAW](socket); + } + } + onServer(server) { + // expose app.server + this.server = server; + // set ignore code + const serverGracefulIgnoreCode = this.config.serverGracefulIgnoreCode || []; + + /* istanbul ignore next */ graceful({ server: [ server ], error: (err, throwErrorCount) => { - if (err.message) { - err.message += ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')'; + const originMessage = err.message; + if (originMessage) { + // shouldjs will override error property but only getter + // https://github.com/shouldjs/should.js/blob/889e22ebf19a06bc2747d24cf34b25cc00b37464/lib/assertion-error.js#L26 + Object.defineProperty(err, 'message', { + get() { + return originMessage + ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')'; + }, + configurable: true, + enumerable: false, + }); } this.coreLogger.error(err); }, + ignoreCode: serverGracefulIgnoreCode, + }); + + server.on('clientError', (err, socket) => this.onClientError(err, socket)); + + // server timeout + if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout); + } + + /** + * global locals for view + * @member {Object} Application#locals + * @see Context#locals + */ + get locals() { + if (!this[LOCALS]) { + this[LOCALS] = {}; + } + return this[LOCALS]; + } + + set locals(val) { + if (!this[LOCALS]) { + this[LOCALS] = {}; + } + + assign(this[LOCALS], val); + } + + handleRequest(ctx, fnMiddleware) { + this.emit('request', ctx); + onFinished(ctx.res, () => this.emit('response', ctx)); + return super.handleRequest(ctx, fnMiddleware); + } + + /** + * save routers to `run/router.json` + * @private + */ + dumpConfig() { + super.dumpConfig(); + + // dump routers to router.json + const rundir = this.config.rundir; + const FULLPATH = this.loader.FileLoader.FULLPATH; + try { + const dumpRouterFile = path.join(rundir, 'router.json'); + const routers = []; + for (const layer of this.router.stack) { + routers.push({ + name: layer.name, + methods: layer.methods, + paramNames: layer.paramNames, + path: layer.path, + regexp: layer.regexp.toString(), + stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'), + }); + } + fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2)); + } catch (err) { + this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`); + } + } + + /** + * Run async function in the background + * @see Context#runInBackground + * @param {Function} scope - the first args is an anonymous ctx + */ + runInBackground(scope) { + const ctx = this.createAnonymousContext(); + if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + this.ctxStorage.run(ctx, () => { + ctx.runInBackground(scope); + }); + } + + /** + * Run async function in the anonymous context scope + * @see Context#runInAnonymousContextScope + * @param {Function} scope - the first args is an anonymous ctx, scope should be async function + * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. + */ + async runInAnonymousContextScope(scope, req) { + const ctx = this.createAnonymousContext(req); + if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + return await this.ctxStorage.run(ctx, async () => { + return await scope(ctx); + }); + } + + /** + * secret key for Application + * @member {String} Application#keys + */ + get keys() { + if (!this[KEYS]) { + if (!this.config.keys) { + if (this.config.env === 'local' || this.config.env === 'unittest') { + const configPath = path.join(this.config.baseDir, 'config/config.default.js'); + console.error('Cookie need secret key to sign and encrypt.'); + console.error('Please add `config.keys` in %s', configPath); + } + throw new Error('Please set config.keys first'); + } + + this[KEYS] = this.config.keys.split(',').map(s => s.trim()); + } + return this[KEYS]; + } + + set keys(_) { + // ignore + } + + /** + * reference to {@link Helper} + * @member {Helper} Application#Helper + */ + get Helper() { + if (!this[HELPER]) { + /** + * The Helper class which can be used as utility function. + * We support developers to extend Helper through ${baseDir}/app/extend/helper.js , + * then you can use all method on `ctx.helper` that is a instance of Helper. + */ + class Helper extends this.BaseContextClass {} + this[HELPER] = Helper; + } + return this[HELPER]; + } + + /** + * bind app's events + * + * @private + */ + [BIND_EVENTS]() { + // Browser Cookie Limits: http://browsercookielimits.squawky.net/ + this.on('cookieLimitExceed', ({ name, value, ctx }) => { + const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`); + err.name = 'CookieLimitExceedError'; + err.key = name; + err.cookie = value; + ctx.coreLogger.error(err); + }); + // expose server to support websocket + this.once('server', server => this.onServer(server)); + } + + /** + * warn when confused configurations are present + * + * @private + */ + [WARN_CONFUSED_CONFIG]() { + const confusedConfigurations = this.config.confusedConfigurations; + Object.keys(confusedConfigurations).forEach(key => { + if (this.config[key] !== undefined) { + this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.', + key, confusedConfigurations[key]); + } }); } } diff --git a/lib/cluster/index.js b/lib/cluster/index.js deleted file mode 100644 index 7cd4959c4b..0000000000 --- a/lib/cluster/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const path = require('path'); -const startCluster = require('egg-cluster').startCluster; - -module.exports = (options, callback) => { - options = options || {}; - options.customEgg = options.customEgg || path.join(__dirname, '../..'); - startCluster(options, callback); -}; diff --git a/lib/core/agent_worker_client.js b/lib/core/agent_worker_client.js deleted file mode 100644 index e743eab0aa..0000000000 --- a/lib/core/agent_worker_client.js +++ /dev/null @@ -1,207 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const is = require('is-type-of'); -const Base = require('sdk-base'); -const co = require('co'); - -/** - * Node 多进程模型下,共享中间件连接的通用解决方案,该类在 agent 进程中被实例化 - * @see https://github.com/eggjs/egg#多进程模型及进程间通讯 - */ -class AgentWorkerClient extends Base { - - /** - * @constructor - * @param {Object} options - * - {Agent} agent: Agent 对象实例 - * - {String} name: 唯一的名字,例如:diamond | configclient - * - {Object} client: 任务的客户端 - * - {Function} subscribe: 提供一个统一注册的方法 - */ - constructor(options) { - assert(options.name, '[egg:agent] AgentWorkerClient#constructor options.name is required'); - assert(options.client, '[egg:agent] AgentWorkerClient#constructor options.client is required'); - assert(options.subscribe, '[egg:agent] AgentWorkerClient#constructor options.subscribe is required'); - - super(); - - this.options = options; - const agent = options.agent; - this.messenger = agent.messenger; - this.logger = agent.loggers.coreLogger; - - // 禁止 AppWorkerClient 重名,以免事件互相干扰 - // 同时保证一个进程中同类型的 WorkerClient 最多只实例化一个 - assert(!agent.agentWorkerClients.has(this.name), - `There is already a AgentWorkerClient named "!{this.name}", pid: ${process.pid}.`); - agent.agentWorkerClients.set(this.name, this); - - // 缓存订阅数据 - this._subscriptions = new Map(); - - /** - * 命令集合 - * @member WorkerClient#commands - */ - this.commands = { - sendResponse: `${this.name}_invoke_response`, - subscribeChanged: `${this.name}_subscribe_changed`, - }; - - // 监听订阅请求事件 - this.messenger.on(`${this.name}_subscribe_request`, this._onSubscribeRequest.bind(this)); - // 监听 API 调用请求事件 - this.messenger.on(`${this.name}_invoke_request`, this._onInvokeRequest.bind(this)); - - this.innerClient.ready(this.ready.bind(this, true)); - - this.logger.info('[egg:agent] create an AgentWorkerClient for "%s"', this.name); - } - - /** - * 唯一的名字,同 options.name - * @member {String} - */ - get name() { - return this.options.name; - } - - /** - * 任务的客户端,同 options.client - * @member {Object} - */ - get innerClient() { - return this.options.client; - } - - /** - * 发送给指定的进程 - * @param {String} pid 接收者进程 id - * @param {String} action 消息动作唯一标识 - * @param {data} data 发送的消息数据。 - * @return {AgentWorkerClient} this - */ - _sendTo(pid, action, data) { - this.messenger.sendTo(pid, action, data); - this.logger.info('[egg:agent] [%s] send a "%s" action to worker:%s with data: %j', this.name, action, pid, data); - return this; - } - - /** - * 发送 message,广播 - * @param {String} action 消息动作唯一标识 - * @param {Object} data 广播的数据。 - * @return {AgentWorkerClient} this - */ - _broadcast(action, data) { - this.messenger.broadcast(action, data); - this.logger.info('[egg:agent] [%s] broadcast a "%s" action to all workers with data: %j', this.name, action, data); - return this; - } - - /** - * 当收到订阅请求时的处理 - * @param {Object} req 请求对象 - * @private - */ - _onSubscribeRequest(req) { - const info = req.info; - const key = req.key; - const oldData = this._subscriptions.get(key); - const pid = req.pid; - - // 已存在,无需重新订阅 - if (oldData) { - // 已经存在的配置,则直接触发一次,定向推送给订阅者本身 - if (oldData.timestamp) { - this._sendTo(pid, this.commands.subscribeChanged, oldData); - } - return; - } - - const data = { - key, - info, - value: null, - timestamp: null, - }; - - this.logger.info('[egg:agent:%s] start subscribe %s, info: %j', this.name, key, data); - this._subscriptions.set(key, data); - - this.options.subscribe(info, this._onSubscribeResult.bind(this, key)); - } - - /** - * 订阅数据改变以后,广播给 worker - * @param {String} key 订阅的键 - * @param {String} value 变化的值 - * @private - */ - _onSubscribeResult(key, value) { - const data = this._subscriptions.get(key); - data.value = value; - data.timestamp = Date.now(); - - this._broadcast(this.commands.subscribeChanged, data); - } - - /** - * 将请求转发给真实的中间件客户端,并把结果通过 messenger 返回给对应 worker - * @param {Object} request 请求对象 - * @private - */ - _onInvokeRequest(request) { - const that = this; - const pid = request.pid; - const oneway = !!request.oneway; - const response = { - opaque: request.opaque, - success: true, - data: null, - error: null, - }; - - // 插入回调函数 - request.args = request.args || []; - - this.logger.info('[egg:agent] [%s] receive a request:%s to call method:%s with args:%j from worker:%s', - this.name, request.opaque, request.method, request.args, request.pid); - - function callback(err, result) { - if (err) { - response.success = false; - // ipc 通道无法直接传送 Error 对象 - response.errorMessage = err.message; - response.errorStack = err.stack; - } else { - response.data = result; - } - // 结果直接返回给指定 worker - that._sendTo(pid, that.commands.sendResponse, response); - } - - if (!oneway) { - request.args.push(callback); - } - - // 执行内置真实客户端的对应的方法 - let method = this.innerClient[request.method]; - // 兼容 generatorFunction - if (is.generatorFunction(method)) { - method = co.wrap(method); - } - - const ret = method.apply(this.innerClient, request.args); - - // 兼容 Promise 方法 - if (!oneway && is.promise(ret)) { - ret.then(result => { - callback(null, result); - }).catch(callback); - } - } -} - -module.exports = AgentWorkerClient; diff --git a/lib/core/app_worker_client.js b/lib/core/app_worker_client.js deleted file mode 100644 index bc33a52cb7..0000000000 --- a/lib/core/app_worker_client.js +++ /dev/null @@ -1,360 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const ms = require('humanize-ms'); -const Base = require('sdk-base'); - -const MAX_VALUE = Math.pow(2, 31) - 10; -const PROCESS_ID = String(process.pid); -const hasOwnProperty = Object.prototype.hasOwnProperty; - -const defaultOptions = { - responseTimeout: '5s', -}; - -/** - * Node 多进程模型下,共享中间件连接的通用解决方案,该类在 worker 进程中被实例化 - */ -class AppWorkerClient extends Base { - - /** - * @constructor - * @param {Object} options - * - {String} name - 确保唯一的名字 - * - {Application} app - application 实例 - * - {Number} responseTimeout - 请求超时时长,默认 5s - */ - constructor(options) { - assert(options && options.name, '[egg:worker] AppWorkerClient#constructor options.name is required'); - assert(options.app, '[egg:worker] AppWorkerClient#constructor options.app is required'); - - super(); - - // 服务依赖肯定会比较多的,设置为 100 个 - this.setMaxListeners(100); - - this.options = {}; - Object.assign(this.options, defaultOptions, options); - this.options.responseTimeout = ms(this.options.responseTimeout); - - const app = options.app; - this.messenger = app.messenger; - this.logger = app.loggers.coreLogger; - - // 缓存订阅信息 - this._subscriptions = new Map(); - // 缓存接口调用信息 - this._invokes = new Map(); - this._opaque = 0; - - /** - * 命令集合 - * @member {Object} AppWorkerClient#commands - * @private - */ - this.commands = { - invoke: `${this.name}_invoke_request`, - sub: `${this.name}_subscribe_request`, - }; - - // 禁止 AppWorkerClient 重名,以免事件互相干扰 - // 同时保证一个进程中同类型的 WorkerClient 最多只实例化一个 - assert(!app.appWorkerClients.has(this.name), - `There is already a AppWorkerClient named "${this.name}", pid: ${process.pid}.`); - app.appWorkerClients.set(this.name, this); - - // 监听 agent worker 进程重启事件 - this.messenger.on('agent-start', this._onAgentWorkerRestart.bind(this)); - // 监听 agent worker 调用返回事件 - this.messenger.on(`${this.name}_invoke_response`, this._onInvokeResponse.bind(this)); - // 监听 agent worker 订阅数据变化事件 - this.messenger.on(`${this.name}_subscribe_changed`, this._onSubscribeChanged.bind(this)); - - this.logger.info('[egg:worker] create an AppWorkerClient for "%s"', this.name); - // 子类需要自己实现客户端 ready 的逻辑 - // this.ready(true); - } - - /** - * 标准事件列表,供参考 - * @member {Array} - * @private - */ - get publicEvents() { - return [ - // agent worker 进程重启时触发 - 'agent_restart', - // 异常时触发 - 'error', - ]; - } - - /** - * 唯一的名字,同 options.name - * @member {String} - */ - get name() { - return this.options.name; - } - - /** - * 当前进程号 - * @member {String} - */ - get pid() { - return PROCESS_ID; - } - - /** - * 获取下一个 opaque 来唯一标识一次接口调用 - * @return {Number} 返回下一个 opaque - * @private - */ - _getNextOpaque() { - if (this._opaque >= MAX_VALUE) { - this._opaque = 0; - } - return this._opaque++; - } - - /** - * 调用 agent worker 中实际客户端的某方法 - * @param {String} method - 方法名 - * @param {Array} args - 参数列表 - * @param {Object} options - 其他参数 - * @return {Promise} 返回一个 Promise - */ - _invoke(method, args, options) { - options = options || {}; - return new Promise((resolve, reject) => { - const opaque = this._getNextOpaque(); - const oneway = !!options.oneway; - const requestObj = { - opaque, - method, - args: args || [], - pid: this.pid, - oneway, - }; - - // 通过 rpc 通道转发给 agent worker - this._sendToAgent(this.commands.invoke, requestObj); - - // 如果是单向的调用,直接返回 - if (oneway) { - resolve(); - return; - } - - requestObj.resolve = resolve; - requestObj.reject = reject; - - // 超时机制 - requestObj.timer = setTimeout(() => { - const err = new Error(`Agent worker no response in ${this.options.responseTimeout}ms, AppWorkerClient:${this.name} invoke ${method} with req#${opaque}`); - err.name = 'AgentWorkerRequestTimeoutError'; - reject(err); - - // 抛异常事件,用于统计异常等用处 - this.error(err); - - // 清理调用记录 - this._invokes.delete(opaque); - }, this.options.responseTimeout); - - // 保存调用记录 - this._invokes.set(opaque, requestObj); - }); - } - - /** - * 调用 agent worker 中实际客户端的某方法 - * @param {String} method - 方法名 - * @param {Array} args - 参数列表 - * @param {Object} options - 其他参数 - */ - _invokeOneway(method, args, options) { - options = options || {}; - options.oneway = true; - this._invoke(method, args, options); - } - - /** - * EventEmitter.on 的别名,主要用于子类覆盖 on 方法的场景使用 - * @param {String} event - 事件名 - * @param {Function} listener - 回调函数 - * @return {AppWorkerClient} this - * @private - */ - _on(event, listener) { - return super.on(event, listener); - } - - /** - * EventEmitter.once 的别名,主要用于子类覆盖 once 方法的场景使用 - * @param {String} event - 事件名 - * @param {Function} listener - 回调函数 - * @return {AppWorkerClient} this - * @private - */ - _once(event, listener) { - return super.once(event, listener); - } - - /** - * EventEmitter.removeListener 的别名,主要用于子类覆盖 removeListener 方法的场景使用 - * @param {String} event - 事件名 - * @param {Function} listener - 回调函数 - * @return {AppWorkerClient} this - * @private - */ - _removeListener(event, listener) { - return super.removeListener(event, listener); - } - - /** - * EventEmitter.removeAllListeners 的别名,主要用于子类覆盖 removeAllListeners 方法的场景使用 - * @param {String} event - 事件名 - * @return {AppWorkerClient} this - * @private - */ - _removeAllListeners(event) { - return super.removeAllListeners(event); - } - - /** - * 注册监听(适用于消息类插件:configclient、diamond 等) - * @param {Object} info - 订阅信息(由子类自己决定结构) - * @param {Function} listener - 回调函数 - * @return {AppWorkerClient} this - */ - _subscribe(info, listener) { - const key = this._formatKey(info); - this.on(key, listener); - const subInfo = this._subscriptions.get(key); - if (!subInfo) { - const subData = { - key, - info, - pid: this.pid, - }; - this._subscriptions.set(key, { subData }); - this._sendToAgent(this.commands.sub, subData); - } else if (hasOwnProperty.call(subInfo, 'value')) { - // trigger listener immediately - listener(subInfo.value); - } - return this; - } - - /** - * 取消注册 - * @param {Object} info - 订阅信息(由子类自己决定结构) - * @param {Function} listener - 回调函数 - * @return {AppWorkerClient} this - */ - _unSubscribe(info, listener) { - const key = this._formatKey(info); - if (this._subscriptions.has(key)) { - this._subscriptions.delete(key); - if (listener) { - this.removeListener(key, listener); - } else { - this.removeAllListeners(key); - } - // todo: 目前只是 app worker 中取消订阅,agent worker 里任然会收到推送 - } - return this; - } - - /** - * 将订阅信息格式化为一个唯一的键值,例如:info { dataId: 'foo', groupId: 'bar'} => `foo@bar` - * @param {Object} info - 订阅信息(由子类自己决定结构) - * @return {String} key - * @private - */ - _formatKey(info) { - return JSON.stringify(info); - } - - /** - * 发送 message 给 AgentWorker - * @param {String} action 消息动作唯一标识 - * @param {Object} data 广播的数据。 - * @return {AppWorkerClient} this - */ - _sendToAgent(action, data) { - this.messenger.broadcast(action, data); - this.logger.info('[egg:worker] [%s] send a "%s" action to agent with data: %j', this.name, action, data); - return this; - } - - /** - * @param {Error} err - 异常对象 - */ - error(err) { - this.emit('error', err); - } - - /** - * agent worker 重启逻辑 - * @private - */ - _onAgentWorkerRestart() { - // 重新订阅 - for (const key of this._subscriptions.keys()) { - const info = this._subscriptions.get(key); - this._sendToAgent(this.commands.sub, info.subData); - } - - this.logger.info('[egg:worker] [%s] reSubscribe done for "agent restart"', this.name); - - // 暴露给子类监听 - this.emit('agent_restart'); - } - - /** - * agent worker 返回调用结果时回调 - * @param {Object} response 回调的数据 - * @private - */ - _onInvokeResponse(response) { - const invoke = this._invokes.get(response.opaque); - if (invoke) { - clearTimeout(invoke.timer); - this._invokes.delete(response.opaque); - - if (response.success) { - invoke.resolve(response.data); - this.logger.info('[egg:worker] [%s] invoke#%s [%s] success with response data: %j', - this.name, invoke.opaque, invoke.method, response.data); - } else { - const err = new Error(response.errorMessage); - err.stack = response.errorStack; - invoke.reject(err); - this.logger.info('[egg:worker] [%s] invoke#%s [%s] failed with error: %s', - this.name, invoke.opaque, invoke.method, response.errorMessage); - } - } else { - this.logger.warn('[egg:worker] [%s] can not find request handler for invoke with response data: %j. maybe invoke timeout.', - this.name, response.data); - } - } - - /** - * 订阅的数据发生变化时触发 - * @param {Object} data 变化的数据 - * @private - */ - _onSubscribeChanged(data) { - const key = data.key; - if (this._subscriptions.has(key)) { - this.logger.info('[egg:worker] [%s] key[%s] value changed, new value: %j', this.name, key, data.value); - const info = this._subscriptions.get(key); - info.value = data.value; - this.emit(key, data.value); - } - } -} - -module.exports = AppWorkerClient; diff --git a/lib/core/base_context_class.js b/lib/core/base_context_class.js new file mode 100644 index 0000000000..1016d60440 --- /dev/null +++ b/lib/core/base_context_class.js @@ -0,0 +1,20 @@ +'use strict'; + +const EggCoreBaseContextClass = require('egg-core').BaseContextClass; +const BaseContextLogger = require('./base_context_logger'); + +const LOGGER = Symbol('BaseContextClass#logger'); + +/** + * BaseContextClass is a base class that can be extended, + * it's instantiated in context level, + * {@link Helper}, {@link Service} is extending it. + */ +class BaseContextClass extends EggCoreBaseContextClass { + get logger() { + if (!this[LOGGER]) this[LOGGER] = new BaseContextLogger(this.ctx, this.pathName); + return this[LOGGER]; + } +} + +module.exports = BaseContextClass; diff --git a/lib/core/base_context_logger.js b/lib/core/base_context_logger.js new file mode 100644 index 0000000000..6c1dc2c8df --- /dev/null +++ b/lib/core/base_context_logger.js @@ -0,0 +1,64 @@ +const CALL = Symbol('BaseContextLogger#call'); + +class BaseContextLogger { + /** + * @class + * @param {Context} ctx - context instance + * @param {String} pathName - class path name + * @since 1.0.0 + */ + constructor(ctx, pathName) { + /** + * @member {Context} BaseContextLogger#ctx + * @since 1.2.0 + */ + this.ctx = ctx; + this.pathName = pathName; + } + + [CALL](method, args) { + // add `[${pathName}]` in log + if (this.pathName && typeof args[0] === 'string') { + args[0] = `[${this.pathName}] ${args[0]}`; + } + this.ctx.app.logger[method](...args); + } + + /** + * @member {Function} BaseContextLogger#debug + * @param {...any} args - log msg + * @since 1.2.0 + */ + debug(...args) { + this[CALL]('debug', args); + } + + /** + * @member {Function} BaseContextLogger#info + * @param {...any} args - log msg + * @since 1.2.0 + */ + info(...args) { + this[CALL]('info', args); + } + + /** + * @member {Function} BaseContextLogger#warn + * @param {...any} args - log msg + * @since 1.2.0 + */ + warn(...args) { + this[CALL]('warn', args); + } + + /** + * @member {Function} BaseContextLogger#error + * @param {...any} args - log msg + * @since 1.2.0 + */ + error(...args) { + this[CALL]('error', args); + } +} + +module.exports = BaseContextLogger; diff --git a/lib/core/base_hook_class.js b/lib/core/base_hook_class.js new file mode 100644 index 0000000000..2bda13bf60 --- /dev/null +++ b/lib/core/base_hook_class.js @@ -0,0 +1,31 @@ +'use strict'; + +const assert = require('assert'); +const INSTANCE = Symbol('BaseHookClass#instance'); + +class BaseHookClass { + + constructor(instance) { + this[INSTANCE] = instance; + } + + get logger() { + return this[INSTANCE].logger; + } + + get config() { + return this[INSTANCE].config; + } + + get app() { + assert(this[INSTANCE].type === 'application', 'agent boot should not use app instance'); + return this[INSTANCE]; + } + + get agent() { + assert(this[INSTANCE].type === 'agent', 'app boot should not use agent instance'); + return this[INSTANCE]; + } +} + +module.exports = BaseHookClass; diff --git a/lib/core/base_service.js b/lib/core/base_service.js deleted file mode 100644 index a0a6377bed..0000000000 --- a/lib/core/base_service.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Service, service 基类,封装 service 的通用逻辑 - */ - -'use strict'; - -/** - * service 基类,封装 service 的通用逻辑。 - * 你可以通过继承此基类来编写 service - * @example - * ```js - * // app/service/user.js - * const Service = require('egg').Service; - * - * class User extends Service { - * constructor(ctx) { - * super(ctx); - * // 你的业务逻辑 - * } - * - * * findUser(uid) { - * return findUserFromDB(); - * } - * - * // 更多其他方法 - * } - * ``` - */ -class Service { - - /** - * @constructor - * @param {Context} ctx 上下文 - */ - constructor(ctx) { - this.ctx = ctx; - this.app = ctx.app; - } - -} - -module.exports = Service; diff --git a/lib/core/context_httpclient.js b/lib/core/context_httpclient.js new file mode 100644 index 0000000000..a1db7d26a5 --- /dev/null +++ b/lib/core/context_httpclient.js @@ -0,0 +1,26 @@ +class ContextHttpClient { + constructor(ctx) { + this.ctx = ctx; + this.app = ctx.app; + } + + /** + * http request helper base on {@link HttpClient}, it will auto save httpclient log. + * Keep the same api with {@link Application#curl}. + * + * @param {String|Object} url - request url address. + * @param {Object} [options] - options for request. + * @return {Object} see {@link Application#curl} + */ + async curl(url, options) { + options = options || {}; + options.ctx = this.ctx; + return await this.app.curl(url, options); + } + + async request(url, options) { + return await this.curl(url, options); + } +} + +module.exports = ContextHttpClient; diff --git a/lib/core/dnscache_httpclient.js b/lib/core/dnscache_httpclient.js new file mode 100644 index 0000000000..ba5e4bcb45 --- /dev/null +++ b/lib/core/dnscache_httpclient.js @@ -0,0 +1,93 @@ +const dns = require('node:dns').promises; +const LRU = require('ylru'); +const { assign } = require('utility'); +const HttpClient = require('./httpclient'); +const utils = require('./utils'); + +const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; +const DNSLOOKUP = Symbol('DNSCacheHttpClient#dnslookup'); +const UPDATE_DNS = Symbol('DNSCacheHttpClient#updateDNS'); + +class DNSCacheHttpClient extends HttpClient { + constructor(app) { + super(app); + this.dnsCacheLookupInterval = this.app.config.httpclient.dnsCacheLookupInterval; + this.dnsCache = new LRU(this.app.config.httpclient.dnsCacheMaxLength); + } + + async request(url, args) { + // disable dns cache in request by args handle + if (args && args.enableDNSCache === false) { + return await super.request(url, args); + } + const result = await this[DNSLOOKUP](url, args); + return await super.request(result.url, result.args); + } + + async [DNSLOOKUP](url, args) { + let parsed; + if (typeof url === 'string') { + parsed = utils.safeParseURL(url); + // invalid url or relative url + if (!parsed) return { url, args }; + } else { + parsed = url; + } + // hostname must exists + const hostname = parsed.hostname; + + // don't lookup when hostname is IP + if (hostname && IP_REGEX.test(hostname)) { + return { url, args }; + } + + args = args || {}; + args.headers = args.headers || {}; + // set when host header doesn't exist + if (!args.headers.host && !args.headers.Host) { + // host must combine with hostname:port, node won't use `parsed.host` + args.headers.host = parsed.port ? `${hostname}:${parsed.port}` : hostname; + } + + const record = this.dnsCache.get(hostname); + const now = Date.now(); + if (record) { + if (now - record.timestamp >= this.dnsCacheLookupInterval) { + // make sure the next request doesn't refresh dns query + record.timestamp = now; + this[UPDATE_DNS](hostname, args).catch(err => this.app.emit('error', err)); + } + + return { url: formatDnsLookupUrl(hostname, url, record.ip), args }; + } + + const address = await this[UPDATE_DNS](hostname, args); + return { url: formatDnsLookupUrl(hostname, url, address), args }; + } + + async [UPDATE_DNS](hostname, args) { + const logger = args.ctx ? args.ctx.coreLogger : this.app.coreLogger; + try { + const { address } = await dns.lookup(hostname, { family: 4 }); + logger.info('[dnscache_httpclient] dns lookup success: %s => %s', + hostname, address); + this.dnsCache.set(hostname, { timestamp: Date.now(), ip: address }); + return address; + } catch (err) { + err.message = `[dnscache_httpclient] dns lookup error: ${hostname} => ${err.message}`; + throw err; + } + } +} + +module.exports = DNSCacheHttpClient; + +function formatDnsLookupUrl(host, url, address) { + if (typeof url === 'string') return url.replace(host, address); + const urlObj = assign({}, url); + urlObj.hostname = urlObj.hostname.replace(host, address); + if (urlObj.host) { + urlObj.host = urlObj.host.replace(host, address); + } + return urlObj; +} diff --git a/lib/core/httpclient.js b/lib/core/httpclient.js new file mode 100644 index 0000000000..97b167b0e0 --- /dev/null +++ b/lib/core/httpclient.js @@ -0,0 +1,119 @@ +const Agent = require('agentkeepalive'); +const HttpsAgent = require('agentkeepalive').HttpsAgent; +const urllib = require('urllib'); +const ms = require('humanize-ms'); +const { FrameworkBaseError } = require('egg-errors'); + +class HttpClientError extends FrameworkBaseError { + get module() { + return 'httpclient'; + } +} + +class HttpClient extends urllib.HttpClient2 { + constructor(app) { + normalizeConfig(app); + const config = app.config.httpclient; + super({ + app, + defaultArgs: config.request, + agent: new Agent(config.httpAgent), + httpsAgent: new HttpsAgent(config.httpsAgent), + }); + this.app = app; + } + + async request(url, args) { + args = args || {}; + if (args.ctx && args.ctx.tracer) { + args.tracer = args.ctx.tracer; + } else { + args.tracer = args.tracer || this.app.tracer; + } + + try { + return await super.request(url, args); + } catch (err) { + if (err.code === 'ENETUNREACH') { + throw HttpClientError.create(err.message, err.code); + } + throw err; + } + } + + async curl(...args) { + return await this.request(...args); + } + + async safeCurl(url, options = {}) { + const ssrfConfig = this.app.config.security.ssrf; + if (ssrfConfig?.checkAddress) { + options.checkAddress = ssrfConfig.checkAddress; + } else { + this.app.logger.warn('[egg-security] please configure `config.security.ssrf` first'); + } + + return await this.curl(url, options); + } +} + +function normalizeConfig(app) { + const config = app.config.httpclient; + + // compatibility + if (typeof config.keepAlive === 'boolean') { + config.httpAgent.keepAlive = config.keepAlive; + config.httpsAgent.keepAlive = config.keepAlive; + } + if (config.timeout) { + config.timeout = ms(config.timeout); + config.httpAgent.timeout = config.timeout; + config.httpsAgent.timeout = config.timeout; + } + // compatibility httpclient.freeSocketKeepAliveTimeout => httpclient.freeSocketTimeout + if (config.freeSocketKeepAliveTimeout && !config.freeSocketTimeout) { + config.freeSocketTimeout = config.freeSocketKeepAliveTimeout; + delete config.freeSocketKeepAliveTimeout; + } + if (config.freeSocketTimeout) { + config.freeSocketTimeout = ms(config.freeSocketTimeout); + config.httpAgent.freeSocketTimeout = config.freeSocketTimeout; + config.httpsAgent.freeSocketTimeout = config.freeSocketTimeout; + } else { + // compatibility agent.freeSocketKeepAliveTimeout + if (config.httpAgent.freeSocketKeepAliveTimeout && !config.httpAgent.freeSocketTimeout) { + config.httpAgent.freeSocketTimeout = config.httpAgent.freeSocketKeepAliveTimeout; + delete config.httpAgent.freeSocketKeepAliveTimeout; + } + if (config.httpsAgent.freeSocketKeepAliveTimeout && !config.httpsAgent.freeSocketTimeout) { + config.httpsAgent.freeSocketTimeout = config.httpsAgent.freeSocketKeepAliveTimeout; + delete config.httpsAgent.freeSocketKeepAliveTimeout; + } + } + + if (typeof config.maxSockets === 'number') { + config.httpAgent.maxSockets = config.maxSockets; + config.httpsAgent.maxSockets = config.maxSockets; + } + if (typeof config.maxFreeSockets === 'number') { + config.httpAgent.maxFreeSockets = config.maxFreeSockets; + config.httpsAgent.maxFreeSockets = config.maxFreeSockets; + } + + if (config.httpAgent.timeout < 30000) { + app.coreLogger.warn('[egg:httpclient] config.httpclient.httpAgent.timeout(%s) can\'t below 30000, auto reset to 30000', + config.httpAgent.timeout); + config.httpAgent.timeout = 30000; + } + if (config.httpsAgent.timeout < 30000) { + app.coreLogger.warn('[egg:httpclient] config.httpclient.httpsAgent.timeout(%s) can\'t below 30000, auto reset to 30000', + config.httpsAgent.timeout); + config.httpsAgent.timeout = 30000; + } + + if (typeof config.request.timeout === 'string') { + config.request.timeout = ms(config.request.timeout); + } +} + +module.exports = HttpClient; diff --git a/lib/core/httpclient_next.js b/lib/core/httpclient_next.js new file mode 100644 index 0000000000..cdc1a7d404 --- /dev/null +++ b/lib/core/httpclient_next.js @@ -0,0 +1,80 @@ +const debug = require('util').debuglog('egg:lib:core:httpclient_next'); +const ms = require('humanize-ms'); + +const SSRF_HTTPCLIENT = Symbol('SSRF_HTTPCLIENT'); + +const mainNodejsVersion = parseInt(process.versions.node.split('.')[0]); +let HttpClient; +if (mainNodejsVersion >= 18) { + // urllib@4 only works on Node.js >= 18 + try { + HttpClient = require('urllib4').HttpClient; + debug('urllib4 enable'); + } catch (err) { + debug('require urllib4 error: %s', err); + } +} +if (!HttpClient) { + // fallback to urllib@3 + HttpClient = require('urllib-next').HttpClient; + debug('urllib3 enable'); +} + +class HttpClientNext extends HttpClient { + constructor(app, options) { + normalizeConfig(app); + options = options || {}; + options = { + ...app.config.httpclient, + ...options, + }; + super({ + app, + defaultArgs: options.request, + allowH2: options.allowH2, + // use on egg-security ssrf + // https://github.com/eggjs/egg-security/blob/master/lib/extend/safe_curl.js#L11 + checkAddress: options.checkAddress, + connect: options.connect, + }); + this.app = app; + } + + async request(url, options) { + options = options || {}; + if (options.ctx && options.ctx.tracer) { + options.tracer = options.ctx.tracer; + } else { + options.tracer = options.tracer || this.app.tracer; + } + return await super.request(url, options); + } + + async curl(...args) { + return await this.request(...args); + } + + async safeCurl(url, options = {}) { + if (!this[SSRF_HTTPCLIENT]) { + const ssrfConfig = this.app.config.security.ssrf; + if (ssrfConfig?.checkAddress) { + options.checkAddress = ssrfConfig.checkAddress; + } else { + this.app.logger.warn('[egg-security] please configure `config.security.ssrf` first'); + } + this[SSRF_HTTPCLIENT] = new HttpClientNext(this.app, { + checkAddress: ssrfConfig.checkAddress, + }); + } + return await this[SSRF_HTTPCLIENT].request(url, options); + } +} + +function normalizeConfig(app) { + const config = app.config.httpclient; + if (typeof config.request.timeout === 'string') { + config.request.timeout = ms(config.request.timeout); + } +} + +module.exports = HttpClientNext; diff --git a/lib/core/keygrip.js b/lib/core/keygrip.js deleted file mode 100644 index ab5ff00c7f..0000000000 --- a/lib/core/keygrip.js +++ /dev/null @@ -1,147 +0,0 @@ -// add encrypt and decrypt for Keygrip -// patch from https://github.com/crypto-utils/keygrip - -/* jshint ignore:start */ -/* eslint-disable */ - -var debug = require('debug')('egg:cookie:keygrip'); -var crypto = require('crypto'); -var constantTimeCompare = require('scmp') - -module.exports = Keygrip - -function Keygrip(keys) { - if (arguments.length > 1) { - console.warn('as of v2, keygrip() only accepts a single argument.') - console.warn('set keygrip().hash= instead.') - console.warn('keygrip() also now only supports buffers.') - } - - if (!Array.isArray(keys) || !keys.length) throw new Error("Keys must be provided.") - if (!(this instanceof Keygrip)) return new Keygrip(keys) - - this.keys = keys -} - -/** - * Allow setting `keygrip.hash = 'sha1'` - * or `keygrip.cipher = 'aes256'` - * with validation instead of always doing `keygrip([], alg, enc)`. - * This also allows for easier defaults. - */ - -Keygrip.prototype = { - constructor: Keygrip, - - get hash() { - return this._hash - }, - - set hash(val) { - // if (!util.supportedHash(val)) - // throw new Error('unsupported hash algorithm: ' + val) - this._hash = val - }, - - get cipher() { - return this._cipher - }, - - set cipher(val) { - // if (!util.supportedCipher(val)) - // throw new Error('unsupported cipher: ' + val) - this._cipher = val - }, - - // defaults - _hash: 'sha256', - _cipher: 'aes-256-cbc', -} - -// encrypt a message -Keygrip.prototype.encrypt = function encrypt(data, iv, key, encoding) { - key = key || this.keys[0] - - var cipher = iv - ? crypto.createCipheriv(this.cipher, key, iv) - : crypto.createCipher(this.cipher, key) - - return crypt(cipher, data, encoding) -} - -// decrypt a single message -// returns false on bad decrypts -Keygrip.prototype.decrypt = function decrypt(data, iv, key, encoding) { - if (!key) { - // decrypt every key - var keys = this.keys - for (var i = 0, l = keys.length; i < l; i++) { - var message = this.decrypt(data, iv, keys[i], encoding) - if (message !== false) return [message, i] - } - - return false - } - - try { - var cipher = iv - ? crypto.createDecipheriv(this.cipher, key, iv) - : crypto.createDecipher(this.cipher, key) - return crypt(cipher, data, encoding) - } catch (err) { - debug(err.stack) - return false - } -} - -// message signing -Keygrip.prototype.sign = function sign(data, key) { - // default to the first key - key = key || this.keys[0] - - return crypto - .createHmac(this.hash, key) - .update(data) - .digest('base64') - .replace(/\/|\+|=/g, function(x) { - return ({ "/": "_", "+": "-", "=": "" })[x] - }) -} - -Keygrip.prototype.verify = function verify(data, digest) { - return this.indexOf(data, digest) > -1 -} - -Keygrip.prototype.index = -Keygrip.prototype.indexOf = function Keygrip$_index(data, digest) { - var keys = this.keys - for (var i = 0, l = keys.length; i < l; i++) { - if (constantTimeCompare(digest, this.sign(data, keys[i]))) return i - } - - return -1 -} - -Keygrip.encrypt = -Keygrip.decrypt = -Keygrip.sign = -Keygrip.verify = -Keygrip.index = -Keygrip.indexOf = function() { - throw new Error("Usage: require('keygrip')()") -} - -function crypt(cipher, data, encoding) { - var text = cipher.update(data, encoding || 'utf8'); - var pad = cipher.final(); - - if (typeof text === 'string') { - // cipher output binary strings (Node.js <= 0.8) - text = new Buffer(text, 'binary'); - pad = new Buffer(pad, 'binary'); - } - - return Buffer.concat([text, pad]); -} - -/* jshint ignore:end */ diff --git a/lib/core/logger.js b/lib/core/logger.js index 32cc4fb639..a38dbddcf5 100644 --- a/lib/core/logger.js +++ b/lib/core/logger.js @@ -1,21 +1,34 @@ -'use strict'; - -const Loggers = require('egg-logger').EggLoggers; +const { EggLoggers } = require('egg-logger'); +const { setCustomLogger } = require('onelogger'); module.exports = function createLoggers(app) { const loggerConfig = app.config.logger; loggerConfig.type = app.type; + loggerConfig.localStorage = app.ctxStorage; - // prod 环境强制配置 INFO - if (app.config.env === 'prod' && loggerConfig.level === 'DEBUG') { + if (app.config.env === 'prod' && loggerConfig.level === 'DEBUG' && !loggerConfig.allowDebugAtProd) { loggerConfig.level = 'INFO'; } - const loggers = new Loggers(app.config); + const loggers = new EggLoggers(app.config); + + // won't print to console after started, except for local and unittest + app.ready(() => { + if (loggerConfig.disableConsoleAfterReady) { + loggers.disableConsole(); + } + }); - // 启动成功了,所有日志不输出到终端, - // 除本地环境,本地环境还是可以根据 consoleLevel 控制日志 - app.ready(() => app.config.env !== 'local' && loggers.disableConsole()); + // set global logger + for (const loggerName of Object.keys(loggers)) { + setCustomLogger(loggerName, loggers[loggerName]); + } + // reset global logger on beforeClose hook + app.beforeClose(() => { + for (const loggerName of Object.keys(loggers)) { + setCustomLogger(loggerName, undefined); + } + }); loggers.coreLogger.info('[egg:logger] init all loggers with options: %j', loggerConfig); return loggers; diff --git a/lib/core/messenger/index.js b/lib/core/messenger/index.js new file mode 100644 index 0000000000..f819cfa917 --- /dev/null +++ b/lib/core/messenger/index.js @@ -0,0 +1,14 @@ +'use strict'; + +const LocalMessenger = require('./local'); +const IPCMessenger = require('./ipc'); + +/** + * @class Messenger + */ + +exports.create = egg => { + return egg.options.mode === 'single' + ? new LocalMessenger(egg) + : new IPCMessenger(egg); +}; diff --git a/lib/core/messenger.js b/lib/core/messenger/ipc.js similarity index 52% rename from lib/core/messenger.js rename to lib/core/messenger/ipc.js index 20f3b7113b..e758d6dd2c 100644 --- a/lib/core/messenger.js +++ b/lib/core/messenger/ipc.js @@ -1,48 +1,51 @@ 'use strict'; -const debug = require('debug')('egg:util:messenger'); +const debug = require('util').debuglog('egg:util:messenger:ipc'); const is = require('is-type-of'); +const workerThreads = require('worker_threads'); const sendmessage = require('sendmessage'); const EventEmitter = require('events'); +/** + * Communication between app worker and agent worker by IPC channel + */ class Messenger extends EventEmitter { + constructor() { super(); this.pid = String(process.pid); - // app/agent 对方的进程,app.messenger.opids 就是 agent 的 pid + // pids of agent or app managed by master + // - retrieve app worker pids when it's an agent worker + // - retrieve agent worker pids when it's an app worker this.opids = []; this.on('egg-pids', pids => { this.opids = pids; }); this._onMessage = this._onMessage.bind(this); process.on('message', this._onMessage); + if (!workerThreads.isMainThread) { + workerThreads.parentPort.on('message', this._onMessage); + } } /** - * 发送 message,广播 - * @param {String} action 消息动作唯一标识 - * @param {Object} data 广播的数据。 + * Send message to all agent and app + * @param {String} action - message key + * @param {Object} data - message value * @return {Messenger} this */ broadcast(action, data) { debug('[%s] broadcast %s with %j', this.pid, action, data); - sendmessage(process, { - action, - data, - }); - this.emit(action, data); + this.send(action, data, 'app'); + this.send(action, data, 'agent'); return this; } - send(action, data) { - return this.broadcast(action, data); - } - /** - * 发送给指定的进程 - * @param {String} pid 接收者进程 id - * @param {String} action 消息动作唯一标识 - * @param {data} data 发送的消息数据。 + * send message to the specified process + * @param {String} pid - the process id of the receiver + * @param {String} action - message key + * @param {Object} data - message value * @return {Messenger} this */ sendTo(pid, action, data) { @@ -52,19 +55,19 @@ class Messenger extends EventEmitter { data, receiverPid: String(pid), }); - this.emit(action, data); return this; } /** - * 随机找一个进程发送 - * - 如果在 app,直接发给 agent - * - 如果在 agent,会随机挑选一个 app 进程发送 - * @param {String} action 消息动作唯一标识 - * @param {data} data 发送的消息数据。 + * send message to one app worker by random + * - if it's running in agent, it will send to one of app workers + * - if it's running in app, it will send to agent + * @param {String} action - message key + * @param {Object} data - message value * @return {Messenger} this */ sendRandom(action, data) { + /* istanbul ignore if */ if (!this.opids.length) return this; const pid = random(this.opids); this.sendTo(String(pid), action, data); @@ -72,46 +75,44 @@ class Messenger extends EventEmitter { } /** - * 发送消息给所有的 app 进程(agent 和 app 上都可以调用) - * @param {String} action 消息动作唯一标识 - * @param {data} data 发送的消息数据。 + * send message to app + * @param {String} action - message key + * @param {Object} data - message value * @return {Messenger} this */ sendToApp(action, data) { debug('[%s] send %s with %j to all app', this.pid, action, data); - sendmessage(process, { - action, - data, - to: 'app', - }); + this.send(action, data, 'app'); return this; } /** - * 发送消息给所有的 agent 进程(agent 和 app 上都可以调用) - * 在 worker 上调用时和 `send` 方法不同的是,不会同时在调用方触发该事件 - * @param {String} action 消息动作唯一标识 - * @param {data} data 发送的消息数据。 + * send message to agent + * @param {String} action - message key + * @param {Object} data - message value * @return {Messenger} this */ sendToAgent(action, data) { debug('[%s] send %s with %j to all agent', this.pid, action, data); + this.send(action, data, 'agent'); + return this; + } + + /** + * @param {String} action - message key + * @param {Object} data - message value + * @param {String} to - let master know how to send message + * @return {Messenger} this + */ + send(action, data, to) { sendmessage(process, { action, data, - to: 'agent', + to, }); return this; } - /** - * 处理 message 事件 - * @method Messenger#on - * @param {Object} message 只处理符合格式的 message - * - {String} action - * - {Object} data - * @return {void} - */ _onMessage(message) { if (message && is.string(message.action)) { debug('[%s] got message %s with %j, receiverPid: %s', @@ -124,6 +125,12 @@ class Messenger extends EventEmitter { process.removeListener('message', this._onMessage); this.removeAllListeners(); } + + /** + * @function Messenger#on + * @param {String} action - message key + * @param {Object} data - message value + */ } module.exports = Messenger; diff --git a/lib/core/messenger/local.js b/lib/core/messenger/local.js new file mode 100644 index 0000000000..7be992e0ee --- /dev/null +++ b/lib/core/messenger/local.js @@ -0,0 +1,141 @@ +'use strict'; + +const debug = require('util').debuglog('egg:util:messenger:local'); +const is = require('is-type-of'); +const EventEmitter = require('events'); + +/** + * Communication between app worker and agent worker with EventEmitter + */ +class Messenger extends EventEmitter { + + constructor(egg) { + super(); + this.egg = egg; + } + + /** + * Send message to all agent and app + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + broadcast(action, data) { + debug('[%s] broadcast %s with %j', this.pid, action, data); + this.send(action, data, 'both'); + return this; + } + + /** + * send message to the specified process + * Notice: in single process mode, it only can send to self process, + * and it will send to both agent and app's messengers. + * @param {String} pid - the process id of the receiver + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendTo(pid, action, data) { + debug('[%s] send %s with %j to %s', this.pid, action, data, pid); + if (pid !== process.pid) return this; + this.send(action, data, 'both'); + return this; + } + + /** + * send message to one worker by random + * Notice: in single process mode, we only start one agent worker and one app worker + * - if it's running in agent, it will send to one of app workers + * - if it's running in app, it will send to agent + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendRandom(action, data) { + debug('[%s] send %s with %j to opposite', this.pid, action, data); + this.send(action, data, 'opposite'); + return this; + } + + /** + * send message to app + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToApp(action, data) { + debug('[%s] send %s with %j to all app', this.pid, action, data); + this.send(action, data, 'application'); + return this; + } + + /** + * send message to agent + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToAgent(action, data) { + debug('[%s] send %s with %j to all agent', this.pid, action, data); + this.send(action, data, 'agent'); + return this; + } + + /** + * @param {String} action - message key + * @param {Object} data - message value + * @param {String} to - let master know how to send message + * @return {Messenger} this + */ + send(action, data, to) { + // use nextTick to keep it async as IPC messenger + process.nextTick(() => { + const { egg } = this; + let application; + let agent; + let opposite; + + if (egg.type === 'application') { + application = egg; + agent = egg.agent; + opposite = agent; + } else { + agent = egg; + application = egg.application; + opposite = application; + } + if (!to) to = egg.type === 'application' ? 'agent' : 'application'; + + if (application && application.messenger && (to === 'application' || to === 'both')) { + application.messenger._onMessage({ action, data }); + } + if (agent && agent.messenger && (to === 'agent' || to === 'both')) { + agent.messenger._onMessage({ action, data }); + } + if (opposite && opposite.messenger && to === 'opposite') { + opposite.messenger._onMessage({ action, data }); + } + }); + + return this; + } + + _onMessage(message) { + if (message && is.string(message.action)) { + debug('[%s] got message %s with %j', this.pid, message.action, message.data); + this.emit(message.action, message.data); + } + } + + close() { + this.removeAllListeners(); + } + + /** + * @function Messenger#on + * @param {String} action - message key + * @param {Object} data - message value + */ +} + +module.exports = Messenger; diff --git a/lib/core/singleton.js b/lib/core/singleton.js index fb1453513c..d0b9c29e27 100644 --- a/lib/core/singleton.js +++ b/lib/core/singleton.js @@ -1,10 +1,10 @@ 'use strict'; const assert = require('assert'); +const is = require('is-type-of'); class Singleton { - constructor(options) { - options = options || {}; + constructor(options = {}) { assert(options.name, '[egg:singleton] Singleton#constructor options.name is required'); assert(options.app, '[egg:singleton] Singleton#constructor options.app is required'); assert(options.create, '[egg:singleton] Singleton#constructor options.create is required'); @@ -13,29 +13,60 @@ class Singleton { this.app = options.app; this.name = options.name; this.create = options.create; + /* istanbul ignore next */ this.options = options.app.config[this.name] || {}; } init() { + return is.asyncFunction(this.create) ? this.initAsync() : this.initSync(); + } + + initSync() { const options = this.options; - if (options.client && options.clients) { - throw new Error(`eggg:singleton ${this.name} can not set options.client and options.clients both`); + assert(!(options.client && options.clients), + `egg:singleton ${this.name} can not set options.client and options.clients both`); + + // alias app[name] as client, but still support createInstance method + if (options.client) { + const client = this.createInstance(options.client, options.name); + this.app[this.name] = client; + this._extendDynamicMethods(client); + return; + } + + // multi client, use app[name].getInstance(id) + if (options.clients) { + Object.keys(options.clients).forEach(id => { + const client = this.createInstance(options.clients[id], id); + this.clients.set(id, client); + }); + this.app[this.name] = this; + return; } + // no config.clients and config.client + this.app[this.name] = this; + } + + async initAsync() { + const options = this.options; + assert(!(options.client && options.clients), + `egg:singleton ${this.name} can not set options.client and options.clients both`); + // alias app[name] as client, but still support createInstance method if (options.client) { - const client = this.createInstance(options.client); + const client = await this.createInstanceAsync(options.client, options.name); this.app[this.name] = client; - assert(!client.createInstance, 'singleton instance should not have createInstance method'); - client.createInstance = this.createInstance.bind(this); + this._extendDynamicMethods(client); return; } - // multi clent, use app[name].getInstance(id) + // multi client, use app[name].getInstance(id) if (options.clients) { - for (const id in options.clients) { - this.clients.set(id, this.createInstance(options.clients[id])); - } + await Promise.all(Object.keys(options.clients).map(id => { + return this.createInstanceAsync(options.clients[id], id) + .then(client => this.clients.set(id, client)); + })); this.app[this.name] = this; return; } @@ -48,10 +79,42 @@ class Singleton { return this.clients.get(id); } - createInstance(config) { + // alias to `get(id)` + getSingletonInstance(id) { + return this.clients.get(id); + } + + createInstance(config, clientName) { + // async creator only support createInstanceAsync + assert(!is.asyncFunction(this.create), + `egg:singleton ${this.name} only support create asynchronous, please use createInstanceAsync`); // options.default will be merge in to options.clients[id] config = Object.assign({}, this.options.default, config); - return this.create(config, this.app); + return this.create(config, this.app, clientName); + } + + async createInstanceAsync(config, clientName) { + // options.default will be merge in to options.clients[id] + config = Object.assign({}, this.options.default, config); + return await this.create(config, this.app, clientName); + } + + _extendDynamicMethods(client) { + assert(!client.createInstance, 'singleton instance should not have createInstance method'); + assert(!client.createInstanceAsync, 'singleton instance should not have createInstanceAsync method'); + + try { + let extendable = client; + // Object.preventExtensions() or Object.freeze() + if (!Object.isExtensible(client) || Object.isFrozen(client)) { + // eslint-disable-next-line no-proto + extendable = client.__proto__ || client; + } + extendable.createInstance = this.createInstance.bind(this); + extendable.createInstanceAsync = this.createInstanceAsync.bind(this); + } catch (err) { + this.app.logger.warn('egg:singleton %s dynamic create is disabled because of client is unextensible', this.name); + } } } diff --git a/lib/core/urllib.js b/lib/core/urllib.js deleted file mode 100644 index dd24a1349b..0000000000 --- a/lib/core/urllib.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const Agent = require('agentkeepalive'); -const HttpsAgent = require('agentkeepalive').HttpsAgent; -const urllib = require('urllib'); - -module.exports = app => urllib.create({ - agent: new Agent(app.config.urllib), - httpsAgent: new HttpsAgent(app.config.urllib), -}); diff --git a/lib/core/util.js b/lib/core/util.js deleted file mode 100644 index c133919759..0000000000 --- a/lib/core/util.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -/** - * 类似 Object.assign - * @param {Object} target - assign 的目标对象 - * @param {Object | Array} objects - assign 的源,可以是一个 object 也可以是一个数组 - * @return {Object} - 返回 target - */ -exports.assign = function(target, objects) { - if (!Array.isArray(objects)) { - objects = [ objects ]; - } - - for (let i = 0; i < objects.length; i++) { - const obj = objects[i]; - if (obj) { - const keys = Object.keys(obj); - for (let j = 0; j < keys.length; j++) { - const key = keys[j]; - target[key] = obj[key]; - } - } - } - return target; -}; diff --git a/lib/core/utils.js b/lib/core/utils.js new file mode 100644 index 0000000000..6f6a5f70fd --- /dev/null +++ b/lib/core/utils.js @@ -0,0 +1,92 @@ +'use strict'; + +const util = require('util'); +const is = require('is-type-of'); +const URL = require('url').URL; + +module.exports = { + convertObject, + safeParseURL, +}; + +function convertObject(obj, ignore, ignoreKeyPaths) { + if (!is.array(ignore)) { + ignore = [ ignore ]; + } + if (!is.array(ignoreKeyPaths)) { + ignoreKeyPaths = ignoreKeyPaths ? [ ignoreKeyPaths ] : []; + } + _convertObject(obj, ignore, ignoreKeyPaths, ''); +} + +function _convertObject(obj, ignore, ignoreKeyPaths, keyPath) { + for (const key of Object.keys(obj)) { + obj[key] = convertValue(key, obj[key], ignore, ignoreKeyPaths, keyPath ? `${keyPath}.${key}` : key); + } + return obj; +} + +function convertValue(key, value, ignore, ignoreKeyPaths, keyPath) { + if (is.nullOrUndefined(value)) return value; + + let hit = false; + let hitKeyPath = false; + for (const matchKey of ignore) { + if (is.string(matchKey) && matchKey === key) { + hit = true; + break; + } else if (is.regExp(matchKey) && matchKey.test(key)) { + hit = true; + break; + } + } + for (const matchKeyPath of ignoreKeyPaths) { + if (is.string(matchKeyPath) && keyPath === matchKeyPath) { + hitKeyPath = true; + break; + } + } + if (!hit && !hitKeyPath) { + if (is.symbol(value) || is.regExp(value)) return value.toString(); + if (is.primitive(value) || is.array(value)) return value; + } + + // only convert recursively when it's a plain object, + // o = {} + if (Object.getPrototypeOf(value) === Object.prototype) { + if (hitKeyPath) { + return ''; + } + return _convertObject(value, ignore, ignoreKeyPaths, keyPath); + } + + // support class + const name = value.name || 'anonymous'; + if (is.class(value)) { + return ``; + } + + // support generator function + if (is.function(value)) { + if (is.generatorFunction(value)) return ``; + if (is.asyncFunction(value)) return ``; + return ``; + } + + const typeName = value.constructor.name; + if (typeName) { + if (is.buffer(value) || is.string(value)) return `<${typeName} len: ${value.length}>`; + return `<${typeName}>`; + } + + /* istanbul ignore next */ + return util.format(value); +} + +function safeParseURL(url) { + try { + return new URL(url); + } catch (err) { + return null; + } +} diff --git a/lib/core/view.js b/lib/core/view.js deleted file mode 100644 index 76feb74d23..0000000000 --- a/lib/core/view.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -const util = require('./util'); - -module.exports = function(ViewClass) { - - /** - * 封装 view 的通用逻辑, 继承于框架提供的 View 类 - * - * 框架的 view 插件, 需要提供 render 和 renderString 的实现, 并注入到 Application - * - * @class Application#View - * - * @example - * ```js - * // lib/xx.js - * const egg = require('egg'); - * - * class NunjucksView { - * render(name, locals) { - * return Promise.resolve('some html'); - * } - * - * renderString(tpl, locals) { - * return Promise.resolve('some html'); - * } - * - * // view.helper 将注入到 locals.helper 上, 需 view 插件如下设置 getter - * get helper() { - * return this.ctx.helper; - * } - * } - * - * class XxApplication extends egg.Application { - * get [Symbol.for('egg#view')]() { - * return NunjucksView; - * } - * } - * ``` - */ - class View extends ViewClass { - constructor(ctx) { - super(ctx); - this.ctx = ctx; - this.app = ctx.app; - } - - /** - * 渲染页面模板,返回渲染后的字符串 - * @method View#render - * @param {String} name 模板文件名 - * @param {Object} [locals] 需要放到页面上的变量 - * @return {Promise} 渲染结果 - */ - render(name, locals) { - locals = this.setLocals(locals); - return super.render(name, locals); - } - - /** - * 渲染模板字符串 - * @method View#renderString - * @param {String} tpl 模板字符串 - * @param {Object} [locals] 需要放到页面上的变量 - * @return {Promise} 渲染结果 - */ - renderString(tpl, locals) { - locals = this.setLocals(locals); - return super.renderString(tpl, locals); - } - - /** - * 设置 locals, 合并 ctx.locals, 并注入 ctx/helper/request - * @param {Object} locals 数据对象 - * @return {Object} 返回新的 locals, 并不会修改入参对象 - * @private - */ - setLocals(locals) { - return util.assign({ - ctx: this.ctx, - request: this.ctx.request, - helper: this.helper, - }, [ this.ctx.locals, locals ]); - } - } - - return View; -}; diff --git a/lib/egg.js b/lib/egg.js index 06bbc76493..55d9fbd63f 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -1,52 +1,183 @@ -'use strict'; - +const { performance } = require('perf_hooks'); const path = require('path'); const fs = require('fs'); +const ms = require('ms'); +const http = require('http'); const EggCore = require('egg-core').EggCore; +const cluster = require('cluster-client'); +const extend = require('extend2'); +const ContextLogger = require('egg-logger').EggContextLogger; +const ContextCookies = require('egg-cookies'); +const CircularJSON = require('circular-json-for-egg'); +const ContextHttpClient = require('./core/context_httpclient'); const Messenger = require('./core/messenger'); -const urllib = require('./core/urllib'); +const DNSCacheHttpClient = require('./core/dnscache_httpclient'); +const HttpClient = require('./core/httpclient'); +const HttpClientNext = require('./core/httpclient_next'); const createLoggers = require('./core/logger'); +const Singleton = require('./core/singleton'); +const utils = require('./core/utils'); +const BaseContextClass = require('./core/base_context_class'); +const BaseHookClass = require('./core/base_hook_class'); -const URLLIB = Symbol('EggApplication#urllib'); +const HTTPCLIENT = Symbol('EggApplication#httpclient'); const LOGGERS = Symbol('EggApplication#loggers'); const EGG_PATH = Symbol.for('egg#eggPath'); +const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); /** - * Base on koa's Application + * Based on koa's Application + * @see https://github.com/eggjs/egg-core * @see http://koajs.com/#application + * @augments EggCore */ class EggApplication extends EggCore { /** - * @constructor - * @param {Object} options - 创建应用配置 + * @class + * @param {Object} options + * - {Object} [type] - type of instance, Agent and Application both extend koa, type can determine what it is. * - {String} [baseDir] - app root dir, default is `process.cwd()` - * - {Object} [plugins] - 自定义插件配置,一般只用于单元测试 + * - {Object} [plugins] - custom plugin config, use it in unittest + * - {String} [mode] - process mode, can be cluster / single, default is `cluster` */ - constructor(options) { + constructor(options = {}) { + options.mode = options.mode || 'cluster'; super(options); + // export context base classes, let framework can impl sub class and over context extend easily. + this.ContextCookies = ContextCookies; + this.ContextLogger = ContextLogger; + this.ContextHttpClient = ContextHttpClient; + this.HttpClient = HttpClient; + this.HttpClientNext = HttpClientNext; + this.loader.loadConfig(); - // agent 和 worker 通信 - this.messenger = new Messenger(); + /** + * messenger instance + * @member {Messenger} + * @since 1.0.0 + */ + this.messenger = Messenger.create(this); + + // trigger `serverDidReady` hook when all the app workers + // and agent worker are ready + this.messenger.once('egg-ready', () => { + this.lifecycle.triggerServerDidReady(); + }); - this.dumpConfig(); + // dump config after ready, ensure all the modifications during start will be recorded + // make sure dumpConfig is the last ready callback + this.ready(() => process.nextTick(() => { + const dumpStartTime = Date.now(); + this.dumpConfig(); + this.dumpTiming(); + this.coreLogger.info('[egg:core] dump config after ready, %s', ms(Date.now() - dumpStartTime)); + })); this._setupTimeoutTimer(); - // 开始启动 this.console.info('[egg:core] App root: %s', this.baseDir); this.console.info('[egg:core] All *.log files save on %j', this.config.logger.dir); this.console.info('[egg:core] Loaded enabled plugin %j', this.loader.orderPlugins); - // 记录未处理的 promise reject - // 每个进程调用一次即可 + // Listen the error that promise had not catch, then log it in common-error this._unhandledRejectionHandler = this._unhandledRejectionHandler.bind(this); process.on('unhandledRejection', this._unhandledRejectionHandler); + + this[CLUSTER_CLIENTS] = []; + + /** + * Wrap the Client with Leader/Follower Pattern + * + * @description almost the same as Agent.cluster API, the only different is that this method create Follower. + * + * @see https://github.com/node-modules/cluster-client + * @param {Function} clientClass - client class function + * @param {Object} [options] + * - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true + * - {Function} [formatKey] - a method to tranform the subscription info into a string,default is JSON.stringify + * - {Object} [transcode|JSON.stringify/parse] + * - {Function} encode - custom serialize method + * - {Function} decode - custom deserialize method + * - {Boolean} [isBroadcast] - whether broadcast subscrption result to all followers or just one, default is true + * - {Number} [responseTimeout] - response timeout, default is 3 seconds + * - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds + * @return {ClientWrapper} wrapper + */ + this.cluster = (clientClass, options) => { + options = Object.assign({}, this.config.clusterClient, options, { + singleMode: this.options.mode === 'single', + // cluster need a port that can't conflict on the environment + port: this.options.clusterPort, + // agent worker is leader, app workers are follower + isLeader: this.type === 'agent', + logger: this.coreLogger, + // debug mode does not check heartbeat + isCheckHeartbeat: this.config.env === 'prod' ? true : require('inspector').url() === undefined, + }); + const client = cluster(clientClass, options); + this._patchClusterClient(client); + return client; + }; + + // register close function + this.beforeClose(async () => { + // single process mode will close agent before app close + if (this.type === 'application' && this.options.mode === 'single') { + await this.agent.close(); + } + + for (const logger of this.loggers.values()) { + logger.close(); + } + this.messenger.close(); + process.removeListener('unhandledRejection', this._unhandledRejectionHandler); + }); + + /** + * Retreive base context class + * @member {BaseContextClass} BaseContextClass + * @since 1.0.0 + */ + this.BaseContextClass = BaseContextClass; + + /** + * Retreive base controller + * @member {Controller} Controller + * @since 1.0.0 + */ + this.Controller = BaseContextClass; + + /** + * Retreive base service + * @member {Service} Service + * @since 1.0.0 + */ + this.Service = BaseContextClass; + + /** + * Retreive base subscription + * @member {Subscription} Subscription + * @since 2.12.0 + */ + this.Subscription = BaseContextClass; + + /** + * Retreive base context class + * @member {BaseHookClass} BaseHookClass + */ + this.BaseHookClass = BaseHookClass; + + /** + * Retreive base boot + * @member {Boot} + */ + this.Boot = BaseHookClass; } /** - * console.log(app) 的时候给出更准确的 app 信息 + * print the information when console.log(app) * @return {Object} inspected app. * @since 1.0.0 * @example @@ -67,10 +198,13 @@ class EggApplication extends EggCore { * ``` */ inspect() { - const res = {}; + const res = { + env: this.config.env, + }; function delegate(res, app, keys) { for (const key of keys) { + /* istanbul ignore else */ if (app[key]) { res[key] = app[key]; } @@ -79,6 +213,7 @@ class EggApplication extends EggCore { function abbr(res, app, keys) { for (const key of keys) { + /* istanbul ignore else */ if (app[key]) { res[key] = ``; } @@ -88,25 +223,32 @@ class EggApplication extends EggCore { delegate(res, this, [ 'name', 'baseDir', - 'env', 'subdomainOffset', ]); abbr(res, this, [ 'config', 'controller', - 'serviceClasses', - 'middlewares', - 'urllib', - 'tair', - 'hsf', + 'httpclient', 'loggers', + 'middlewares', + 'router', + 'serviceClasses', ]); + return res; } + toJSON() { + return this.inspect(); + } + /** - * 对 {@link urllib} 的封装,会自动记录调用日志,参数跟 `urllib.request(url, args)` 保持一致. + * http request helper base on {@link httpclient}, it will auto save httpclient log. + * Keep the same api with `httpclient.request(url, args)`. + * + * See https://github.com/node-modules/urllib#api-doc for more details. + * * @param {String} url request url address. * @param {Object} opts * - method {String} - Request method, defaults to GET. Could be GET, POST, DELETE or PUT. Alias 'type'. @@ -122,41 +264,66 @@ class EggApplication extends EggCore { * - auth {String} - `username:password` used in HTTP Basic Authorization. * - followRedirect {Boolean} - follow HTTP 3xx responses as redirects. defaults to false. * - gzip {Boolean} - let you get the res object when request connected, default false. alias customResponse + * - nestedQuerystring {Boolean} - urllib default use querystring to stringify form data which don't + * support nested object, will use qs instead of querystring to support nested object by set this option to true. + * - more options see https://www.npmjs.com/package/urllib + * @return {Object} + * - status {Number} - HTTP response status + * - headers {Object} - HTTP response seaders + * - res {Object} - HTTP response meta + * - data {Object} - HTTP response body * - * 更多参数请访问: https://github.com/node-modules/urllib#api-doc - * @return {Object} - - * - status {Number} - HTTP Response Status - * - headers {Object} - HTTP Response Headers - * - res {Object} - HTTP Response Object - * - data {Object} * @example * ```js - * yield app.curl('http://example.com/foo.json', { + * const result = await app.curl('http://example.com/foo.json', { * method: 'GET', - * dataType: 'json + * dataType: 'json', * }); + * console.log(result.status, result.headers, result.data); * ``` */ - * curl(url, opts) { - return yield this.urllib.request(url, opts); + async curl(url, opts) { + return await this.httpclient.request(url, opts); } /** - * 需要统一记录 httpclient log, app 也必须使用相同的 - * 参考: [urllib](https://npmjs.com/package/urllib) - * @member {Urllib} + * Create a new HttpClient instance with custom options + * @param {Object} [options] HttpClient init options */ - get urllib() { - if (!this[URLLIB]) { - this[URLLIB] = urllib(this); + createHttpClient(options) { + let httpClient; + if (this.config.httpclient.useHttpClientNext || this.config.httpclient.allowH2) { + httpClient = new this.HttpClientNext(this, options); + } else if (this.config.httpclient.enableDNSCache) { + httpClient = new DNSCacheHttpClient(this, options); + } else { + httpClient = new this.HttpClient(this, options); } - return this[URLLIB]; + return httpClient; + } + + /** + * HttpClient instance + * @see https://github.com/node-modules/urllib + * @member {HttpClient} + */ + get httpclient() { + if (!this[HTTPCLIENT]) { + this[HTTPCLIENT] = this.createHttpClient(); + } + return this[HTTPCLIENT]; + } + + /** + * @alias httpclient + * @member {HttpClient} + */ + get httpClient() { + return this.httpclient; } /** - * logger 集合,包含两个: - * - 应用使用:loggers.logger - * - 框架使用:loggers.coreLogger + * All loggers contain logger, coreLogger and customLogger * @member {Object} * @since 1.0.0 */ @@ -168,27 +335,46 @@ class EggApplication extends EggCore { } /** - * 同 {@link Agent#coreLogger} 相同 + * Get logger by name, it's equal to app.loggers['name'], + * but you can extend it with your own logical. + * @param {String} name - logger name + * @return {Logger} logger + */ + getLogger(name) { + return this.loggers[name] || null; + } + + /** + * application logger, log file is `$HOME/logs/{appname}/{appname}-web` * @member {Logger} * @since 1.0.0 */ get logger() { - return this.loggers.logger; + return this.getLogger('logger'); } /** - * agent 的 logger,日志生成到 $HOME/logs/${agentLogName} + * core logger for framework and plugins, log file is `$HOME/logs/{appname}/egg-web` * @member {Logger} * @since 1.0.0 */ get coreLogger() { - return this.loggers.coreLogger; + return this.getLogger('coreLogger'); } _unhandledRejectionHandler(err) { if (!(err instanceof Error)) { - err = new Error(String(err)); + const newError = new Error(String(err)); + // err maybe an object, try to copy the name, message and stack to the new error instance + /* istanbul ignore else */ + if (err) { + if (err.name) newError.name = err.name; + if (err.message) newError.message = err.message; + if (err.stack) newError.stack = err.stack; + } + err = newError; } + /* istanbul ignore else */ if (err.name === 'Error') { err.name = 'unhandledRejectionError'; } @@ -196,20 +382,78 @@ class EggApplication extends EggCore { } /** - * 将 app.config 保存到 run/${type}_config.json 便于排查 + * dump out the config and meta object + * @private + * @return {Object} the result + */ + dumpConfigToObject() { + let ignoreList; + try { + // support array and set + ignoreList = Array.from(this.config.dump.ignore); + } catch (_) { + ignoreList = []; + } + + let ignoreKeyPaths; + try { + ignoreKeyPaths = this.config.dump.ignoreKeyPaths; + } catch (e) { + ignoreKeyPaths = {}; + } + + const json = extend(true, {}, { config: this.config, plugins: this.loader.allPlugins, appInfo: this.loader.appInfo }); + utils.convertObject(json, ignoreList, ignoreKeyPaths ? Object.keys(ignoreKeyPaths) : []); + return { + config: json, + meta: this.loader.configMeta, + }; + } + + /** + * save app.config to `run/${type}_config.json` * @private */ dumpConfig() { const rundir = this.config.rundir; - const configdir = path.join(rundir, `${this.type}_config.json`); try { + /* istanbul ignore if */ if (!fs.existsSync(rundir)) fs.mkdirSync(rundir); - fs.writeFileSync(configdir, JSON.stringify({ - config: this.config, - plugins: this.plugins, - }, null, 2)); + + // get dumpped object + const { config, meta } = this.dumpConfigToObject(); + + // dump config + const dumpFile = path.join(rundir, `${this.type}_config.json`); + fs.writeFileSync(dumpFile, CircularJSON.stringify(config, null, 2)); + + // dump config meta + const dumpMetaFile = path.join(rundir, `${this.type}_config_meta.json`); + fs.writeFileSync(dumpMetaFile, CircularJSON.stringify(meta, null, 2)); } catch (err) { - this.logger.warn(`dumpConfig error: ${err.message}`); + this.coreLogger.warn(`dumpConfig error: ${err.message}`); + } + } + + dumpTiming() { + try { + const items = this.timing.toJSON(); + const rundir = this.config.rundir; + const dumpFile = path.join(rundir, `${this.type}_timing_${process.pid}.json`); + fs.writeFileSync(dumpFile, CircularJSON.stringify(items, null, 2)); + this.coreLogger.info(this.timing.toString()); + // only disable, not clear bootstrap timing data. + this.timing.disable(); + // show duration >= ${slowBootActionMinDuration}ms action to warnning log + for (const item of items) { + // ignore #0 name: Process Start + if (item.index > 0 && item.duration >= this.config.dump.timing.slowBootActionMinDuration) { + this.coreLogger.warn('[egg:core][slow-boot-action] #%d %dms, name: %s', + item.index, item.duration, item.name); + } + } + } catch (err) { + this.coreLogger.warn(`dumpTiming error: ${err.message}`); } } @@ -219,22 +463,150 @@ class EggApplication extends EggCore { _setupTimeoutTimer() { const startTimeoutTimer = setTimeout(() => { - this.logger.error(`${this.type} still doesn't ready after ${this.config.workerStartTimeout} ms.`); + this.coreLogger.error(this.timing.toString()); + this.coreLogger.error(`${this.type} still doesn't ready after ${this.config.workerStartTimeout} ms.`); + // log unfinished + const items = this.timing.toJSON(); + for (const item of items) { + if (item.end) continue; + this.coreLogger.error(`unfinished timing item: ${CircularJSON.stringify(item)}`); + } + this.coreLogger.error(`check run/${this.type}_timing_${process.pid}.json for more details.`); this.emit('startTimeout'); + this.dumpConfig(); + this.dumpTiming(); }, this.config.workerStartTimeout); this.ready(() => clearTimeout(startTimeoutTimer)); } + /** + * app.env delegate app.config.env + * @deprecated + */ + get env() { + this.deprecate('please use app.config.env instead'); + return this.config.env; + } + /* eslint no-empty-function: off */ + set env(_) {} + + /** + * app.proxy delegate app.config.proxy + * @deprecated + */ + get proxy() { + this.deprecate('please use app.config.proxy instead'); + return this.config.proxy; + } + /* eslint no-empty-function: off */ + set proxy(_) {} /** - * 关闭 app 上的所有事件监听 - * @public + * create a singleton instance + * @param {String} name - unique name for singleton + * @param {Function|AsyncFunction} create - method will be invoked when singleton instance create */ - close() { - super.close(); - this.messenger.close(); - process.removeListener('unhandledRejection', this._unhandledRejectionHandler); + addSingleton(name, create) { + const options = {}; + options.name = name; + options.create = create; + options.app = this; + const singleton = new Singleton(options); + const initPromise = singleton.init(); + if (initPromise) { + this.beforeStart(async () => { + await initPromise; + }); + } + } + + _patchClusterClient(client) { + const create = client.create; + client.create = (...args) => { + const realClient = create.apply(client, args); + this[CLUSTER_CLIENTS].push(realClient); + this.beforeClose(() => cluster.close(realClient)); + return realClient; + }; } + + /** + * Create an anonymous context, the context isn't request level, so the request is mocked. + * then you can use context level API like `ctx.service` + * @member {String} EggApplication#createAnonymousContext + * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. + * @return {Context} context + */ + createAnonymousContext(req) { + const request = { + headers: { + host: '127.0.0.1', + 'x-forwarded-for': '127.0.0.1', + }, + query: {}, + querystring: '', + host: '127.0.0.1', + hostname: '127.0.0.1', + protocol: 'http', + secure: 'false', + method: 'GET', + url: '/', + path: '/', + socket: { + remoteAddress: '127.0.0.1', + remotePort: 7001, + }, + }; + if (req) { + for (const key in req) { + if (key === 'headers' || key === 'query' || key === 'socket') { + Object.assign(request[key], req[key]); + } else { + request[key] = req[key]; + } + } + } + const response = new http.ServerResponse(request); + return this.createContext(request, response); + } + + /** + * Create egg context + * @function EggApplication#createContext + * @param {Req} req - node native Request object + * @param {Res} res - node native Response object + * @return {Context} context object + */ + createContext(req, res) { + const app = this; + const context = Object.create(app.context); + const request = context.request = Object.create(app.request); + const response = context.response = Object.create(app.response); + context.app = request.app = response.app = app; + context.req = request.req = response.req = req; + context.res = request.res = response.res = res; + request.ctx = response.ctx = context; + request.response = response; + response.request = request; + context.onerror = context.onerror.bind(context); + context.originalUrl = request.originalUrl = req.url; + + /** + * Request start time + * @member {Number} Context#starttime + */ + context.starttime = Date.now(); + + if (this.config.logger.enablePerformanceTimer) { + /** + * Request start timer using `performance.now()` + * @member {Number} Context#performanceStarttime + */ + context.performanceStarttime = performance.now(); + } + return context; + } + } module.exports = EggApplication; diff --git a/lib/jsdoc/config.jsdoc b/lib/jsdoc/config.jsdoc deleted file mode 100644 index 0f67bd0a58..0000000000 --- a/lib/jsdoc/config.jsdoc +++ /dev/null @@ -1,5 +0,0 @@ -/** - * 应用程序配置信息,由 egg-loader 定义 - * @namespace Config - * @see https://github.com/eggjs/egg-loader - */ diff --git a/lib/jsdoc/context.jsdoc b/lib/jsdoc/context.jsdoc deleted file mode 100644 index 5cf5c0f7c2..0000000000 --- a/lib/jsdoc/context.jsdoc +++ /dev/null @@ -1,111 +0,0 @@ -/** - * 继承 koa 的 Context - * @class Context - * @see http://koajs.com/#context - */ - -/** - * 实现页面跳转 - * @see Response#redirect - * @method Context#redirect - * @param {String} url 需要跳转的地址 - */ - -/** - * 开启 {@link Rest} 功能后,将会有 `this.params` 对象 - * @member {Object} Context#params - * @example - * ##### ctx.params.id {String} - * - * 资源 id,如 `GET /api/users/1` => `'1'` - * - * ##### ctx.params.ids {Array} - * - * 一组资源 id,如 `GET /api/users/1,2,3` => `['1', '2', '3']` - * - * ##### ctx.params.fields {Array} - * - * 期待返回的资源字段,如 `GET /api/users/1?fields=name,title` => `['name', 'title']`. - * 即使应用 Controller 实现返回了全部字段,[REST] 处理器会根据 `fields` 筛选只需要的字段。 - * - * ##### ctx.params.data {Object} - * - * 请求数据对象 - * - * ##### ctx.params.page {Number} - * - * 分页码,如 `GET /api/users?page=10` => `10` - * - * ##### ctx.params.per_page {Number} - * - * 每页资源数目,如 `GET /api/users?per_page=20` => `20` - */ - -/** - * 设置返回资源对象 - * @member {Object} Context#data= - * @example - * ```js - * ctx.data = { - * id: 1, - * name: 'fengmk2' - * }; - * ``` - * - * 会返回 200 响应 - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "data": { - * "id": 1, - * "name": "fengmk2" - * } - * } - * ``` - */ - -/** - * 设置 meta 响应数据 - * @member {Object} Context#meta= - * @example - * ```js - * ctx.meta = { - * count: 100 - * }; - * - * ctx.data = [ - * { - * id: 1, - * title: 'post title 1' - * }, - * { - * id: 2, - * title: 'post title 2' - * } - * ]; - * ``` - * - * 会返回 200 响应 - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "meta": { - * "count": 100 - * } - * "data": [ - * { - * "id": 1, - * "title": 'post title 1' - * }, - * { - * "id": 2, - * "title": 'post title 2' - * } - * ] - * } - * ``` - */ diff --git a/lib/jsdoc/request.jsdoc b/lib/jsdoc/request.jsdoc deleted file mode 100644 index 0cee0026db..0000000000 --- a/lib/jsdoc/request.jsdoc +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 继承 koa 的 Request - * @class Request - * @see http://koajs.com/#request - */ - - -// 文档注释 -/** - * Request header - * @member {Object} Request#header - */ - -/** - * Request headers - * @member {Object} Request#headers - */ - -/** - * Request HTTP Method, 比如: GET, POST, PATCH, PUT, DELETE - * @member {String} Request#method - */ - -/** - * 完整的请求 URL 地址 - * @member {String} Request#url - */ - -/** - * 完整的请求 URL 地址 - * @member {String} Request#originalUrl - */ - -/** - * 完整的请求 URL 地址,的路径部分(不包含域名) - * @member {String} Request#path - */ - -/** - * 请求的 GET 参数 - * @member {String} Request#querystring - * @example - * GET http://127.0.0.1:7001?name=Foo&age=20 - * ```js - * this.request.querystring - * => 'name=Foo&age=20' - * ``` - */ diff --git a/lib/jsdoc/response.jsdoc b/lib/jsdoc/response.jsdoc deleted file mode 100644 index f8cfd8d007..0000000000 --- a/lib/jsdoc/response.jsdoc +++ /dev/null @@ -1,5 +0,0 @@ -/** - * 继承 koa 的 Response - * @class Response - * @see http://koajs.com/#response - */ diff --git a/lib/loader/agent_worker_loader.js b/lib/loader/agent_worker_loader.js index 9f564943af..abf4c3bde1 100644 --- a/lib/loader/agent_worker_loader.js +++ b/lib/loader/agent_worker_loader.js @@ -1,13 +1,9 @@ -/** - * AgentWorkerLoader 类,继承 BaseLoader,实现整个应用的加载机制 - */ - 'use strict'; const EggLoader = require('egg-core').EggLoader; /** - * Agent Worker 进程的 Loader,继承 egg-loader + * Agent worker process loader * @see https://github.com/eggjs/egg-loader */ class AgentWorkerLoader extends EggLoader { @@ -16,12 +12,14 @@ class AgentWorkerLoader extends EggLoader { * loadPlugin first, then loadConfig */ loadConfig() { - super.loadPlugin(); + this.loadPlugin(); super.loadConfig(); } load() { this.loadAgentExtend(); + this.loadContextExtend(); + this.loadCustomAgent(); } } diff --git a/lib/loader/app_worker_loader.js b/lib/loader/app_worker_loader.js index 9fe7eaee95..f2de253de3 100644 --- a/lib/loader/app_worker_loader.js +++ b/lib/loader/app_worker_loader.js @@ -3,7 +3,7 @@ const EggLoader = require('egg-core').EggLoader; /** - * App Worker process Loader, will load plugins + * App worker process Loader, will load plugins * @see https://github.com/eggjs/egg-loader */ class AppWorkerLoader extends EggLoader { @@ -13,12 +13,12 @@ class AppWorkerLoader extends EggLoader { * @since 1.0.0 */ loadConfig() { - super.loadPlugin(); + this.loadPlugin(); super.loadConfig(); } /** - * 开始加载所有约定目录 + * Load all directories in convention * @since 1.0.0 */ load() { @@ -29,6 +29,8 @@ class AppWorkerLoader extends EggLoader { this.loadContextExtend(); this.loadHelperExtend(); + this.loadCustomLoader(); + // app > plugin this.loadCustomApp(); // app > plugin @@ -38,7 +40,7 @@ class AppWorkerLoader extends EggLoader { // app this.loadController(); // app - this.loadRouter(); // 依赖 controller + this.loadRouter(); // Depend on controllers } } diff --git a/lib/start.js b/lib/start.js new file mode 100644 index 0000000000..478c086d13 --- /dev/null +++ b/lib/start.js @@ -0,0 +1,39 @@ +'use strict'; + +const path = require('path'); + +module.exports = async (options = {}) => { + + options.baseDir = options.baseDir || process.cwd(); + options.mode = 'single'; + + // get agent from options.framework and package.egg.framework + if (!options.framework) { + try { + options.framework = require(path.join(options.baseDir, 'package.json')).egg.framework; + } catch (_) { + // ignore + } + } + let Agent; + let Application; + if (options.framework) { + Agent = require(options.framework).Agent; + Application = require(options.framework).Application; + } else { + Application = require('./application'); + Agent = require('./agent'); + } + + const agent = new Agent(Object.assign({}, options)); + await agent.ready(); + const application = new Application(Object.assign({}, options)); + application.agent = agent; + agent.application = application; + await application.ready(); + + // emit egg-ready message in agent and application + application.messenger.broadcast('egg-ready'); + + return application; +}; diff --git a/package.json b/package.json index ec2bfad9bd..660d118a6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "egg", - "version": "0.2.1", + "version": "3.30.1", + "publishConfig": { + "tag": "latest", + "access": "public" + }, "description": "A web framework's framework for Node.js", "keywords": [ "web", @@ -13,100 +17,111 @@ "egg" ], "dependencies": { - "accepts": "^1.3.3", - "agentkeepalive": "^2.2.0", - "co": "^4.6.0", - "debug": "^2.2.0", + "@types/accepts": "^1.3.5", + "@types/koa": "^2.13.5", + "@types/koa-router": "^7.4.4", + "accepts": "^1.3.8", + "agentkeepalive": "^4.2.1", + "cache-content-type": "^1.0.1", + "circular-json-for-egg": "^1.0.0", + "cluster-client": "^3.3.0", "delegates": "^1.0.0", - "egg-cluster": "^0.1.0", - "egg-cookies": "^1.0.0", - "egg-core": "^0.2.1", - "egg-cors": "^0.0.2", - "egg-development": "^1.0.1", - "egg-i18n": "^1.0.2", - "egg-logger": "^1.2.0", - "egg-logrotator": "^2.1.0", - "egg-multipart": "^1.0.0", - "egg-onerror": "^0.0.3", - "egg-rest": "^1.0.1", - "egg-schedule": "^2.0.0", - "egg-security": "^1.2.1", - "egg-session": "^0.0.2", - "egg-static": "^0.1.0", - "egg-userrole": "^0.1.0", - "egg-userservice": "^1.0.0", - "egg-validate": "^0.0.2", - "egg-watcher": "^1.0.0", - "graceful": "^1.0.1", - "humanize-ms": "^1.2.0", - "is-type-of": "^1.0.0", - "jsonp-body": "^1.0.0", - "koa-bodyparser": "^2.2.0", + "egg-cluster": "^2.0.0", + "egg-cookies": "^2.6.1", + "egg-core": "^5.4.0", + "egg-development": "^3.0.0", + "egg-errors": "^2.3.1", + "egg-i18n": "^2.1.1", + "egg-jsonp": "^2.0.0", + "egg-logger": "^3.0.1", + "egg-logrotator": "^3.1.0", + "egg-multipart": "^3.1.0", + "egg-onerror": "^2.1.1", + "egg-schedule": "^4.0.0", + "egg-security": "^3.0.0", + "egg-session": "^3.3.0", + "egg-static": "^2.2.0", + "egg-view": "^2.1.3", + "egg-watcher": "^3.1.1", + "extend2": "^1.0.1", + "graceful": "^1.1.0", + "humanize-ms": "^1.2.1", + "is-type-of": "^2.1.0", + "koa-bodyparser": "^4.4.1", "koa-is-json": "^1.0.0", - "koa-override": "^1.0.0", - "mime-types": "^2.1.12", - "scmp": "^1.0.0", - "sdk-base": "^2.0.1", - "sendmessage": "^1.0.5", - "urllib": "^2.14.0" + "koa-override": "^3.0.0", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "onelogger": "^1.0.0", + "sendmessage": "^2.0.0", + "urllib": "^2.33.0", + "urllib-next": "npm:urllib@^3.27.1", + "urllib4": "npm:urllib@^4.5.0", + "utility": "^2.1.0", + "ylru": "^1.3.2" }, "devDependencies": { - "autod": "^2.7.1", - "autod-egg": "^1.0.0", - "beautify-benchmark": "^0.2.4", - "benchmark": "^2.1.0", - "co-sleep": "^0.0.1", - "coffee": "^3.2.5", - "cross-env": "^2.0.1", - "egg-alinode": "^1.0.3", - "egg-bin": "^1.3.0", - "egg-ci": "^1.0.3", - "egg-mock": "^0.0.4", - "egg-plugin-puml": "1", - "egg-view-nunjucks": "^0.5.0", - "eslint": "^3.0.0", - "eslint-config-egg": "^3.1.0", - "estraverse": "^4.1.1", - "formstream": "^1.0.0", - "glob": "^7.0.6", - "koa": "^1.2.4", - "koa-router": "^5.4.0", - "merge-descriptors": "^1.0.1", - "moment": "^2.15.0", - "npminstall": "^2.1.1", - "nunjucks": "^2.5.2", - "once": "^1.3.3", - "pedding": "^1.0.0", - "rds": "^0.1.0", - "rimraf": "^2.5.4", - "should": "^11.1.0", - "stream-wormhole": "^1.0.0", - "supertest": "^2.0.0", - "toa": "^2.1.0", - "toa-router": "^1.5.1" + "@eggjs/tsconfig": "^1.1.0", + "@types/node": "^20.1.2", + "@umijs/preset-react": "^2.1.6", + "address": "^1.2.1", + "antd": "^4.23.2", + "assert-file": "^1.0.0", + "coffee": "^5.4.0", + "cross-env": "^7.0.3", + "dumi": "^1.1.47", + "dumi-theme-egg": "^1.2.2", + "egg-bin": "^6.4.1", + "egg-mock": "^5.10.7", + "egg-plugin-puml": "^2.4.0", + "egg-tracer": "^2.0.0", + "egg-view-nunjucks": "^2.3.0", + "eslint": "^8.23.1", + "eslint-config-egg": "^12.0.0", + "formstream": "^1.1.1", + "https-pem": "^3.0.0", + "jsdoc": "^3.6.11", + "koa": "^2.13.4", + "koa-static": "^5.0.0", + "node-libs-browser": "^2.2.1", + "pedding": "^1.1.0", + "prettier": "^2.7.1", + "react": "^16.14.0", + "react-dom": "^16.14.0", + "react-router": "^5.3.4", + "sdk-base": "^4.2.1", + "spy": "^1.0.0", + "supertest": "^6.2.4", + "ts-node": "^10.9.1", + "tsd": "^0.28.1", + "typescript": "^5.0.4", + "umi": "^3.5.36" }, "main": "index.js", + "types": "index.d.ts", "files": [ + "index.js", + "lib", "app", "config", - "bin", - "lib", - "index.js" + "agent.js", + "index.d.ts" ], "scripts": { - "lint": "eslint lib test examples *.js", - "test": "npm run lint && npm run test-local", - "test-local": "egg-bin test", - "test-examples": "cross-env TESTS=examples/**/test/**/*.test.js egg-bin test", - "cov": "egg-bin cov", - "ci": "npm run lint && npm run test-examples && npm run cov", - "doc-server": "./scripts/doc.sh server", - "doc-deploy": "./scripts/doc.sh deploy", - "autod": "autod", - "autod-china": "autod --registry=https://registry.npm.taobao.org", - "puml": "puml . --dest ./docs", - "commits": "./scripts/commits.sh", - "example": "node examples/start.js" + "lint": "eslint app config lib test *.js", + "tsd": "tsd", + "test": "npm run lint -- --fix && npm run tsd && npm run test-local", + "test-local": "egg-bin test --ts false", + "test-local-changed": "egg-bin test --changed --ts false", + "cov": "egg-bin cov --timeout 100000 --ts false", + "ci": "npm run lint && npm run tsd && npm run cov", + "site:dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider APP_ROOT=./site dumi dev", + "site:devWithNode14-16": "cross-env APP_ROOT=./site dumi dev", + "site:build": "cross-env NODE_OPTIONS=--openssl-legacy-provider APP_ROOT=./site dumi build", + "site:buildWithNode14-16": "cross-env APP_ROOT=./site dumi build", + "site:prettier": "prettier --config site/.prettierrc --ignore-path site/.prettierignore --write \"site/**/*.{js,jsx,tsx,ts,less,md,json}\"", + "puml": "puml . --dest ./site", + "commits": "./scripts/commits.sh" }, "homepage": "https://github.com/eggjs/egg", "repository": { @@ -114,9 +129,7 @@ "url": "https://github.com/eggjs/egg.git" }, "engines": { - "node": ">= 4.0.0" + "node": ">= 14.20.0" }, - "ci": { - "version": "4, 6" - } + "license": "MIT" } diff --git a/scripts/doc.sh b/scripts/doc.sh deleted file mode 100755 index 11e8cd9201..0000000000 --- a/scripts/doc.sh +++ /dev/null @@ -1,49 +0,0 @@ -#! /usr/bin/env bash - -export PATH=./docs/node_modules/.bin:./node_modules/.bin:./scripts:$PATH - -npm_install() { - pushd docs > /dev/null - [ -d node_modules ] || npminstall - popd > /dev/null -} - -import_ghpages() { - echo "Pushing gh-pages" - local message="Update documentation based on `git log -1 --pretty=%H`" - ghp-import -p -m "$message" docs/public || exit $? -} - -copy_release() { - echo -e "layout: release\n---\n" > tmp - cat tmp History.md > docs/source/release/index.md || exit $? - rm tmp -} - -copy_files() { - copy_release || exit $? - cp CONTRIBUTING.md docs/source/contributing.md || exit $? - cp CONTRIBUTING.zh-CN.md docs/source/zh-cn/contributing.md || exit $? - cp MEMBER_GUIDE.md docs/source/member_guide.md || exit $? -} - -server() { - copy_files || exit $? - npm_install || exit $? - hexo --cwd docs server -l -} - -deploy() { - copy_files || exit $? - npm_install || exit $? - hexo --cwd docs generate --force || exit $? - import_ghpages || exit $? -} - -action=$1 - -if [ $action = 'deploy' ]; then - deploy -elif [ $action = 'server' ]; then - server -fi diff --git a/scripts/ghp-import b/scripts/ghp-import deleted file mode 100755 index bc12366175..0000000000 --- a/scripts/ghp-import +++ /dev/null @@ -1,202 +0,0 @@ -#! /usr/bin/env python -# -# This file is part of the ghp-import package released under -# the Tumbolia Public License. See the LICENSE file for more -# information. - -import errno -import optparse as op -import os -import subprocess as sp -import sys -import time -import unicodedata - -__usage__ = "%prog [OPTIONS] DIRECTORY" - - -if sys.version_info[0] == 3: - def enc(text): - if isinstance(text, bytes): - return text - return text.encode() - - def dec(text): - if isinstance(text, bytes): - return text.decode('utf-8') - return text - - def write(pipe, data): - try: - pipe.stdin.write(data) - except IOError as e: - if e.errno != errno.EPIPE: - raise -else: - def enc(text): - if isinstance(text, unicode): - return text.encode('utf-8') - return text - - def dec(text): - if isinstance(text, unicode): - return text - return text.decode('utf-8') - - def write(pipe, data): - pipe.stdin.write(data) - - -def normalize_path(path): - # Fix unicode pathnames on OS X - # See: http://stackoverflow.com/a/5582439/44289 - if sys.platform == "darwin": - return unicodedata.normalize("NFKC", dec(path)) - return path - - -def check_repo(parser): - cmd = ['git', 'rev-parse'] - p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) - (ignore, error) = p.communicate() - if p.wait() != 0: - if not error: - error = "Unknown Git error" - error = error.decode("utf-8") - if error.startswith("fatal: "): - error = error[len("fatal: "):] - parser.error(error) - - -def try_rebase(remote, branch): - cmd = ['git', 'rev-list', '--max-count=1', '%s/%s' % (remote, branch)] - p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) - (rev, ignore) = p.communicate() - if p.wait() != 0: - return True - cmd = ['git', 'update-ref', 'refs/heads/%s' % branch, rev.strip()] - if sp.call(cmd) != 0: - return False - return True - - -def get_config(key): - p = sp.Popen(['git', 'config', key], stdin=sp.PIPE, stdout=sp.PIPE) - (value, stderr) = p.communicate() - return value.strip() - - -def get_prev_commit(branch): - cmd = ['git', 'rev-list', '--max-count=1', branch, '--'] - p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) - (rev, ignore) = p.communicate() - if p.wait() != 0: - return None - return rev.decode('utf-8').strip() - - -def mk_when(timestamp=None): - if timestamp is None: - timestamp = int(time.time()) - currtz = "%+05d" % (-1 * time.timezone / 36) # / 3600 * 100 - return "%s %s" % (timestamp, currtz) - - -def start_commit(pipe, branch, message): - uname = dec(get_config("user.name")) - email = dec(get_config("user.email")) - write(pipe, enc('commit refs/heads/%s\n' % branch)) - write(pipe, enc('committer %s <%s> %s\n' % (uname, email, mk_when()))) - write(pipe, enc('data %d\n%s\n' % (len(message), message))) - head = get_prev_commit(branch) - if head: - write(pipe, enc('from %s\n' % head)) - write(pipe, enc('deleteall\n')) - - -def add_file(pipe, srcpath, tgtpath): - with open(srcpath, "rb") as handle: - if os.access(srcpath, os.X_OK): - write(pipe, enc('M 100755 inline %s\n' % tgtpath)) - else: - write(pipe, enc('M 100644 inline %s\n' % tgtpath)) - data = handle.read() - write(pipe, enc('data %d\n' % len(data))) - write(pipe, enc(data)) - write(pipe, enc('\n')) - - -def add_nojekyll(pipe): - write(pipe, enc('M 100644 inline .nojekyll\n')) - write(pipe, enc('data 0\n')) - write(pipe, enc('\n')) - - -def gitpath(fname): - norm = os.path.normpath(fname) - return "/".join(norm.split(os.path.sep)) - - -def run_import(srcdir, branch, message, nojekyll): - cmd = ['git', 'fast-import', '--date-format=raw', '--quiet'] - kwargs = {"stdin": sp.PIPE} - if sys.version_info >= (3, 2, 0): - kwargs["universal_newlines"] = False - pipe = sp.Popen(cmd, **kwargs) - start_commit(pipe, branch, message) - for path, dnames, fnames in os.walk(srcdir): - for fn in fnames: - fpath = os.path.join(path, fn) - fpath = normalize_path(fpath) - gpath = gitpath(os.path.relpath(fpath, start=srcdir)) - add_file(pipe, fpath, gpath) - if nojekyll: - add_nojekyll(pipe) - write(pipe, enc('\n')) - pipe.stdin.close() - if pipe.wait() != 0: - sys.stdout.write(enc("Failed to process commit.\n")) - - -def options(): - return [ - op.make_option('-n', dest='nojekyll', default=False, - action="store_true", - help='Include a .nojekyll file in the branch.'), - op.make_option('-m', dest='mesg', default='Update documentation', - help='The commit message to use on the target branch.'), - op.make_option('-p', dest='push', default=False, action='store_true', - help='Push the branch to origin/{branch} after committing.'), - op.make_option('-r', dest='remote', default='origin', - help='The name of the remote to push to. [%default]'), - op.make_option('-b', dest='branch', default='gh-pages', - help='Name of the branch to write to. [%default]'), - ] - - -def main(): - parser = op.OptionParser(usage=__usage__, option_list=options()) - opts, args = parser.parse_args() - - if len(args) == 0: - parser.error("No import directory specified.") - - if len(args) > 1: - parser.error("Unknown arguments specified: %s" % ', '.join(args[1:])) - - if not os.path.isdir(args[0]): - parser.error("Not a directory: %s" % args[0]) - - check_repo(parser) - - if not try_rebase(opts.remote, opts.branch): - parser.error("Failed to rebase %s branch." % opts.branch) - - run_import(args[0], opts.branch, opts.mesg, opts.nojekyll) - - if opts.push: - sp.check_call(['git', 'push', opts.remote, opts.branch]) - - -if __name__ == '__main__': - main() diff --git a/site/.prettierignore b/site/.prettierignore new file mode 100644 index 0000000000..f2b88c4d8a --- /dev/null +++ b/site/.prettierignore @@ -0,0 +1,9 @@ +**/*.svg +**/*.ejs +**/*.html +package.json + +.umi +.umi-production + +dist diff --git a/site/.prettierrc b/site/.prettierrc new file mode 100644 index 0000000000..94beb14840 --- /dev/null +++ b/site/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80, + "overrides": [ + { + "files": ".prettierrc", + "options": { "parser": "json" } + } + ] +} diff --git a/site/app.ts b/site/app.ts new file mode 100644 index 0000000000..2b5d6c2152 --- /dev/null +++ b/site/app.ts @@ -0,0 +1,26 @@ +// @ts-ignore +import { history, Route } from 'umi'; + +export function onRouteChange(props: any) { + const { location }: RouterChangeProps = props; + + let pathname = location.pathname; + + if (pathname.startsWith('/en')) { + pathname = pathname.replace('/en', ''); + pathname = pathname.replace('.html', ''); + history.push(pathname); + } + + if (pathname.startsWith('/zh-cn')) { + pathname = pathname.replace('zh-cn', 'zh-CN'); + pathname = pathname.replace('.html', ''); + history.push(pathname); + } +} + +interface RouterChangeProps { + location: Location; + routes: Route[]; + action: string; +} diff --git a/site/config/config.ts b/site/config/config.ts new file mode 100644 index 0000000000..ba4c5d1350 --- /dev/null +++ b/site/config/config.ts @@ -0,0 +1,111 @@ +import { defineConfig } from 'dumi'; + +export default defineConfig({ + mode: 'site', + title: 'Egg', + + description: 'Born to build better enterprise frameworks and apps', + + logo: '/logo.svg', + favicon: '/favicon.png', + + // algolia: { + // apiKey: '1561de31a86f79507ea00cdb54ce647c', + // indexName: 'eggjs', + // }, + + theme: { + '@c-primary': '#22ab28', + }, + + exportStatic: {}, + + sitemap: { + hostname: 'https://eggjs.org', + }, + + navs: { + 'en-US': [ + null, + { + title: 'GitHub', + path: 'https://github.com/eggjs/egg', + }, + { + title: 'Release', + path: 'https://github.com/eggjs/egg/releases', + }, + { + title: 'Plugins', + path: 'https://github.com/search?q=topic%3Aegg-plugin&type=Repositories', + }, + ], + 'zh-CN': [ + null, + { + title: 'GitHub', + path: 'https://github.com/eggjs/egg', + }, + { + title: '插件列表', + path: 'https://github.com/search?q=topic%3Aegg-plugin&type=Repositories', + }, + { + title: '发布日志', + path: 'https://github.com/eggjs/egg/releases', + }, + ], + }, + + themeConfig: { + links: [ + { + title: 'Resources', + list: [ + { + name: 'Egg', + url: 'https://github.com/eggjs/egg', + }, + { + name: 'Organization', + url: 'https://github.com/eggjs', + }, + ], + }, + { + title: 'XTech', + list: [ + { name: 'EggJS - 企业级 Node.js 开发框架', url: 'https://eggjs.org' }, + { name: 'Ant Design - UI 体系', url: 'https://ant.design' }, + { name: 'AntV - 数据可视化', url: 'https://antv.vision' }, + { + name: '语雀 - 知识创作与分享工具', + url: 'https://www.yuque.com', + }, + ], + }, + { + title: 'Community', + list: [ + { name: 'Artus.js 官网', url: 'https://artusjs.org' }, + { name: 'CNode 社区', url: 'https://cnodejs.org/' }, + { name: 'Node.js 专栏', url: 'https://www.yuque.com/egg/nodejs' }, + { + name: '提交反馈', + url: 'https://github.com/eggjs/egg/issues', + }, + { name: '发布日志', url: 'https://github.com/eggjs/egg/releases' }, + ], + }, + { + title: 'Egg.js Telegram Channel', + list: [ + { + name: '钉钉', + qrcode: '/img_egg/qrcode_egg_channel.png', + }, + ], + }, + ], + }, +}); diff --git a/site/docs/advanced/cluster-client.md b/site/docs/advanced/cluster-client.md new file mode 100644 index 0000000000..bbe6b834f2 --- /dev/null +++ b/site/docs/advanced/cluster-client.md @@ -0,0 +1,558 @@ +--- +title: Multi-Process Development Model Enhancement +order: 4 +--- + +In the previous [Multi-Process Model chapter](../core/cluster-and-ipc.md), we covered the multi-process model of the framework in detail, whose Agent process suits for a common class of scenarios: some middleware clients need to establish a persistent connection with server. In theory, a server had better establish only one persistent connection. However, the multi-process model will result in n times (n = number of Worker processes) connections being created. + +```bash ++--------+ +--------+ +| Client | | Client | ... n ++--------+ +--------+ + | \ / | + | \ / | n * m links + | / \ | + | / \ | ++--------+ +--------+ +| Server | | Server | ... m ++--------+ +--------+ +``` + +In order to reuse persistent connections as much as possible (because they are very valuable resources for server), we put them into the Agent process to maintain, and then we transmit data to each Worker via messenger. It's feasible but we often need to write many codes to encapsulate the interface and realize data transmission, which is very troublesome. + +In addition, it's relatively inefficient to transmit data via messenger, since messenger will do the transmission through the Master; In case IPC channel goes wrong, it would probably break Master process down. + +So is there any better way? The answer is: YES! We provide a new type of model to reduce the complexity of this type of client encapsulation. The new Model bypasses the Master by establishing a direct socket between Agent and Worker. And as an external interface, Agent maintains shared connection among multiple Worker processes. + +## Core Idea + +- Inspired by the [Leader/Follower](https://www.dre.vanderbilt.edu/~schmidt/PDF/lf.pdf) model. +- The client is divided into two roles: + - Leader: Be responsible for maintaining the connection with the remote server, only one Leader for the same type of client. + - Follower: Delegate specific operations to the Leader. A common way is Subscribe-Model (let the Leader interact with remote server and wait for its return). +- How to determine who Leader is, who Follower is? There are two modes: + - Free competition mode: clients determine the Leader by the competition of the local port when start up. For example: every one tries to monitor port 7777, and finally the only one instance who seizes it will become Leader, the rest will be Followers. + - Mandatory mode: the framework designates a Leader and the rest are Followers. +- we use mandatory mode inside the framework. The Leader can only be created inside the Agent, which is also in line with our positioning of the Agent. +- When the framework starts up, Master will randomly select an available port as the communication port monitored by the Cluster Client, and passes it by parameter to Agent and App Worker. +- Leader communicates with Follower through direct socket connection (through communication port), no longer needs Master to transit. + +Under the new mode, the client's communication is as follows: + +```js + +-------+ + | start | + +---+---+ + | + +--------+---------+ + __| port competition |__ +win / +------------------+ \ lose + / \ ++---------------+ tcp conn +-------------------+ +| Leader(Agent) |<---------------->| Follower(Worker1) | ++---------------+ +-------------------+ + | \ tcp conn + | \ ++--------+ +-------------------+ +| Client | | Follower(Worker2) | ++--------+ +-------------------+ +``` + +## Client Interface Type Abstraction + +We abstract the client interface into the following two broad categories, which is also a specification of the client interface. For clients that are in line with norms, we can automatically wrap it as Leader / Follower mode. + +- Subscribe / Publish Mode: + - The `subscribe(info, listener)` interface contains two parameters. The first one is the information subscribed and the second one is callback function for subscribe. + - The `publish(info)` interface contains a parameter which is the information subscribed. +- Invoke Mode, supports three styles of interface: callback, promise and generator function, but generator function is recommended. + +Client example + +```js +const Base = require('sdk-base'); + +class Client extends Base { + constructor(options) { + super(options); // remember to invoke ready after initialization is successful + this.ready(true); + } + /** + * Subscribe + * + * @param {Object} info - subscription information (a JSON object, try not to include attributes such as Function, Buffer, Date) + * @param {Function} listener - monitoring callback function, receives a parameter as the result of monitoring + */ + + subscribe(info, listener) { + // ... + } + /** + * Publish + * + * @param {Object} info - publishing information, which is similar to that of subscribe described above + */ + + publish(info) { + // ... + } + /** + * Get data (invoke) + * + * @param {String} id - id + * @return {Object} result + */ + + async getData(id) { + // ... + } +} +``` + +## Exception Handling + +- If Leader "dies", a new round of port contention will be triggered. The instance which seizes the port will be elected as the new Leader. +- To ensure that the channel between Leader and Follower is healthy, heartbeat mechanism needs to be introduced. If the Follower does not send a heartbeat packet within a fixed time, the Leader will proactively disconnect from the Follower, which will trigger the reinitialization of Follower. + +## Protocol and Time Series to Invoke + +Leader and Follower exchange data via the following protocols: + +```js + 0 1 2 4 12 + +-------+-------+---------------+---------------------------------------------------------------+ + |version|req/res| reserved | request id | + +-------------------------------+-------------------------------+-------------------------------+ + | timeout | connection object length | application object length | + +-------------------------------+---------------------------------------------------------------+ + | conn object (JSON format) ... | app object | + +-----------------------------------------------------------+ | + | ... | + +-----------------------------------------------------------------------------------------------+ +``` + +1. On the communication port Leader starts a Local Server, via which all Leaders / Followers communicate. +2. After Follower connects Local Server, it will firstly send a register channel packet (introduction of the channel concept is to distinguish between different types of clients). +3. Local Server will assign Follower to a specified Leader (match based on client type). +4. Follower sends requests to Leader to subscribe and publish. +5. Leader notifies Follower through the subscribe result packet when the subscription data changes. +6. Follower sends a call request to the Leader. The Leader executes a corresponding operation after receiving, and returns the result. + +```js + +----------+ +---------------+ +---------+ + | Follower | | Local Server | | Leader | + +----------+ +---------------+ +---------+ + | register channel | assign to | + + -----------------------> | --------------------> | + | | | + | subscribe | + + ------------------------------------------------> | + | publish | + + ------------------------------------------------> | + | | + | subscribe result | + | <------------------------------------------------ + + | | + | invoke | + + ------------------------------------------------> | + | invoke result | + | <------------------------------------------------ + + | | +``` + +## Specific Usage + +In the following I will use a simple example to introduce how to make a client support Leader / Follower mode in the framework. + +- The first step, our client is best to meet the interface conventions mentioned above, for example: + +```js +// registry_client.js +const URL = require('url'); +const Base = require('sdk-base'); + +class RegistryClient extends Base { + constructor(options) { + super({ + // Specify a method for asynchronous start + initMethod: 'init', + }); + this._options = options; + this._registered = new Map(); + } + /** + * Start logic + */ + + async init() { + this.ready(true); + } + /** + * Get configuration + * @param {String} dataId - the dataId + * @return {Object} configuration + */ + + async getConfig(dataId) { + return this._registered.get(dataId); + } + /** + * Subscribe + * @param {Object} reg + * - {String} dataId - the dataId + * @param {Function} listener - the listener + */ + + subscribe(reg, listener) { + const key = reg.dataId; + this.on(key, listener); + + const data = this._registered.get(key); + if (data) { + process.nextTick(() => listener(data)); + } + } + /** + * publish + * @param {Object} reg + * - {String} dataId - the dataId + * - {String} publishData - the publish data + */ + + publish(reg) { + const key = reg.dataId; + let changed = false; + + if (this._registered.has(key)) { + const arr = this._registered.get(key); + if (arr.indexOf(reg.publishData) === -1) { + changed = true; + arr.push(reg.publishData); + } + } else { + changed = true; + this._registered.set(key, [reg.publishData]); + } + if (changed) { + this.emit( + key, + this._registered.get(key).map((url) => URL.parse(url, true)), + ); + } + } +} + +module.exports = RegistryClient; +``` + +- The second step is to encapsulate the `RegistryClient` using the `agent.cluster` interface: + +```js +// agent.js +const RegistryClient = require('registry_client'); + +module.exports = (agent) => { + // encapsulate and instantiate RegistryClient + agent.registryClient = agent + .cluster(RegistryClient) // parameter of create method is the parameter of RegistryClient constructor + .create({}); + + agent.beforeStart(async () => { + await agent.registryClient.ready(); + agent.coreLogger.info('registry client is ready'); + }); +}; +``` + +- The third step, use the `app.cluster` interface to encapsulate `RegistryClient`: + +```js +// app.js +const RegistryClient = require('registry_client'); + +module.exports = (app) => { + app.registryClient = app.cluster(RegistryClient).create({}); + app.beforeStart(async () => { + await app.registryClient.ready(); + app.coreLogger.info('registry client is ready'); + + // invoke subscribe to subscribe + app.registryClient.subscribe( + { + dataId: 'demo.DemoService', + }, + (val) => { + // ... + }, + ); + + // invoke publish to publsih data + app.registryClient.publish({ + dataId: 'demo.DemoService', + publishData: 'xxx', + }); + + // invoke getConfig interface + const res = await app.registryClient.getConfig('demo.DemoService'); + console.log(res); + }); +}; +``` + +Isn't it so simple? + +Of course, if your client is not so 『standard』, then you may need to use some other APIs, for example, your subscription function is not named `subscribe`, but `sub`: + +```js +class MockClient extends Base { + constructor(options) { + super({ + initMethod: 'init', + }); + this._options = options; + this._registered = new Map(); + } + + async init() { + this.ready(true); + } + + sub(info, listener) { + const key = reg.dataId; + this.on(key, listener); + + const data = this._registered.get(key); + if (data) { + process.nextTick(() => listener(data)); + } + } + + ... +} +``` + +You need to set it manually with the `delegate` API: + +```js +// agent.js +module.exports = (agent) => { + agent.mockClient = agent + .cluster(MockClient) + // delegate sub to logic of subscribe + .delegate('sub', 'subscribe') + .create(); + + agent.beforeStart(async () => { + await agent.mockClient.ready(); + }); +}; +``` + +```js +// app.js +module.exports = (app) => { + app.mockClient = app + .cluster(MockClient) + // delegate sub to subscribe logic + .delegate('sub', 'subscribe') + .create(); + + app.beforeStart(async () => { + await app.mockClient.ready(); + + app.sub({ id: 'test-id' }, (val) => { + // put your code here + }); + }); +}; +``` + +We've already known that using `cluster-client` allows us to develop a 『pure』 RegistryClient without understanding the multi-process model. We can only focus on interacting with server, and use the `cluster-client` with a simple wrap to get a `ClusterClient` which supports multi-process model. The `RegistryClient` here is actually a `DataClient` that is specifically responsible for data communication with remote service. + +You may have noticed that the `ClusterClient` brings with several constraints at the same time. If you want to expose the same approach to each process, `RegistryClient` can only support sub/pub mode and asynchronous API calls. Because all interactions in multi-process model must use socket communications, under which it is bound to bring this constraint. + +Suppose we want to realize a synchronous `get` method. Put subscribed data directly into memory and use the `get` method to return data directly. How to achieve it? The real situation may be more complicated. + +Here, we introduce an `APIClient` best practice. For modules that have requirements of synchronous API such as reading cached data, an `APIClient` is encapsulated base on RegistryClient to implement these APIs that are not related to interaction with the remote server. The `APIClient` instance is exposed to the user. + +In `APIClient` internal implementation: + +- To obtain data asynchronously, invoke RegistryClient's API base on ClusterClient. +- Interfaces that are unrelated to server, such as synchronous call, are to be implemented in `APIClient`. Since ClusterClient's APIs have flushed multi-process differences, there is no need to concern about multi-process model when calls to RegistryClient during developing `APIClient`. + +For example, add a synchronous get method with buffer in the `APIClient` module: + +```js +// some-client/index.js +const cluster = require('cluster-client'); +const RegistryClient = require('./registry_client'); + +class APIClient extends Base { + constructor(options) { + super(options); // options.cluster is used to pass app.cluster to Egg's plugin + + this._client = (options.cluster || cluster)(RegistryClient).create(options); + this._client.ready(() => this.ready(true)); + + this._cache = {}; + + // subMap: + // { + // foo: reg1, + // bar: reg2, + // } + const subMap = options.subMap; + + for (const key in subMap) { + this.subscribe(subMap[key], (value) => { + this._cache[key] = value; + }); + } + } + + subscribe(reg, listener) { + this._client.subscribe(reg, listener); + } + + publish(reg) { + this._client.publish(reg); + } + + get(key) { + return this._cache[key]; + } +} + +// at last the module exposes this APIClient +module.exports = APIClient; +``` + +Then we can use this module like this: + +```js +// app.js || agent.js +const APIClient = require('some-client'); // the module above +module.exports = app => { + const config = app.config.apiClient; + app.apiClient = new APIClient(Object.assign({}, config, { cluster: app.cluster }); + app.beforeStart(async () => { + await app.apiClient.ready(); + }); +}; + +// config.${env}.js +exports.apiClient = { + subMap: { + foo: { + id: '', + }, + // bar... + } +}; +``` + +To make it easy for you to encapsulate `APIClient`, we provide an` APIClientBase` base class in the [cluster-client](https://www.npmjs.com/package/cluster-client) module. Then `APIClient` above can be rewritten as: + +```js +const APIClientBase = require('cluster-client').APIClientBase; +const RegistryClient = require('./registry_client'); + +class APIClient extends APIClientBase { + // return the original client class + get DataClient() { + return RegistryClient; + } // used to set the cluster-client related parameters, equivalent to the second parameter of the cluster method + + get clusterOptions() { + return { + responseTimeout: 120 * 1000, + }; + } + + subscribe(reg, listener) { + this._client.subscribe(reg, listener); + } + + publish(reg) { + this._client.publish(reg); + } + + get(key) { + return this._cache[key]; + } +} +``` + +in conclusion: + +```bash ++------------------------------------------------+ +| APIClient | +| +----------------------------------------| +| | ClusterClient | +| | +---------------------------------| +| | | RegistryClient | ++------------------------------------------------+ +``` + +- RegistryClient - responsible for communicating with remote service, to access data, supports for asynchronous APIs only, and does't care about multi-process model. +- ClusterClient - a client instance that is simply wrapped by the `cluster-client` module and is responsible for automatically flushing differences in multi-process model. +- APIClient - internally calls `ClusterClient` to synchronize data, without the need to concern about multi-process model and is the final exposed module for users. APIs are exposed Through this, and support for synchronization and asynchronization. + +Students who are interested may have look at [enhanced multi-process development model](https://github.com/eggjs/egg/issues/322) discussion process. + +## The Configuration Items Related to Cluster-Client in the Framework + +```js +/** + * @property {Number} responseTimeout - response timeout, default is 60000 + * @property {Transcode} [transcode] + * - {Function} encode - custom serialize method + * - {Function} decode - custom deserialize method + */ +config.clusterClient = { + responseTimeout: 60000, +}; +``` + +| Configuration Items | Type | Default | Description | +| ------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| responseTimeout | number | 60000 (one minute) | Global interprocess communication timeout, you cannot set too short, because the proxy interface itself has a timeout setting | +| transcode | function | N/A | Serialization of interprocess communication, by default [serialize-json](https://www.npmjs.com/package/serialize-json) (set up manually is not recommended) | + +The above is about global configuration. If you want to do a separate setting for a client: + +- You can override by setting the second argument `options` in `app/agent.cluster(ClientClass, options)`: + +```js +app.registryClient = app + .cluster(RegistryClient, { + responseTimeout: 120 * 1000, // the parameters passing here are related to cluster-client + }) + .create({ + // here are parameters required by RegistryClient + }); +``` + +- You can also override the `getter` attribute of `clusterOptions` in `APIClientBase`: + +```js +const APIClientBase = require('cluster-client').APIClientBase; +const RegistryClient = require('./registry_client'); + +class APIClient extends APIClientBase { + get DataClient() { + return RegistryClient; + } + + get clusterOptions() { + return { + responseTimeout: 120 * 1000, + }; + } + + // ... +} + +module.exports = APIClient; +``` diff --git a/site/docs/advanced/cluster-client.zh-CN.md b/site/docs/advanced/cluster-client.zh-CN.md new file mode 100644 index 0000000000..9e6ba1a33a --- /dev/null +++ b/site/docs/advanced/cluster-client.zh-CN.md @@ -0,0 +1,557 @@ +--- +title: 多进程研发模式增强 +order: 4 +--- + +在前面的 [多进程模型章节](../core/cluster-and-ipc.md) 中,我们详细讲述了框架的多进程模型。适合使用 Agent 进程的,有一类常见的场景:一些中间件客户端需要和服务器建立长连接。理论上,一台服务器最好只建立一个长连接,但多进程模型会导致 n 倍(n = Worker 进程数)的连接被创建。 + +```bash ++--------+ +--------+ +| Client | | Client | ... n ++--------+ +--------+ + | \ / | + | \ / | n * m 个链接 + | / \ | + | / \ | ++--------+ +--------+ +| Server | | Server | ... m ++--------+ +--------+ +``` + +为了尽可能地复用长连接(因为它们对于服务端来说是非常宝贵的资源),我们会把它放到 Agent 进程里维护,然后通过 messenger 将数据传递给各个 Worker。这种做法是可行的,但往往需要写大量代码去封装接口和实现数据的传递,非常麻烦。 + +另外,通过 messenger 传递数据效率较低,因为它会通过 Master 来做中转;万一 IPC 通道出现问题,还可能把 Master 进程弄挂。 + +那么有没有更好的方法呢?答案是肯定的,我们提供了一种新的模式来降低这类客户端封装的复杂度。通过建立 Agent 和 Worker 的 socket 直连,跳过 Master 的中转,Agent 作为对外的门面,维持多个 Worker 进程的共享连接。 +## 核心思想 + +- 受到 [Leader/Follower](https://www.dre.vanderbilt.edu/~schmidt/PDF/lf.pdf) 模式的启发。 +- 客户端会被区分为两种角色: + - Leader:负责和远程服务端维持连接,对于同一类的客户端只有一个 Leader。 + - Follower:会将具体的操作委托给 Leader,常见的是订阅模型(让 Leader 和远程服务端交互,并等待其返回)。 +- 如何确定谁是 Leader,谁是 Follower 呢?有两种模式: + - 自由竞争模式:客户端启动时通过本地端口的争夺来确定 Leader。例如:大家都尝试监听 7777 端口,最后只有一个实例抢占到,那它就变成了 Leader,其余的都是 Follower。 + - 强制指定模式:框架指定某一个 Leader,其余的就是 Follower。 +- 框架里面我们采用的是强制指定模式,Leader 只能在 Agent 里面创建,这也符合我们对 Agent 的定位。 +- 框架启动时,Master 会随机选择一个可用的端口作为 Cluster Client 监听的通讯端口,并将它通过参数传递给 Agent 和 App Worker。 +- Leader 和 Follower 之间通过 socket 直连(通过通讯端口),不再需要 Master 中转。 + +新的模式下,客户端的通信方式如下: + +```js + +-------+ + | start | + +---+---+ + | + +--------+---------+ + __| 端口竞争 |__ +win / +------------------+ \ lose + / \ ++---------------+ tcp 连接 +-------------------+ +| Leader(Agent) |<---------------->| Follower(Worker1) | ++---------------+ +-------------------+ + | \ tcp 连接 + | \ ++--------+ +-------------------+ +| Client | | Follower(Worker2) | ++--------+ +-------------------+ +``` +## 客户端接口类型抽象 + +我们将客户端接口抽象为以下两大类,这也是对客户端接口的一个规范,对于符合规范的客户端,我们可以自动将其包装为 Leader/Follower 模式。 + +- 订阅、发布类(subscribe / publish): + - `subscribe(info, listener)` 接口包含两个参数,第一个是订阅的信息,第二个是订阅的回调函数。 + - `publish(info)` 接口包含一个参数,即订阅的信息。 + +- 调用类(invoke),支持 `callback`,`Promise` 和 `async function` 三种风格的接口,但是推荐使用 `async function`。 + +客户端示例 + +```js +const Base = require('sdk-base'); + +class Client extends Base { + constructor(options) { + super(options); + // 在初始化成功后,记得要调用 ready + this.ready(true); + } + + /** + * 订阅 + * + * @param {Object} info - 订阅的信息(一个 JSON 对象,注意尽量不包含 Function、Buffer、Date 这类属性) + * @param {Function} listener - 监听的回调函数,它接收一个参数,就是监听到的结果对象。 + */ + subscribe(info, listener) { + // ... + } + + /** + * 发布 + * + * @param {Object} info - 发布的信息,与 subscribe 方法中的 info 类似。 + */ + publish(info) { + // ... + } + + /** + * 获取数据(invoke) + * + * @param {String} id - ID + * @return {Object} - 结果对象 + */ + async getData(id) { + // ... + } +} +``` +## 异常处理 + +- 如果 Leader 实例“死掉”,将触发新一轮的端口争夺。争夺到端口的实例将被推举为新的 Leader。 +- 为了保证 Leader 和 Follower 之间通道的健康,需要引入定时的心跳检查机制。如果 Follower 在固定时间内未发送心跳包,Leader 会将其主动断开,以触发 Follower 的重新初始化。 +## 协议和调用时序 + +Leader 和 Follower 通过下面的协议进行数据交换: + +```js + 0 1 2 4 12 + +-------+-------+---------------+---------------------------------------------------------------+ + | version | req/res | reserved | request id | + +-------------------------------+-------------------------------+-------------------------------+ + | timeout | connection object length | application object length | + +-------------------------------+---------------------------------------------------------------+ + | conn object (JSON format) ... | app object | + +-----------------------------------------------------------+ | + | ... | + +-----------------------------------------------------------------------------------------------+ +``` + +1. 在通讯端口上,Leader 启动一个 Local Server,所有的 Leader/Follower 通讯都经过 Local Server。 +2. Follower 连接上 Local Server 后,首先发送一个 register channel 的 packet(引入 channel 的概念是为了区分不同类型的客户端)。 +3. Local Server 会将 Follower 分配给指定的 Leader(根据客户端类型进行配对)。 +4. Follower 向 Leader 发送订阅、发布请求。 +5. Leader 在订阅数据变更时,通过 subscribe result packet 通知 Follower。 +6. Follower 向 Leader 发送调用请求,Leader 收到后执行相应操作并返回结果。 + +```js + +----------+ +---------------+ +---------+ + | Follower | | Local Server | | Leader | + +----------+ +---------------+ +---------+ + | register channel | assign to | + + -----------------------> | --------------------> | + | | | + | subscribe | + + ------------------------------------------------> | + | publish | + + ------------------------------------------------> | + | | + | subscribe result | + | <------------------------------------------------ + + | | + | invoke | + + ------------------------------------------------> | + | invoke result | + | <------------------------------------------------ + + | | +``` +## 具体的使用方法 + +下面我用一个简单的例子,介绍在框架里面如何让一个客户端支持 Leader/Follower 模式: + +- 第一步,我们的客户端最好是符合上面提到过的接口约定,例如: + +```js +// registry_client.js +const URL = require('url'); +const Base = require('sdk-base'); + +class RegistryClient extends Base { + constructor(options) { + super({ + // 指定异步启动的方法 + initMethod: 'init', + }); + this._options = options; + this._registered = new Map(); + } + + /** + * 启动逻辑 + */ + async init() { + this.ready(true); + } + + /** + * 获取配置 + * @param {String} dataId - 数据 ID + * @return {Object} 配置 + */ + async getConfig(dataId) { + return this._registered.get(dataId); + } + + /** + * 订阅 + * @param {Object} reg + * - {String} dataId - 数据 ID + * @param {Function} listener - 监听器函数 + */ + subscribe(reg, listener) { + const key = reg.dataId; + this.on(key, listener); + + const data = this._registered.get(key); + if (data) { + process.nextTick(() => listener(data)); + } + } + + /** + * 发布 + * @param {Object} reg + * - {String} dataId - 数据 ID + * - {String} publishData - 要发布的数据 + */ + publish(reg) { + const key = reg.dataId; + let changed = false; + + if (this._registered.has(key)) { + const arr = this._registered.get(key); + if (arr.indexOf(reg.publishData) === -1) { + changed = true; + arr.push(reg.publishData); + } + } else { + changed = true; + this._registered.set(key, [reg.publishData]); + } + if (changed) { + this.emit( + key, + this._registered.get(key).map(url => URL.parse(url, true)), + ); + } + } +} + +module.exports = RegistryClient; +``` + +- 第二步,使用 `agent.cluster` 接口对 `RegistryClient` 进行封装: + +```js +// agent.js +const RegistryClient = require('./registry_client'); + +module.exports = agent => { + // 对 RegistryClient 进行封装和实例化 + agent.registryClient = agent + .cluster(RegistryClient) + // create 方法的参数就是 RegistryClient 构造函数的参数 + .create({}); + + agent.beforeStart(async () => { + await agent.registryClient.ready(); + agent.coreLogger.info('注册客户端已就绪'); + }); +}; +``` + +- 第三步,使用 `app.cluster` 接口对 `RegistryClient` 进行封装: + +```js +// app.js +const RegistryClient = require('./registry_client'); + +module.exports = app => { + app.registryClient = app.cluster(RegistryClient).create({}); + app.beforeStart(async () => { + await app.registryClient.ready(); + app.coreLogger.info('注册客户端已就绪'); + + // 调用 subscribe 进行订阅 + app.registryClient.subscribe( + { + dataId: 'demo.DemoService', + }, + val => { + // ... + }, + ); + + // 调用 publish 发布数据 + app.registryClient.publish({ + dataId: 'demo.DemoService', + publishData: 'xxx', + }); + + // 调用 getConfig 获取配置 + const res = await app.registryClient.getConfig('demo.DemoService'); + console.log(res); + }); +}; +``` + +是不是很简单? + +当然,如果你的客户端不是那么“标准”,那你可能需要用到其他一些 API,比如你的订阅函数不叫 `subscribe` 而是叫 `sub`: + +```js +class MockClient extends Base { + constructor(options) { + super({ + initMethod: 'init', + }); + this._options = options; + this._registered = new Map(); + } + + async init() { + this.ready(true); + } + + sub(info, listener) { + const key = info.dataId; + this.on(key, listener); + + const data = this._registered.get(key); + if (data) { + process.nextTick(() => listener(data)); + } + } + + // ... +} +``` + +你需要通过 `delegate`(API 代理)手动设置这个委托: + +```js +// agent.js +module.exports = agent => { + agent.mockClient = agent + .cluster(MockClient) + // 将 sub 代理到 subscribe + .delegate('sub', 'subscribe') + .create(); + + agent.beforeStart(async () => { + await agent.mockClient.ready(); + }); +}; +``` + +```js +// app.js +module.exports = app => { + app.mockClient = app + .cluster(MockClient) + // 将 sub 代理到 subscribe + .delegate('sub', 'subscribe') + .create(); + + app.beforeStart(async () => { + await app.mockClient.ready(); + + app.mockClient.sub({ id: 'test-id' }, val => { + // 请把你的代码放在这里 + }); + }); +}; +``` + +我们已经理解,通过 `cluster-client` 可以让我们在不理解多进程模型的情况下开发“纯粹”的 `RegistryClient`,只负责和服务端进行交互,然后使用 `cluster-client` 进行简单的封装就可以得到一个支持多进程模型的 `ClusterClient`。这里的 `RegistryClient` 实际上是一个专门负责和远程服务通信进行数据通信的 `DataClient`。 + +大家可能已经发现,`ClusterClient` 同时带来了一些约束,如果想在各进程暴露同样的方法,那么 `RegistryClient` 上只能支持 sub/pub 模式以及异步的 API 调用。因为在多进程模型中所有的交互都必须经过 socket 通信,势必带来了这一约束。 + +假设我们要实现一个同步的 get 方法,订阅过的数据直接放入内存,使用 get 方法时直接返回。要怎么实现呢?而真实情况可能比这更复杂。 + +在这里,我们引入一个 `APIClient` 的最佳实践。对于有读取缓存数据等同步 API 需求的模块,在 `RegistryClient` 基础上再封装一个 `APIClient` 来实现这些与远程服务端交互无关的 API,暴露给用户使用的是这个 `APIClient` 的实例。 + +在 `APIClient` 内部实现上: + +- 异步数据获取,通过调用基于 `ClusterClient` 的 `RegistryClient` 的 API 实现。 +- 同步调用等与服务端无关的接口在 `APIClient` 上实现。由于 `ClusterClient` 的 API 已经抹平了多进程差异,所以在开发 `APIClient` 调用到 `RegistryClient` 时也无需关心多进程模型。 + +例如,在模块的 `APIClient` 中增加带缓存的 get 同步方法: + +```js +// some-client/index.js +const cluster = require('cluster-client'); +const RegistryClient = require('./registry_client'); + +class APIClient extends Base { + constructor(options) { + super(options); + + // options.cluster 用于给 Egg 的插件传递 app.cluster + this._client = (options.cluster || cluster)(RegistryClient).create(options); + this._client.ready(() => this.ready(true)); + + this._cache = {}; + + // subMap 的例子: + // { + // foo: reg1, + // bar: reg2, + // } + const subMap = options.subMap; + + for (const key in subMap) { + this.subscribe(subMap[key], value => { + this._cache[key] = value; + }); + } + } + + subscribe(reg, listener) { + this._client.subscribe(reg, listener); + } + + publish(reg) { + this._client.publish(reg); + } + + get(key) { + return this._cache[key]; + } +} + +// 最终模块向外暴露这个 `APIClient` +module.exports = APIClient; +``` + +那么,我们就可以这样使用这个模块: + +```js +// app.js 或 agent.js +const APIClient = require('some-client'); // 上文中的模块 +module.exports = app => { + const config = app.config.apiClient; + app.apiClient = new APIClient(Object.assign({}, config, { cluster: app.cluster })); + app.beforeStart(async () => { + await app.apiClient.ready(); + }); +}; + +// config.${env}.js +exports.apiClient = { + subMap: { + foo: { + id: '', + }, + // bar... + } +}; +``` + +为了方便你封装 `APIClient`,在 `cluster-client` 模块中提供了一个 `APIClientBase` 基类,那么上文中的 `APIClient` 可以改写为: + +```js +const APIClientBase = require('cluster-client').APIClientBase; +const RegistryClient = require('./registry_client'); + +class APIClient extends APIClientBase { + // 返回原始的客户端类 + get DataClient() { + return RegistryClient; + } + + // 用于设置 cluster-client 相关参数,等同于 cluster 方法的第二个参数 + get clusterOptions() { + return { + responseTimeout: 120 * 1000, + }; + } + + subscribe(reg, listener) { + this._client.subscribe(reg, listener); + } + + publish(reg) { + this._client.publish(reg); + } + + get(key) { + return this._cache[key]; + } +} +``` + +总结一下: + +```plaintext ++------------------------------------------------+ +| APIClient | +| +----------------------------------------| +| | ClusterClient | +| | +---------------------------------| +| | | RegistryClient | ++------------------------------------------------+ +``` + +- `RegistryClient` - 负责和远端服务通讯,实现数据的存取,只支持异步 API,不关心多进程模型。 +- `ClusterClient` - 通过 `cluster-client` 模块进行简单包装得到的客户端实例,负责自动抹平多进程模型的差异。 +- `APIClient` - 内部调用 `ClusterClient` 做数据同步,无需关心多进程模型,用户最终使用的模块。API 通过此处暴露,支持同步和异步。 + +有兴趣的同学可以查看《增强多进程研发模式》讨论过程。 +## 在框架里面 cluster-client 相关的配置项 + +```js +/** + * @property {Number} responseTimeout - 响应超时,默认值为 60000 + * @property {Transcode} [transcode] + * - {Function} encode - 自定义序列化方法 + * - {Function} decode - 自定义反序列化方法 + */ +config.clusterClient = { + responseTimeout: 60000, +}; +``` + +| 配置项 | 类型 | 默认值 | 描述 | +| --------------- | -------- | ------------------ | ------------------------------------------------------------------ | +| responseTimeout | number | 60000(一分钟) | 全局的进程间通讯的超时时长,因为代理接口本身也有超时设置,所以不宜设置太短 | +| transcode | function | 未设置(N/A) | 进程间通讯的序列化方式,默认使用 [serialize-json](https://www.npmjs.com/package/serialize-json),建议不要自行设置 | + +上述表格为全局配置方式。如果你想为特定客户端单独设置,可以使用以下方法: + +- 可以通过 `app/agent.cluster(ClientClass, options)` 的第二个参数 `options` 进行覆盖。 + +```js +app.registryClient = app + .cluster(RegistryClient, { + responseTimeout: 120 * 1000, // 这里传入的是与 cluster-client 相关的参数 + }) + .create({ + // 这里传入的是 RegistryClient 需要的参数 + }); +``` + +- 也可以通过覆盖 `APIClientBase` 的 `clusterOptions` 这个 `getter` 属性。 + +```js +const APIClientBase = require('cluster-client').APIClientBase; +const RegistryClient = require('./registry_client'); + +class APIClient extends APIClientBase { + get DataClient() { + return RegistryClient; + } + + get clusterOptions() { + return { + responseTimeout: 120 * 1000, + }; + } + + // ... +} + +module.exports = APIClient; +``` \ No newline at end of file diff --git a/site/docs/advanced/framework.md b/site/docs/advanced/framework.md new file mode 100644 index 0000000000..3eb3e8c91f --- /dev/null +++ b/site/docs/advanced/framework.md @@ -0,0 +1,374 @@ +--- +title: Framework Development +order: 3 +--- + +If your team have met with these scenarios: + +- Each project contains the same configuration files that need to be copied every time, such as `gulpfile.js`, `webpack.config.js`. +- Each project has similiar dependencies. +- It's difficult to synchronize those projects based on the same configurations like those mentioned above once they have been optimized? + +If your team needs: + +- a unified technique selection, such as the choice of databases, templates, frontend frameworks, and middlewares. +- a unified default configuration to balance the deviation of different situations, which are not supposed to resolve in code level, like the differences between companies and open communities. +- a unified [deployment plan](../core/deployment.md) keeping developers concentrate on code without paying attention to deployment details of connecting the framework and platforms. +- a unified code style to decrease code's repetition and optimize code's appearance, which is important for a enterprise level framework. + +To satisfy these demands, Egg endows developers with the capacity of `customazing a framework`. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Futhermore, Egg apply a quantity of coding conventions based on Koa. + +Therefore, a uniform spec can be applied on projects in which the differentiation fulfilled in plugins. And the best practice summed from those projects can be continuously extracted from these plugins to the framework, which is available to other projects by just updating the dependencies' versions. + +See more details in [Progressive Development](../intro/progressive.md)。 + +## Framework and Multiprocess + +The framework extension is applied to Multiprocess Model, as we know [Multiprocess Model](../core/cluster-and-ipc.md) and the differences between Agent Worker and App Worker, which have different APIs and both need to inherit. + +They both are inherited from [EggCore](https://github.com/eggjs/egg-core), and Agent is instantiated during the initiation of Agent Worker, while App is instantiated during the initiation of App Worker. + +We could regard EggCore as the advanced version of Koa Application, which integrates built-in features such as [Loader](./loader.md)、[Router](../basics/router.md) and asynchronous launch. + +``` + Koa Application + ^ + EggCore + ^ + ┌──────┴───────┐ + │ │ + Egg Agent Egg Application + ^ ^ + agent worker app worker +``` + +## How to Customize a Framework + +Just use [egg-boilerplate-framework](https://github.com/eggjs/egg-boilerplate-framework) to generates a scaffold for you. + +```bash +$ mkdir yadan && cd yadan +$ npm init egg --type=framework +$ npm i +$ npm test +``` + +But in order to illustrate details, let's do it step by step. Here is the [sample code](https://github.com/eggjs/examples/tree/master/framework). + +### Framework API + +Each of those APIs is required to be implemented almost twice - one for Agent and another for Application. + +#### `egg.startCluster` + +This is the entry function of Egg's multiprocess launcher, based on [egg-cluster](https://github.com/eggjs/egg-cluster), to start Master, but EggCore running in a single process doesn't invoke this function while Egg does. + +```js +const startCluster = require('egg').startCluster; +startCluster( + { + // directory of code + baseDir: '/path/to/app', + // directory of framework + framework: '/path/to/framework', + }, + () => { + console.log('app started'); + }, +); +``` + +All available options could be found in [egg-cluster](https://github.com/eggjs/egg-cluster#options). + +#### `egg.Application` And `egg.Agent` + +These are both singletons but still different with each other. To inherit framework, it's likely to inherited these two classes. + +#### `egg.AppWorkerLoader` and `egg.AgentWorkerLoader` + +To customize framework, Loader is required and has to be inherited from Egg Loader for the propose of either loading directories or rewriting functions. + +### Framework Extension + +If we consider a framework as a class, then Egg framework is the base class,and implementing a framework demands to implement entire APIs of Egg. + +```js +// package.json +{ + "name": "yadan", + "dependencies": { + "egg": "^2.0.0" + } +} + +// index.js +module.exports = require('./lib/framework.js'); + +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends egg.Application { + get [EGG_PATH]() { + // return the path of framework + return path.dirname(__dirname); + } +} + +// rewrite Egg's Application +module.exports = Object.assign(egg, { + Application, +}); +``` + +The name of framework, default as `egg`, is a indispensable option to launch an application, set by `egg.framwork` of `package.json`, then Loader loads the exported app of a module named it. + +```json +{ + "scripts": { + "dev": "egg-bin dev" + }, + "egg": { + "framework": "yadan" + } +} +``` + +As a loadUnit of framework, yadan is going to load specific directories and files, such as `app` and `config`. Find more files loaded at [Loader](./loader.md). + +### Principle of Framework Extension + +The path of framework is set as a varible named as `Symbol.for('egg#eggPath')` to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessiable through the prototype chain. + +Given a triple-layer framework: department level > enterprise level > Egg + +```js +// enterprise +const Application = require('egg').Application; +class Enterprise extends Application { + get [EGG_PATH]() { + return '/path/to/enterprise'; + } +} +// Customize Application +exports.Application = Enterprise; + +// department +const Application = require('enterprise').Application; +// extend enterprise's Application +class department extends Application { + get [EGG_PATH]() { + return '/path/to/department'; + } +} + +// the path of `department` have to be designated as described above +const Application = require('department').Application; +const app = new Application(); +app.ready(); +``` + +These code are pseudocode to elaborate the framework's loading process, and we have provided scaffolds to [development](../core/development.md) and [deployment](../core/deployment.md). + +### Custom Agent + +Egg's mutilprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similiar to Application, is also required to be implemented. + +```js +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends egg.Application { + get [EGG_PATH]() { + // return the path of framework + return path.dirname(__dirname); + } +} + +class Agent extends egg.Agent { + get [EGG_PATH]() { + return path.dirname(__dirname); + } +} + +// rewrite Egg's Application +module.exports = Object.assign(egg, { + Application, + Agent, +}); +``` + +**To be careful about that Agent and Application based on the same Class possess different APIs.** + +### Custom Loader + +Loader, the core of the launch process, is capable of loading data code, adjusting loading orders or even strengthen regulation of code. + +As the same as Egg-Path, Loader exposes itself at `Symbol.for('egg#loader')` to ensuer it's accessibility on prototype chain. + +```js +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class YadanAppWorkerLoader extends egg.AppWorkerLoader { + load() { + super.load(); + // do something + } +} + +class Application extends egg.Application { + get [EGG_PATH]() { + // return the path of framework + return path.dirname(__dirname); + } + // supplant default Loader + get [EGG_LOADER]() { + return YadanAppWorkerLoader; + } +} + +// rewrite Egg's Application +module.exports = Object.assign(egg, { + Application, + // custom Loader, a dependence of the high level frameword, needs to be exported. + AppWorkerLoader: YadanAppWorkerLoader, +}); +``` + +AgentworkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at `agent.js` instand of `app.js`. + +## The principle of Launch + +Many descriptions of launch process are scattered at [Mutilprocess Model](../core/cluster-and-ipc.md), [Loader](./loader.md) and [Plugin](./plugin.md), and here is a summarization. + +- `startCluster` is invoked with `baseDir` and `framework`, then Master process is launched. +- Master forks a new process as Agent Worker + - instantiate Agent Class of the framework loaded from path passed by the `framework` param. + - Agent finds out the AgentWorkerLoader and then starts to load + - use AgentWorkerLoader to load Worker synchronously in the sequence of Plugin Config, Extend, `agent.js` and other files. + - The initiation of `agent.js` is able to be customized, and it supports asynchronous launch after which it notifies Master and invoke the function passed to `beforeStart`. +- After recieving the message that Agent Worker is launched,Master forks App Workers by cluster. + - App Workers are mutilple identical processes launched simultaneously + - App Worker is instantiated, which is similiar to Agent inherited Application class of framework loaded from framework path. + - The same as Agent, Loading process of Application starts with AppWorkerLoader which loads files in the same order and finally informed Master. +- After informed of luanching successfully of each App Worker, Master is finally functioning. + +## Framework Testing + +You'd better read [unittest](../core/unittest.md) first, which is similiar to framework testing in a quantity of situations. + +### Initiation + +Here are some differences between initiation of frameworks. + +```js +const mock = require('egg-mock'); +describe('test/index.test.js', () => { + let app; + before(() => { + app = mock.app({ + // test/fixtures/apps/example + baseDir: 'apps/example', + // importent !! Do not miss + framework: true, + }); + return app.ready(); + }); + + after(() => app.close()); + afterEach(mock.restore); + + it('should success', () => { + return app.httpRequest().get('/').expect(200); + }); +}); +``` + +- Different from application testing, framework testing tests framework code instead of application code, so that baseDir varies for the propose of testing kinds of applications. +- BaseDir is potentially considered to be under the path of `test/fixtures`, otherwise it should be absolute paths. +- The `framework` option is indispensable, which could be a absolute path or `true` meaning the path of the framework to be current directory. +- The use of the app should wait for the `ready` event in `before` hook, or some of the APIs is not avaiable. +- Do not forget to invoke `app.close()` after testing, which could arouse the exhausting of fds, caused by unclosed log files. + +### Cache + +`mm.app` enables cache as default, which means new envoriment setting would not work once loaded. + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + afterEach(() => app.close()); + + it('should test on local', () => { + mock.env('local'); + app = mock.app({ + baseDir: 'apps/example', + framework: true, + cache: false, + }); + return app.ready(); + }); + it('should test on prod', () => { + mock.env('prod'); + app = mock.app({ + baseDir: 'apps/example', + framework: true, + cache: false, + }); + return app.ready(); + }); +}); +``` + +### Multipleprocess Testing + +Mutilprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model. + +The option of `mock.cluster` have no difference with `mm.app` while their APIs are totally distinct, however, SuperTest still works. + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + before(() => { + app = mock.cluster({ + baseDir: 'apps/example', + framework: true, + }); + return app.ready(); + }); + after(() => app.close()); + afterEach(mock.restore); + it('should success', () => { + return app.httpRequest().get('/').expect(200); + }); +}); +``` + +Tests of `stdout/stderr` are also avaiable, since `mm.cluster` is based on [coffee](https://github.com/popomore/coffee) in which multiprocess testing is supported. + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + before(() => { + app = mock.cluster({ + baseDir: 'apps/example', + framework: true, + }); + return app.ready(); + }); + after(() => app.close()); + it('should get `started`', () => { + // set the expectation of console + app.expect('stdout', /started/); + }); +}); +``` diff --git a/site/docs/advanced/framework.zh-CN.md b/site/docs/advanced/framework.zh-CN.md new file mode 100644 index 0000000000..99dc71efac --- /dev/null +++ b/site/docs/advanced/framework.zh-CN.md @@ -0,0 +1,374 @@ +--- +title: 框架开发 +order: 3 +--- + +如果你的团队遇到过: + +- 维护很多个项目,每个项目都需要复制拷贝诸如 `gulpfile.js`、`webpack.config.js` 之类的文件。 +- 每个项目都需要使用一些相同的类库,相同的配置。 +- 在新项目中对上面的配置做了一个优化后,如何同步到其他项目? + +如果你的团队需要: + +- 统一的技术选型,比如数据库、模板、前端框架及各种中间件设施都需要选型,而框架封装后保证应用使用一套架构。 +- 统一的默认配置,开源社区的配置可能不适用于公司,而又不希望应用去配置。 +- 统一的部署方案,通过框架和平台的双向控制,应用只需要关注自己的代码,具体查看[应用部署](../core/deployment.md)。 +- 统一的代码风格,框架不仅仅解决代码重用问题,还可以对应用做一定约束,作为企业框架是很必要的。Egg 在 Koa 的基础上做了很多约定,框架可以使用 [Loader](./loader.md) 自己定义代码规则。 + +为此,Egg 为团队架构师和技术负责人提供 `框架定制` 的能力,框架是一层抽象,可以基于 Egg 去封装上层框架,并且 Egg 支持多层继承。 + +这样,整个团队就可以遵循统一的方案,并且在项目中可以根据业务场景自行使用插件做差异化;当后者验证为最佳实践后,就能下沉到框架中,其他项目仅需简单的升级框架版本即可享受到。 + +具体可以参见[渐进式开发](../intro/progressive.md)。 + +## 框架与多进程 + +框架的扩展与多进程模型有关,我们已经了解了[多进程模型](../core/cluster-and-ipc.md),也知道 `Agent Worker` 和 `App Worker` 的区别。因此,我们需要扩展的类也有两个:`Agent` 和 `Application`,这两个类的 API 不一定相同。 + +在 `Agent Worker` 启动的时候会实例化 `Agent`,而在 `App Worker` 启动时会实例化 `Application`。这两个类又同时继承自 [EggCore](https://github.com/eggjs/egg-core)。 + +EggCore 可以看做是 Koa `Application` 的升级版,默认内置了 [Loader](./loader.md)、[Router](../basics/router.md) 及应用异步启动等功能,可以看作是支持 `Loader` 的 Koa。 + +``` + Koa Application + ^ + EggCore + ^ + ┌──────┴───────┐ + │ │ + Egg Agent Egg Application + ^ ^ + agent worker app worker +``` +## 如何定制一个框架 + +你可以直接通过 [egg-boilerplate-framework](https://github.com/eggjs/egg-boilerplate-framework) 脚手架快速上手。 + +```bash +$ mkdir yadan && cd yadan +$ npm init egg --type=framework +$ npm i +$ npm test +``` + +但同样,为了让大家了解细节,接下来我们会手把手地来定制一个框架,具体代码可以查看[示例](https://github.com/eggjs/examples/tree/master/framework)。 + +### 框架 API + +Egg 框架提供了一些 API,所有继承的框架都需要提供,只增不减。这些 API 基本都存在于 Agent 和 Application 两份实现中。 + +#### `egg.startCluster` + +Egg 的多进程启动器,通过这个方法来启动 Master,主要的功能实现在 [egg-cluster](https://github.com/eggjs/egg-cluster) 上。因此,直接使用 EggCore 是单进程方式启动的,而 Egg 实现了多进程模式。 + +```js +const startCluster = require('egg').startCluster; +startCluster({ + // 应用的代码目录 + baseDir: '/path/to/app', + // 需要通过这个参数来指定框架目录 + framework: '/path/to/framework', +}, () => { + console.log('app started'); +}); +``` + +所有参数可以查看 [egg-cluster](https://github.com/eggjs/egg-cluster#options)。 + +#### `egg.Application` 和 `egg.Agent` + +它们是进程中的唯一实例,但 Application 和 Agent 存在一定差异。如果框架继承自 Egg,会定制这两个类,那么框架应该导出(export)这两个类。 + +#### `egg.AppWorkerLoader` 和 `egg.AgentWorkerLoader` + +框架也可能会有定制 Loader 的场景,如覆盖原方法或新加载目录,都需要提供自己的 Loader。而且必须继承自 Egg 的 Loader。 + +### 框架继承 + +框架支持继承关系,可以把框架类比于一个类,那么基类就是 Egg 框架。如果你想对 Egg 进行扩展,那么可以继承它。 + +首先,定义一个框架需要实现 Egg 所有的 API: + +```js +// package.json +{ + "name": "yadan", + "dependencies": { + "egg": "^2.0.0" + } +} + +// index.js +module.exports = require('./lib/framework.js'); + +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends egg.Application { + get [EGG_PATH]() { + // 返回框架路径 + return path.dirname(__dirname); + } +} + +module.exports = Object.assign(egg, { + Application, + // 可能还会有其他扩展 +}); +``` + +应用启动时需要指定框架名(在 `package.json` 中指定 `egg.framework`,默认值是 `egg`),Loader 会从 `node_modules` 中寻找指定模块作为框架,并载入其导出的 Application。 + +```json +{ + "scripts": { + "dev": "egg-bin dev" + }, + "egg": { + "framework": "yadan" + } +} +``` + +现在,yadan 框架的目录已经成为了一个加载单元(loadUnit),因此相应的目录和文件(如 `app` 和 `config`)都会被加载。详情可以查看[框架被加载的文件](./loader.md)。 + +### 框架继承原理 + +使用 `Symbol.for('egg#eggPath')` 来指定当前框架的路径,目的是让 Loader 能探测到框架路径。为什么采取这种实现方式?本可以将框架路径直接传给 Loader,但为了实现多级框架继承,每一层框架都要提供自己的路径,且继承有其顺序。 + +现在的实现方案是基于类继承的。每一层框架都必须继承上一层框架,并且指定 eggPath,之后遍历原型链,就可以获取到每一层框架的路径了。 + +比如有三层框架:部门框架(department)> 企业框架(enterprise)> Egg + +```js +// enterprise +const Application = require('egg').Application; +class EnterpriseApplication extends Application { + get [EGG_PATH]() { + return '/path/to/enterprise'; + } +} +// 自定义模块的 Application +exports.Application = EnterpriseApplication; + +// department +const EnterpriseApplication = require('enterprise').Application; +// 继承自 enterprise 的 Application +class DepartmentApplication extends EnterpriseApplication { + get [EGG_PATH]() { + return '/path/to/department'; + } +} + +// 启动时需要传入 department 的框架路径 +const DepartmentApplication = require('department').Application; +const app = new DepartmentApplication(); +app.ready(); +``` + +以上都是示例代码,用于解释框架路径加载过程。实际上,Egg 已经提供了[本地开发](../core/development.md)和[应用部署](../core/deployment.md)的优秀工具,不需要我们自行实现。 +下面是根据《优秀技术文档的写作标准》修改后的全文内容: + +### 自定义 Agent + +上面的例子自定义了 Application。由于 Egg 是多进程模型,因此还需要定义 Agent。原理是一样的。 + +```js +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends egg.Application { + get [EGG_PATH]() { + // 返回 framework 路径 + return path.dirname(__dirname); + } +} + +class Agent extends egg.Agent { + get [EGG_PATH]() { + return path.dirname(__dirname); + } +} + +// 覆盖了 Egg 的 Application +module.exports = Object.assign(egg, { + Application, + Agent, +}); +``` + +**但因为 Agent 和 Application 是两个实例,API 有可能不一致。** + +### 自定义 Loader + +Loader 是应用启动的核心。利用它,我们不仅能规范应用代码,还能基于这个类扩展更多功能,比如加载数据模型。扩展 Loader 还可以覆盖默认的实现,或调整现有的加载顺序等。 + +我们使用 `Symbol.for('egg#loader')` 来自定义 Loader,主要原因还是为了使用原型链。这样,上层框架可以覆盖底层 Loader。在上面的例子基础上: + +```js +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class YadanAppWorkerLoader extends egg.AppWorkerLoader { + load() { + super.load(); + // 进行自己的扩展 + } +} + +class Application extends egg.Application { + get [EGG_PATH]() { + // 返回 framework 路径 + return path.dirname(__dirname); + } + // 覆盖 Egg 的 Loader,启动时将使用这个 Loader + get [EGG_LOADER]() { + return YadanAppWorkerLoader; + } +} + +// 覆盖了 Egg 的 Application +module.exports = Object.assign(egg, { + Application, + // 自定义的 Loader 也需要导出,以便上层框架进行扩展 + AppWorkerLoader: YadanAppWorkerLoader, +}); +``` + +AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader 加载的文件可以与 AppWorkerLoader 不同。比如,默认加载时,Egg 的 AppWorkerLoader 会加载 `app.js`,而 AgentWorkerLoader 加载的是 `agent.js`。 + +## 框架启动原理 + +框架启动过程在[多进程模型](../core/cluster-and-ipc.md)、[Loader](./loader.md)、[插件](./plugin.md)中或多或少都有提及。这里我们系统地梳理一下启动顺序: + +1. `startCluster` 启动时传入 `baseDir` 和 `framework`,从而启动 Master 进程。 +2. Master 首先 fork Agent Worker: + - 根据 framework 找到框架目录,实例化该框架的 Agent 类。 + - Agent 根据定义的 AgentWorkerLoader 开始加载。 + - 整个 AgentWorkerLoader 的加载过程是同步的,按照 plugin > config > extend > `agent.js` > 其他文件的顺序进行。 + - 如果 `agent.js` 中定义了自定义初始化并支持异步启动,当执行完成后,会告知 Master 启动已完成。 +3. Master 在接到 Agent Worker 启动成功的消息后,会 fork App Worker: + - App Worker 由多个进程组成,这些进程会并行启动,但执行逻辑是一致的。 + - 单个 App Worker 通过 framework 找到框架目录,实例化该框架的 Application 类。 + - Application 根据 AppWorkerLoader 开始加载,加载顺序类似,会异步等待完成后通知 Master 启动完成。 +4. Master 在等到所有 App Worker 发来的启动成功消息后,完成启动,开始对外提供服务。 +## 框架测试 + +在看下文之前,请先查看[单元测试章节](../core/unittest.md)。框架测试的大部分使用场景和应用类似。 + +### 初始化 + +框架的初始化方式有一定差异。 + +```js +const mock = require('egg-mock'); +describe('test/index.test.js', () => { + let app; + before(() => { + app = mock.app({ + // 转换成 test/fixtures/apps/example + baseDir: 'apps/example', + // 重要:配置 framework + framework: true + }); + return app.ready(); + }); + + after(() => app.close()); + afterEach(mock.restore); + + it('should success', () => { + return app.httpRequest().get('/').expect(200); + }); +}); +``` + +- 框架和应用不同,应用测试当前代码,而框架是测试框架代码,所以会频繁更换 baseDir 达到测试各种应用的目的。 +- baseDir 有潜规则,我们一般会把测试的应用代码放到 `test/fixtures` 下,所以自动补全,也可以传入绝对路径。 +- 必须指定 `framework: true`,告知当前路径为框架路径;也可以传入绝对路径。 +- app 应用需要在 before 等待 ready,否则在 testcase 里无法获取部分 API。 +- 框架在测试完毕后,需要使用 `app.close()` 关闭,否则会有遗留问题,例如日志写文件未关闭导致 fd 不够。 + +### 缓存 + +在测试多环境场景需要使用到 cache 参数,因为 `mock.app` 默认有缓存,当第一次加载后再次加载会直接读取缓存,那么设置的环境也不会生效。 + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + afterEach(() => app.close()); + + it('should test on local', () => { + mock.env('local'); + app = mock.app({ + baseDir: 'apps/example', + framework: true, + cache: false + }); + return app.ready(); + }); + it('should test on prod', () => { + mock.env('prod'); + app = mock.app({ + baseDir: 'apps/example', + framework: true, + cache: false + }); + return app.ready(); + }); +}); +``` + +### 多进程测试 + +很少场景会使用多进程测试,因为多进程无法进行 API 级别的 mock,导致测试成本很高。而进程在有覆盖率的场景启动很慢,测试会超时。但多进程测试是验证多进程模型最好的方式。还可以测试 stdout 和 stderr。 + +多进程测试和 `mock.app` 参数一致,但 app 的 API 完全不同。不过,SuperTest 依然可用。 + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + before(() => { + app = mock.cluster({ + baseDir: 'apps/example', + framework: true + }); + return app.ready(); + }); + after(() => app.close()); + afterEach(mock.restore); + it('should success', () => { + return app.httpRequest().get('/').expect(200); + }); +}); +``` + +多进程测试还可以测试 stdout/stderr,因为 `mock.cluster` 是基于 [coffee](https://github.com/popomore/coffee) 扩展的,可进行进程测试。 + +```js +const mock = require('egg-mock'); +describe('/test/index.test.js', () => { + let app; + before(() => { + app = mock.cluster({ + baseDir: 'apps/example', + framework: true + }); + return app.ready(); + }); + after(() => app.close()); + it('should get `started`', () => { + // 判断终端输出 + app.expect('stdout', /started/); + }); +}); +``` diff --git a/site/docs/advanced/index.md b/site/docs/advanced/index.md new file mode 100644 index 0000000000..e2c25b4af2 --- /dev/null +++ b/site/docs/advanced/index.md @@ -0,0 +1,14 @@ +--- +title: Advanced +order: 0 +nav: + title: Advanced + order: 5 +--- + +- [Loader](./advanced/loader.md) +- [Plugin Development](./advanced/plugin.md) +- [Framework Development](./advanced/framework.md) +- [Multi-Process Development Model Enhancement](./advanced/cluster-client.md) +- [View Plugin Development](./advanced/view-plugin.md) +- [Upgrade your event functions in your lifecycle](./advanced/loader-update.md) diff --git a/site/docs/advanced/index.zh-CN.md b/site/docs/advanced/index.zh-CN.md new file mode 100644 index 0000000000..e6b31d62a0 --- /dev/null +++ b/site/docs/advanced/index.zh-CN.md @@ -0,0 +1,14 @@ +--- +title: 进阶 +order: 0 +nav: + title: 进阶 + order: 5 +--- + +- [加载器(Loader)](./advanced/loader.md) +- [插件开发](./advanced/plugin.md) +- [框架开发](./advanced/framework.md) +- [多进程研发模式增强](./advanced/cluster-client.md) +- [View 插件开发](./advanced/view-plugin.md) +- [升级你的生命周期事件函数](./advanced/loader-update.md) diff --git a/site/docs/advanced/loader-update.md b/site/docs/advanced/loader-update.md new file mode 100644 index 0000000000..05e0fe98bc --- /dev/null +++ b/site/docs/advanced/loader-update.md @@ -0,0 +1,103 @@ +--- +title: Upgrade your event functions in your lifecycle +order: 6 +--- + +We've simplified the functions of our lifecycle for your convenience to control when to load application or plugins. Generally speaking, the lifecycle events can be divided into two forms: + +1. Function (already deprecated, just for compatibility). +2. Class Method (recommanded). + +## Replacer for `beforeStart` + +We usually handle `beforeStart` through `module.export` with the input parameter `app` in the app.js. +Take this as an example below: + +```js +module.exports = (app) => { + app.beforeStart(async () => { + // Here's where your codes were before + }); +}; +``` + +Now we've got some changes after upgration: We can use methods in a class in `app.js`. For application, we should write in the `WillReady`, for plugins, `didLoad` is our choice. They look like below: + +```js +// app.js or agent.js: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async didLoad() { + // Please put your codes of `app.beforeStart` here for your plugin + } + + async willReady() { + // Please put your codes of `app.beforeStart` here for your application + } +} + +module.exports = AppBootHook; +``` + +## Replacer for `ready` + +We used to process our logic in `app.ready`: + +```js +module.exports = (app) => { + app.ready(async () => { + // Here's where your codes were before + }); +}; +``` + +Now `didReady` takes the place of it: + +```js +// app.js or agent.js: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async didReady() { + // Please put your codes of `app.ready` here + } +} + +module.exports = AppBootHook; +``` + +## Replacer for `beforeClose` + +We used to handle `app.beforeClose` like this following: + +```js +module.exports = (app) => { + app.beforeClose(async () => { + // Here's where your codes were before + }); +}; +``` + +Now we can use `beforeClose` instead of it: + +```js +// app.js or agent.js: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async beforeClose() { + // Please put your codes of `app.beforeClose` here + } +} +``` + +## Others + +In order to let you pick up quickly to replace your old functions, this torturial only tells you how to replace item by item. So if you want to know more about the whole principle of `Loader`, as well as all the functions of Egg's lifecycle, Please refer to [Loader](./loader.md) and [Application Startup Configuration](../basics/app-start.md). diff --git a/site/docs/advanced/loader-update.zh-CN.md b/site/docs/advanced/loader-update.zh-CN.md new file mode 100644 index 0000000000..e933ee7757 --- /dev/null +++ b/site/docs/advanced/loader-update.zh-CN.md @@ -0,0 +1,105 @@ +--- +title: 升级你的生命周期事件函数 +order: 6 +--- + +为了使得大家更方便地控制加载应用和插件的时机,我们对 Loader 的生命周期函数进行了精简处理。概括地说,生命周期事件目前总共可以分成两种形式: + +1. 函数形式(已经作废,仅为兼容保留)。 +2. 类形式(推荐使用)。 + +## beforeStart 函数替代 + +我们通常在 `app.js` 中通过 `module.exports` 中传入的 `app` 参数进行此函数的操作,一个典型的例子: + +```js +module.exports = (app) => { + app.beforeStart(async () => { + // 此处是你原来的逻辑代码 + }); +}; +``` + +现在升级之后的写法略有改变 —— 我们可以直接在 `app.js` 中用类方法的形式体现出来。对于应用开发而言,应该写在 `willReady` 方法中;对于插件则写在 `didLoad` 中。形式如下: + +```js +// app.js 或 agent.js 文件: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async didLoad() { + // 请将你的插件项目中 app.beforeStart 中的代码置于此处 + } + + async willReady() { + // 请将你的应用项目中 app.beforeStart 中的代码置于此处 + } +} + +module.exports = AppBootHook; +``` + + +## ready 函数替代 + +同样地,我们之前在 `app.ready` 中处理我们的逻辑: + +```js +module.exports = (app) => { + app.ready(async () => { + // 此处是你原来的逻辑代码 + }); +}; +``` + +现在直接用 `didReady` 进行替换: + +```js +// app.js 或 agent.js 文件: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async didReady() { + // 请将你的 app.ready 中的代码置于此处 + } +} + +module.exports = AppBootHook; +``` + + +## beforeClose 函数替代 + +原先的 `app.beforeClose` 如以下形式: + +```js +module.exports = (app) => { + app.beforeClose(async () => { + // 此处是你原来的逻辑代码 + }); +}; +``` + +现在我们只需使用类方法 `beforeClose` 替代即可: + +```js +// app.js 或 agent.js 文件: +class AppBootHook { + constructor(app) { + this.app = app; + } + + async beforeClose() { + // 请将你的 app.beforeClose 中的代码置于此处 + } +} +module.exports = AppBootHook; +``` + +## 其它说明 + +本教程只是一对一地讲了替换方法,便于开发者们快速上手进行替换。若想要具体了解整个 Loader 原理以及生命周期的完整函数版本,请参考《[加载器](./loader.md)》和《[启动自定义](../basics/app-start.md)》两篇文章。 diff --git a/site/docs/advanced/loader.md b/site/docs/advanced/loader.md new file mode 100644 index 0000000000..8836cfc009 --- /dev/null +++ b/site/docs/advanced/loader.md @@ -0,0 +1,538 @@ +--- +title: Loader +order: 1 +--- + +The most importance of Egg which enhanced Koa is that it is based on a certain agreement, code will be placed in different directories according to the functional differences, it significantly reduces development costs. Loader supports this set of conventions and abstracts that many low-level APIs could be extended. + +## Application, Framework and Plugin + +Egg is a low-level framework, applications could use it directly, but Egg only has a few default plugins, we need to configure plugins to extend features in application, such as MySQL. + +```js +// application configuration +// package.json +{ + "dependencies": { + "egg": "^2.0.0", + "egg-mysql": "^3.0.0" + } +} + +// config/plugin.js +module.exports = { + mysql: { + enable: true, + package: 'egg-mysql', + }, +} +``` + +With the increasing number of applications, we find most of them have similar configurations, so we could extend a new framework based on Egg, which makes application configurations simpler. + +```js +// framework configuration +// package.json +{ + "name": "framework1", + "version": "1.0.0", + "dependencies": { + "egg-mysql": "^3.0.0", + "egg-view-nunjucks": "^2.0.0" + } +} + +// config/plugin.js +module.exports = { + mysql: { + enable: false, + package: 'egg-mysql', + }, + view: { + enable: false, + package: 'egg-view-nunjucks', + } +} + +// application configuration +// package.json +{ + "dependencies": { + "framework1": "^1.0.0", + } +} + +// config/plugin.js +module.exports = { + // enable plugins + mysql: true, + view: true, +} +``` + +From the scene above we can see the relationship of application, plugin and framework. + +- We implement business logics in application, and specify a framework to run, so we could configure plugin to meet a special scene feature (such as MySQL). +- Plugin only performs specific function, when two separate functions are interdependent, they still need to be separated into two plugins, and requires configuration. +- Framework is a launcher (default is Egg), which is indispensable to run. Framework is also a wrapper, it aggregates plugins to provide functions unitedly, and it can also configure plugins. +- We can extend a new framework based on framework, which means that **framework can be inherited infinitely**, like class inheritance. + +``` ++-----------------------------------+--------+ +| app1, app2, app3, app4 | | ++-----+--------------+--------------+ | +| | | framework3 | | ++ | framework1 +--------------+ plugin | +| | | framework2 | | ++ +--------------+--------------+ | +| Egg | | ++-----------------------------------+--------| +| Koa | ++-----------------------------------+--------+ +``` + +## LoadUnit + +Egg regards application, framework and plugin as loadUnit, because they are similar in code structure, here is the directory structure: + +``` +loadUnit +├── package.json +├── app.js +├── agent.js +├── app +│ ├── extend +│ | ├── helper.js +│ | ├── request.js +│ | ├── response.js +│ | ├── context.js +│ | ├── application.js +│ | └── agent.js +│ ├── service +│ ├── middleware +│ └── router.js +└── config + ├── config.default.js + ├── config.prod.js + ├── config.test.js + ├── config.local.js + └── config.unittest.js +``` + +However, there are still some differences: + +| File | Application | Framework | Plugin | +| ---------------------- | ----------- | --------- | ------ | +| app/router.js | ✔︎ | | +| app/controller | ✔︎ | | +| app/middleware | ✔︎ | ✔︎ | ✔︎ | +| app/service | ✔︎ | ✔︎ | ✔︎ | +| app/extend | ✔︎ | ✔︎ | ✔︎ | +| app.js | ✔︎ | ✔︎ | ✔︎ | +| agent.js | ✔︎ | ✔︎ | ✔︎ | +| config/config.{env}.js | ✔︎ | ✔︎ | ✔︎ | +| config/plugin.js | ✔︎ | ✔︎ | +| package.json | ✔︎ | ✔︎ | ✔︎ | + +During the loading process, Egg will traverse all loadUnits to load the files above(application, framework and plugin are different), the loading process has priority. + +- Load according to `Plugin => Framework => Application`. +- The order of loading plugin depends on the dependencies, dependent plugins will be loaded first, independent plugins are loaded by the object key configuration order, see [Plugin](./plugin.md) for details. +- Frameworks are loaded by the order of inheritance, the lower the more priority. + +For example, an application is configured with the following dependencies: + +``` +app +| ├── plugin2 (depends plugin3) +| └── plugin3 +└── framework1 + | └── plugin1 + └── egg +``` + +The final loading order is: + +``` +=> plugin1 +=> plugin3 +=> plugin2 +=> egg +=> framework1 +=> app +``` + +The plugin1 is framework1's dependent plugin, the object key order of plugin1 after configuration merger is prior to plugin2/plugin3. Because of the dependencies between plugin2 and plugin3, the order is swapped. The framework1 inherits the Egg, so the order is after the egg. The application is the last to be loaded. + +See [Loader.getLoadUnits](https://github.com/eggjs/egg-core/blob/65ea778a4f2156a9cebd3951dac12c4f9455e636/lib/loader/egg_loader.js#L233) method for details. + +### File Order + +The files that will be loaded by default are listed above. Egg will load files by the following order, each file or directory will be loaded according to loadUnit order (application, framework and plugin are different): + +- Loading [plugin](./plugin.md), find application and framework, loading `config/plugin.js` +- Loading [config](../basics/config.md), traverse loadUnit to load `config/config.{env}.js` +- Loading [extend](../basics/extend.md), traverse loadUnit to load `app/extend/xx.js` +- [Application startup configuration](../basics/app-start.md), traverse loadUnit to load `app.js` and `agent.js` +- Loading [service](../basics/service.md), traverse loadUnit to load `app/service` directory +- Loading [middleware](../basics/middleware.md), traverse loadUnit to load `app/middleware` directory +- Loading [controller](../basics/controller.md), loading application's `app/controller` directory +- Loading [router](../basics/router.md), loading application's `app/router.js` + +Note: + +- Same name will be override in loading, for example, if we want to override `ctx.ip` we could define ip in application's `app/extend/context.js` directly. +- See [framework development](./framework.md) for detail application launch order. + +### Life Cycles + +The framework has provided you several functions to handle during the whole life cycle: + +- `configWillLoad`: All the config files are ready to load, so this is the LAST chance to modify them. +- `configDidLoad`: When all the config files have been loaded. +- `didLoad`: When all the files have been loaded. +- `willReady`: When all the plugins are ready. +- `didReady`: When all the workers are ready. +- `serverDidReady`: When the server is ready. +- `beforeClose`: Before the application is closed. + +Here're the definations: + +```js +// app.js or agent.js +class AppBootHook { + constructor(app) { + this.app = app; + } + + configWillLoad() { + // Ready to call configDidLoad, + // Config, plugin files are referred, + // this is the last chance to modify the config. + } + + configDidLoad() { + // Config, plugin files have been loaded. + } + + async didLoad() { + // All files have loaded, start plugin here. + } + + async willReady() { + // All plugins have started, can do some thing before app ready + } + + async didReady() { + // Worker is ready, can do some things + // don't need to block the app boot. + } + + async serverDidReady() { + // Server is listening. + } + + async beforeClose() { + // Do some thing before app close. + } +} + +module.exports = AppBootHook; +``` + +The framework will automatically load and initialize this class after developers have defined `app.js` and `agenet.js` in the form of class, and it will call the corresponding methods during each of the life cycles. + +Here's the image of starting process: + +![](https://user-images.githubusercontent.com/40081831/50559449-2d3cdc80-0d32-11e9-96f2-42b3cc56d5d3.png) + +**Notice: We have an expiring time limitation when using `beforeClose` to close the processing of the framework. If a worker has accepted the signal of exit but doesn't exit within the limit period, it will be FORCELY closed.** + +If you need to modify the expiring time, please see [this document](https://github.com/eggjs/egg-cluster). + +Deprecated methods: + +## `beforeStart` + +`beforeStart` is called during the loading process, all of its methods are running in parallel. So we usually execute some asynchronized methods (e.g: Check the state of connection, in [`egg-mysql`](https://github.com/eggjs/egg-mysql/blob/master/lib/mysql.js) we use this method to check the connection state with mysql). When all the tasks in `beforeStart` finished, the state will be `ready`. It's NOT recommended to excute a function that consumes too much time there, which will cause the expiration of application's start.plugin developers should use `didLoad` instead, for application developers, `willReady` is the replacer. + +## `ready` + +All the methods mounted on `ready` will be executed when load ends, and after all the methods in `beforeStart` have finished executing. By the time Http server's listening also starts. This means all the plugins are fully loaded and everything is ready, So we use it to process some tasks after start. For developers now, we use `didReady` instead. + +## `beforeClose` + +All the methods mounted on `beforeClose` are called in an inverted order after `close` method in app/agent instance is called. E.g: in [`egg`](https://github.com/eggjs/egg/blob/master/lib/egg.js), we close logger, remove listening methods ...,ect.Developers SHOULDN'T use `app.beforeClose` directly now, but in the form of class to implement `beforeClose` instead. + +**We don't recommend to use this function in a PROD env, because the process may end before it finishes.** + +What's more, we can use [`egg-development`](https://github.com/eggjs/egg-development#loader-trace) to see the loading process. + +### File-Loading Rules + +The framework will convert file names when loading files, because there is a difference between the file naming style and the API style. We recommend that files use underscores, while APIs use lower camel case. For examplem `app/service/user_info.js` will be converted to `app.service.userInfo`. + +The framework also supports hyphens and camel case: + +- `app/service/user-info.js` => `app.service.userInfo` +- `app/service/userInfo.js` => `app.service.userInfo` + +Loader also provides [caseStyle](#caseStyle-string) to force the first letter case, such as make the first letter of the API upper case when loading the model, `app/model/user.js` => `app.model.User`, we can set `caseStyle: 'upper'`. + +## Extend Loader + +[Loader] is a base class and provides some built-in methods based on the rules of the file loading, but itself does not call them in most cases, inherited classes call those methods. + +- loadPlugin() +- loadConfig() +- loadAgentExtend() +- loadApplicationExtend() +- loadRequestExtend() +- loadResponseExtend() +- loadContextExtend() +- loadHelperExtend() +- loadCustomAgent() +- loadCustomApp() +- loadService() +- loadMiddleware() +- loadController() +- loadRouter() + +Egg implements [AppWorkerLoader] and [AgentWorkerLoader] based on the Loader, inherited frameworks could make extensions based on these two classes, and **Extensions of the Loader can only be done in the framework**. + +```js +// custom AppWorkerLoader +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class YadanAppWorkerLoader extends egg.AppWorkerLoader { + constructor(opt) { + super(opt); + // custom initialization + } + + loadConfig() { + super.loadConfig(); + // process config + } + + load() { + super.load(); + // custom loading other directories + // or processing loaded files + } +} + +class Application extends egg.Application { + get [EGG_PATH]() { + return path.dirname(__dirname); + } + // override Egg's Loader, use this Loader when launching + get [EGG_LOADER]() { + return YadanAppWorkerLoader; + } +} + +module.exports = Object.assign(egg, { + Application, + // custom Loader also need export, inherited framework make extensions based on it + AppWorkerLoader: YadanAppWorkerLoader, +}); +``` + +It's convenient for development team to customize loading via the Loader supported APIs. such as `this.model.xx`, `app/extend/filter.js` and so on. + +The mention above is just a description of the Loader wording, please see [Framework Development](./framework.md) for details. + +## Loader API + +Loader also supports some low level APIs to simplify code when extending, [here](https://github.com/eggjs/egg-core#eggloader) are all APIs. + +### `loadFile` + +Used to load a file, such as loading `app/xx.js`: + +```js +// app/xx.js +module.exports = (app) => { + console.log(app.config); +}; + +// app.js +// app/xx.js, as an example, we could load this file in app.js +const path = require('path'); +module.exports = (app) => { + app.loader.loadFile(path.join(app.config.baseDir, 'app/xx.js')); +}; +``` + +If the file exports a function, then the function will be called with `app` as its parameter, otherwise uses this value directly. + +### `loadToApp` + +Used to load files from a directory into the app, such as `app/controller/home.js` to `app.controller.home`. + +```js +// app.js +// The following is just an example, using loadController to load controller in practice +module.exports = (app) => { + const directory = path.join(app.config.baseDir, 'app/controller'); + app.loader.loadToApp(directory, 'controller'); +}; +``` + +The method has three parameters `loadToApp(directory, property, LoaderOptions)`: + +1. Directory could be String or Array, Loader will load files in those directories. +2. Property is app's property. +3. [LoaderOptions](#LoaderOptions) are some configurations. + +### `loadToContext` + +The difference between loadToApp and loadToContext is that loadToContext loads files into ctx instead of app, and it's a lazy loading. It puts files into a temp object when loading, and instantiates objects when calling ctx APIs. + +We load service in this mode as an example: + +```js +// The following is just an example, using loadService in practice +// app/service/user.js +const Service = require('egg').Service; +class UserService extends Service {} +module.exports = UserService; + +// app.js +// get all loadUnit +const servicePaths = app.loader + .getLoadUnits() + .map((unit) => path.join(unit.path, 'app/service')); + +app.loader.loadToContext(servicePaths, 'service', { + // service needs to inherit app.Service, so needs app as parameter + // enable call will return UserService when loading + call: true, + // loading file into app.serviceClasses + fieldClass: 'serviceClasses', +}); +``` + +`app.serviceClasses.user` becomes UserService after file loading, it instantiates UserService when calling `ctx.service.user`. +So this class will only be instantiated when first calling, and will be cached after instantiation, multiple calling same request will be instantiated only once. + +### LoaderOptions + +#### `ignore [String]` + +`ignore` could ignore some files, supports glob, the default is empty. + +```js +app.loader.loadToApp(directory, 'controller', { + // ignore files in app/controller/util + ignore: 'util/**', +}); +``` + +#### `initializer [Function]` + +Processing each file exported values, the default is empty. + +```js +// app/model/user.js +module.exports = class User { + constructor(app, path) {} +}; + +// Loading from app/model, could do some initializations when loading. +const directory = path.join(app.config.baseDir, 'app/model'); +app.loader.loadToApp(directory, 'model', { + initializer(model, opt) { + // The first parameter is export's object + // The second parameter is an object that only contains current file path. + return new model(app, opt.path); + }, +}); +``` + +#### `caseStyle [String]` + +File conversion rules, could be `camel`, `upper`, `lower`, the default is `camel`. + +All three convert file name to camel case, but deal with the initials differently: + +- `camel`: initials unchanged. +- `upper`: initials upper case. +- `lower`: initials lower case. + +Loading different files uses different configurations: + +| File | Configuration | +| -------------- | ------------- | +| app/controller | lower | +| app/middleware | lower | +| app/service | lower | + +#### `override [Boolean]` + +Overriding or throwing exception when encounter existing files, the default is false. + +For example, the `app/service/user.js` is both loaded by the application and the plugin, if the setting is true, the application will override the plugin, otherwise an error will be throwed when loading. + +Loading different files uses different configurations: + +| File | Configuration | +| -------------- | ------------- | +| app/controller | true | +| app/middleware | false | +| app/service | false | + +#### `call [Boolean]` + +Calling when export's object is function, and get the return value, the default is true + +Loading different files uses different configurations: + +| File | Configuration | +| -------------- | ------------- | +| app/controller | true | +| app/middleware | false | +| app/service | true | + +## CustomLoader + +You can use `customLoader` instead of `loadToContext` and `loadToApp`. + +When you define a loader with `loadToApp` + +```js +// app.js +module.exports = (app) => { + const directory = path.join(app.config.baseDir, 'app/adapter'); + app.loader.loadToApp(directory, 'adapter'); +}; +``` + +Instead, you can define `customLoader` + +```js +// config/config.default.js +module.exports = { + customLoader: { + // the property name when load to application, E.X. app.adapter + adapter: { + // relative to app.config.baseDir + directory: 'app/adapter', + // if inject is ctx, it will use loadToContext + inject: 'app', + // whether load the directory of the framework and plugin + loadunit: false, + // you can also use other LoaderOptions + } + }, +}; +``` +## Reference Links +- [Loader](https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js) +- [AppWorkerLoader](https://github.com/eggjs/egg/blob/master/lib/loader/app_worker_loader.js) +- [AgentWorkerLoader](https://github.com/eggjs/egg/blob/master/lib/loader/agent_worker_loader.js) + diff --git a/site/docs/advanced/loader.zh-CN.md b/site/docs/advanced/loader.zh-CN.md new file mode 100644 index 0000000000..074a23a284 --- /dev/null +++ b/site/docs/advanced/loader.zh-CN.md @@ -0,0 +1,540 @@ +--- +title: 加载器(Loader) +order: 1 +--- + +Egg 在 Koa 的基础上进行增强,最重要的就是基于一定的约定,将功能不同的代码分类放置到不同的目录下管理,这对整体团队的开发成本提升有着明显的效果。Loader 实现了这套约定,并且抽象了很多底层 API,以便于进一步扩展。 + +## 应用、框架和插件 + +Egg 是一个底层框架,应用可以直接使用,但 Egg 本身的插件比较少。因此,应用需要自己配置插件来增加各种特性,比如 MySQL。 + +```js +// 应用配置 +// package.json +{ + "dependencies": { + "egg": "^2.0.0", + "egg-mysql": "^3.0.0" + } +} + +// config/plugin.js +module.exports = { + mysql: { + enable: true, + package: 'egg-mysql' + } +} +``` + +当应用数量达到一定规模时,会发现大部分应用的配置都相似。这时,可以基于 Egg 扩展出一个框架,进而简化应用的配置。 + +```js +// 框架配置 +// package.json +{ + "name": "framework1", + "version": "1.0.0", + "dependencies": { + "egg-mysql": "^3.0.0", + "egg-view-nunjucks": "^2.0.0" + } +} + +// config/plugin.js +module.exports = { + mysql: { + enable: false, + package: 'egg-mysql' + }, + view: { + enable: false, + package: 'egg-view-nunjucks' + } +} + +// 应用配置 +// package.json +{ + "dependencies": { + "framework1": "^1.0.0" + } +} + +// config/plugin.js +module.exports = { + // 开启插件 + mysql: true, + view: true +} +``` + +从上面的使用场景可以看出应用、插件和框架三者之间的关系。 + +- 在应用中完成业务,需要指定框架才能运行。当应用需要某一特定功能时,可以通过配置插件来获得,例如 MySQL。 +- 插件专注于完成特定的功能。如果两个独立功能之间存在依赖,可以分成两个插件,但需要相互配置依赖。 +- 框架是一个启动器(默认是 Egg),有了框架应用才能运行。框架还起到封装器的作用,将多个插件的功能聚合起来统一提供,并且框架也可以配置插件。 +- 在框架的基础上还可以扩展新的框架,也就是说,框架可以无限级继承,这有点类似于类的继承。 + +``` ++-----------------------------------+--------+ +| app1, app2, app3, app4 | | ++-----+--------------+--------------+ | +| | | framework3 | | ++ | framework1 +--------------+ plugin | +| | | framework2 | | ++ +--------------+--------------+ | +| Egg | | ++-----------------------------------+--------| +| Koa | ++-----------------------------------+--------+ +``` +## 加载单元(loadUnit) + +Egg 将应用、框架和插件都称为加载单元(loadUnit),因为在代码结构上几乎没有什么差异。下面是一种典型的目录结构: + +``` +loadUnit +├── package.json +├── app.js +├── agent.js +├── app +│ ├── extend +│ │ ├── helper.js +│ │ ├── request.js +│ │ ├── response.js +│ │ ├── context.js +│ │ ├── application.js +│ │ └── agent.js +│ ├── service +│ ├── middleware +│ └── router.js +└── config + ├── config.default.js + ├── config.prod.js + ├── config.test.js + ├── config.local.js + └── config.unittest.js +``` + +不过,还存在一些差异,如下表所示: + +| 文件 | 应用 | 框架 | 插件 | +| ------------------------- | ---- | ---- | ---- | +| package.json | ✔ | ✔ | ✔ | +| config/plugin.{env}.js | ✔ | ✔ | | +| config/config.{env}.js | ✔ | ✔ | ✔ | +| app/extend/application.js | ✔ | ✔ | ✔ | +| app/extend/request.js | ✔ | ✔ | ✔ | +| app/extend/response.js | ✔ | ✔ | ✔ | +| app/extend/context.js | ✔ | ✔ | ✔ | +| app/extend/helper.js | ✔ | ✔ | ✔ | +| agent.js | ✔ | ✔ | ✔ | +| app.js | ✔ | ✔ | ✔ | +| app/service | ✔ | ✔ | ✔ | +| app/middleware | ✔ | ✔ | ✔ | +| app/controller | ✔ | | | +| app/router.js | ✔ | | | + +文件按表格内的顺序从上到下加载。 + +在加载过程中,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级: + +- 按插件 => 框架 => 应用的顺序依次加载。 +- 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖者按 object key 的配置顺序加载。具体可以查看[插件章节](./plugin.md)。 +- 框架按继承顺序加载,越底层越先加载。 + +例如,有这样一个应用配置了如下依赖: + +``` +app +| ├── plugin2 (依赖 plugin3) +| └── plugin3 +└── framework1 + | └── plugin1 + └── egg +``` + +最终的加载顺序为: + +``` +=> plugin1 +=> plugin3 +=> plugin2 +=> egg +=> framework1 +=> app +``` + +plugin1 是 framework1 依赖的插件。由于 plugin2 和 plugin3 的依赖关系,因此交换了它们的位置。由于 framework1 继承了 egg,因此它的加载顺序会晚于 egg。应用将最后加载。 + +更多信息请查看 [Loader.getLoadUnits](https://github.com/eggjs/egg-core/blob/65ea778a4f2156a9cebd3951dac12c4f9455e636/lib/loader/egg_loader.js#L233) 方法。 + +### 文件顺序 + +上文已经列出了默认会加载的文件。Egg 会按照如下文件顺序进行加载,每个文件或目录再根据 loadUnit 的顺序去加载(应用、框架、插件各有不同): + +1. 加载 [plugin](./plugin.md),找到应用和框架,加载 `config/plugin.js`。 +2. 加载 [config](../basics/config.md),遍历 loadUnit 加载 `config/config.{env}.js`。 +3. 加载 [extend](../basics/extend.md),遍历 loadUnit 加载 `app/extend/xx.js`。 +4. [自定义初始化](../basics/app-start.md),遍历 loadUnit 加载 `app.js` 和 `agent.js`。 +5. 加载 [service](../basics/service.md),遍历 loadUnit 加载 `app/service` 目录。 +6. 加载 [middleware](../basics/middleware.md),遍历 loadUnit 加载 `app/middleware` 目录。 +7. 加载 [controller](../basics/controller.md),加载应用的 `app/controller` 目录。 +8. 加载 [router](../basics/router.md),加载应用的 `app/router.js`。 + +请注意: + +- 加载时如果遇到同名文件将会被覆盖。比如,如果想要覆盖 `ctx.ip`,可以在应用的 `app/extend/context.js` 中直接定义 `ip`。 +- 应用完整启动顺序请查看[框架开发](./framework.md)。 +### 生命周期 + +框架提供了以下生命周期函数供开发者使用: + +- 配置文件即将加载,为修改配置的最后机会(`configWillLoad`) +- 配置文件已加载完成(`configDidLoad`) +- 文件已加载完成(`didLoad`) +- 插件启动完毕(`willReady`) +- worker 准备就绪(`didReady`) +- 应用启动完成(`serverDidReady`) +- 应用即将关闭(`beforeClose`) + +定义方法如下: + +```js +// app.js 或 agent.js +class AppBootHook { + constructor(app) { + this.app = app; + } + + configWillLoad() { + // 准备调用 configDidLoad, + // 配置文件和插件文件将被引用, + // 这是修改配置的最后机会。 + } + + configDidLoad() { + // 配置文件和插件文件已被加载。 + } + + async didLoad() { + // 所有文件已加载,这里开始启动插件。 + } + + async willReady() { + // 所有插件已启动,在应用准备就绪前可执行一些操作。 + } + + async didReady() { + // worker 已准备就绪,在这里可以执行一些操作, + // 这些操作不会阻塞应用启动。 + } + + async serverDidReady() { + // 服务器已开始监听。 + } + + async beforeClose() { + // 应用关闭前执行一些操作。 + } +} + +module.exports = AppBootHook; +``` + +开发者使用类的方式定义 `app.js` 和 `agent.js` 后,框架将自动加载并实例化这个类,并在各个生命周期阶段调用相应的方法。 + +启动过程如图所示: + +![](https://user-images.githubusercontent.com/40081831/47344271-a688d500-d6da-11e8-96e9-663fa9f45108.png) + +**在使用 `beforeClose` 时,需要注意:框架在处理关闭进程时设有超时限制。如果 worker 进程在收到退出信号后,未能在规定时间内退出,则会被强制终止。** + +如需调整超时时间,请查阅[相关文档](https://github.com/eggjs/egg-cluster)。 + +弃用的方法: + +## beforeStart + +`beforeStart` 方法在加载过程中调用,所有方法并行执行。通常用于执行一些异步任务,例如检查连接状态等。例如,[`egg-mysql`](https://github.com/eggjs/egg-mysql/blob/master/lib/mysql.js) 使用 `beforeStart` 来检查 MySQL 的连接状态。所有 `beforeStart` 任务结束后,应用将进入 `ready` 状态。不建议执行耗时长的方法,可能导致应用启动超时。插件开发者应使用 `didLoad` 替代,应用开发者应使用 `willReady` 替代。 + +## ready + +注册到 `ready` 方法的任务将在加载结束后,所有 `beforeStart` 方法执行完毕后顺序执行,HTTP 服务器监听也在此时开始。此时代表所有插件已加载完成且准备工作已完成,通常用于执行一些启动后置任务。开发者应使用 `didReady` 替代。 + +## beforeClose + +`beforeClose` 注册方法在 app/agent 实例的 `close` 方法调用后,按注册的逆序执行。通常用于资源释放操作,例如 [`egg`](https://github.com/eggjs/egg/blob/master/lib/egg.js) 用于关闭日志、移除监听器等。开发者不应直接使用 `app.beforeClose`,而是通过定义类的形式,实现 `beforeClose` 方法。 + +**此方法不建议在生产环境使用,因可能会出现未完全执行结束就结束进程的情况。** + +另外,我们可以使用 [`egg-development`](https://github.com/eggjs/egg-development#loader-trace) 来查看加载过程。 + +### 文件加载规则 + +框架在加载文件时会进行转换,因为文件命名风格与 API 风格有所差异。我们推荐文件使用下划线命名,而 API 使用驼峰命名。例如 `app/service/user_info.js` 会转换为 `app.service.userInfo`。 + +框架也支持其它风格命名的文件;连字符和驼峰方式命名的文件同样支持: + +- `app/service/user-info.js` => `app.service.userInfo` +- `app/service/userInfo.js` => `app.service.userInfo` + +Loader 也提供了 [caseStyle](#caseStyle-string) 设置来强制指定命名方式,如将 model 加载时的 API 首字母大写,`app/model/user.js` => `app.model.User`,可指定 `caseStyle: 'upper'`。 +## 扩展 Loader + +`Loader` 是一个基类,并根据文件加载的规则提供了一些内置的方法。它本身并不会去调用这些方法,而是由继承类调用。 + +- `loadPlugin()` +- `loadConfig()` +- `loadAgentExtend()` +- `loadApplicationExtend()` +- `loadRequestExtend()` +- `loadResponseExtend()` +- `loadContextExtend()` +- `loadHelperExtend()` +- `loadCustomAgent()` +- `loadCustomApp()` +- `loadService()` +- `loadMiddleware()` +- `loadController()` +- `loadRouter()` + +`Egg` 基于 `Loader` 实现了 `AppWorkerLoader` 和 `AgentWorkerLoader`,上层框架基于这两个类来扩展。**Loader 的扩展只能在框架进行**。 + +```js +// 自定义 AppWorkerLoader +// lib/framework.js +const path = require('path'); +const egg = require('egg'); +const EGG_PATH = Symbol.for('egg#eggPath'); + +class YadanAppWorkerLoader extends egg.AppWorkerLoader { + constructor(opt) { + super(opt); + // 自定义初始化 + } + + loadConfig() { + super.loadConfig(); + // 对 config 进行处理 + } + + load() { + super.load(); + // 自定义加载其他目录 + // 或对已加载的文件进行处理 + } +} + +class Application extends egg.Application { + get [EGG_PATH]() { + return path.dirname(__dirname); + } + // 覆盖 Egg 的 Loader,启动时使用这个 Loader + get [EGG_LOADER]() { + return YadanAppWorkerLoader; + } +} + +module.exports = Object.assign(egg, { + Application, + // 自定义的 Loader 也需要 export,上层框架需要基于这个扩展 + AppWorkerLoader: YadanAppWorkerLoader, +}); +``` + +通过 `Loader` 提供的这些 API,可以很方便地定制团队的自定义加载,例如 `this.model.xx`,`app/extend/filter.js` 等等。 + +以上只是说明 `Loader` 的写法,具体可以查看[框架开发](./framework.md)。 +## 加载器函数(Loader API) + +Loader 提供了一些基础 API,方便在扩展时简化代码。想了解所有相关 API,请[点击此处](https://github.com/eggjs/egg-core#eggloader)。 + +### loadFile + +此函数用来加载文件,例如加载 `app/xx.js` 就会用到它。 + +```js +// app/xx.js +module.exports = (app) => { + console.log(app.config); +}; + +// app.js +// 以 app/xx.js 为例子,在 app.js 中加载此文件: +const path = require('path'); +module.exports = (app) => { + app.loader.loadFile(path.join(app.config.baseDir, 'app/xx.js')); +}; +``` + +如果文件导出了一个函数,这个函数会被调用,`app` 作为参数传入;如果不是函数,则直接使用文件导出的值。 + +### loadToApp + +此函数用来将一个目录下的文件加载到 app 对象上,例如 `app/controller/home.js` 会被加载到 `app.controller.home`。 + +```js +// app.js +// 以下只是示例,加载 controller 请用 loadController +module.exports = (app) => { + const directory = path.join(app.config.baseDir, 'app/controller'); + app.loader.loadToApp(directory, 'controller'); +}; +``` + +`loadToApp` 有三个参数:`loadToApp(directory, property, LoaderOptions)` + +1. `directory` 可以是字符串或数组。Loader 会从这些目录中加载文件。 +2. `property` 是 app 的属性名。 +3. [`LoaderOptions`](#LoaderOptions) 包含了一些配置选项。 + +### loadToContext + +`loadToContext` 与 `loadToApp` 略有不同,它是将文件加载到 `ctx` 上,而不是 `app`,并且支持懒加载。加载操作会将文件放到一个临时对象中,在调用 `ctx` API 时才去实例化。 + +例如,加载 service 文件的方式就用到了这种模式: + +```js +// 以下为示例,请使用 loadService +// app/service/user.js +const Service = require('egg').Service; +class UserService extends Service {} +module.exports = UserService; + +// app.js +// 获取所有的 loadUnit +const servicePaths = app.loader + .getLoadUnits() + .map((unit) => path.join(unit.path, 'app/service')); + +app.loader.loadToContext(servicePaths, 'service', { + // service 需要继承 app.Service,因此需要 app 参数 + // 设置 call 为 true,会在加载时调用函数,并返回 UserService + call: true, + // 将文件加载到 app.serviceClasses + fieldClass: 'serviceClasses', +}); +``` + +文件加载完成后,`app.serviceClasses.user` 就代表 UserService 类。当调用 `ctx.service.user` 时,会实例化 UserService 类。因此,这个类只有在每次请求中首次被访问时才会实例化。实例化后,对象会被缓存,同一个请求中多次调用也只实例化一次。 +### LoaderOptions + +#### ignore [String] + +`ignore` 可用于忽略某些文件,支持 glob 匹配模式,默认值为空。 + +```js +app.loader.loadToApp(directory, 'controller', { + // 忽略 app/controller/util 目录下的文件 + ignore: 'util/**', +}); +``` + +#### initializer [Function] + +对每个文件 export 的值进行处理,此项默认为空。 + +```js +// app/model/user.js +module.exports = class User { + constructor(app, path) {} +}; + +// 从 app/model 目录加载,且可以在加载时进行一些初始化处理 +const directory = path.join(app.config.baseDir, 'app/model'); +app.loader.loadToApp(directory, 'model', { + initializer(model, opt) { + // 第一个参数为 export 的对象 + // 第二个参数为一个对象,里面包含当前文件的路径 + return new model(app, opt.path); + }, +}); +``` + +#### caseStyle [String] + +设置文件命名的转换规则,可选项为 `camel`、`upper` 或 `lower`,默认值为 `camel`。 + +这些选项都会将文件名转换为驼峰命名,但是首字符的大小写处理不同: +- `camel`:首字母保持不变。 +- `upper`:首字母转为大写。 +- `lower`:首字母转为小写。 + +根据不同文件类型设置相应的转换规则,如下表所示: + +| 文件类型 | `caseStyle` 配置 | +| ------------- | -------------- | +| app/controller | lower | +| app/middleware | lower | +| app/service | lower | + +#### override [Boolean] + +当存在同名文件时,是否覆盖原有文件,或抛出异常。默认值为 `false`。 + +例如,当同时加载应用和插件中的 `app/service/user.js` 文件时: +- 若 `override` 设为 `true`,则应用中的文件会覆盖插件中的同名文件。 +- 若设为 `false`,则在尝试加载应用中的文件时会报错。 + +根据不同文件类型设置 `override` 的配置值,如下表所示: + +| 文件类型 | `override` 配置 | +| ------------- | --------------- | +| app/controller | true | +| app/middleware | false | +| app/service | false | + +#### call [Boolean] + +若 export 出的对象是函数,则可以调用此函数并获取其返回值,默认值为 `true`。 + +根据不同文件类型设置 `call` 的配置值,如下表所示: + +| 文件类型 | `call` 配置 | +| ------------- | ------------- | +| app/controller | true | +| app/middleware | false | +| app/service | true | + + +## CustomLoader + +`loadToContext` 和 `loadToApp` 方法可以通过 `customLoader` 的配置来替代。 + +以下是用 `loadToApp` 方法加载代码的示例: + +```js +// app.js +module.exports = (app) => { + const directory = path.join(app.config.baseDir, 'app/adapter'); + app.loader.loadToApp(directory, 'adapter'); +}; +``` + +改为使用 `customLoader` 后的写法是: + +```js +// config/config.default.js +module.exports = { + customLoader: { + // 在 app 对象上定义的属性名为 app.adapter + adapter: { + // 路径相对于 app.config.baseDir + directory: 'app/adapter', + // 如果用于 ctx,则应该使用 loadToContext 方法 + inject: 'app', + // 是否加载框架和插件的目录 + loadunit: false, + // 也可以定义其他 LoaderOptions + }, + }, +}; +``` + +参考链接: +- [loader](https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js) +- [appworkerloader](https://github.com/eggjs/egg/blob/master/lib/loader/app_worker_loader.js) +- [agentworkerloader](https://github.com/eggjs/egg/blob/master/lib/loader/agent_worker_loader.js) \ No newline at end of file diff --git a/site/docs/advanced/plugin.md b/site/docs/advanced/plugin.md new file mode 100644 index 0000000000..f661568924 --- /dev/null +++ b/site/docs/advanced/plugin.md @@ -0,0 +1,500 @@ +--- +title: Plugin Development +order: 2 +--- + +Plugins are the most important features in Egg framework. They keep Egg simple, stable and efficient, and also they make the best reuse of business logic to build an entire ecosystem. Maybe we want to ask: + +- Since Koa already has the mechanism of middleware, Why do we need plugins? +- What are the differences / relationships among middlewares, plugins and applications? +- How can I use the plugin? +- How do I build a plugin? +- ... + +As we've already explained some these points in chapter [using plugins](../basics/plugin.md) before. Now we are going through how to build a plugin. + +## Plugin Development + +### Quick Start with Scaffold + +Just use [egg-boilerplate-plugin] to generates a scaffold for you. + +```bash +$ mkdir egg-hello && cd egg-hello +$ npm init egg --type=plugin +$ npm i +$ npm test +``` + +## Directory of Plugin + +Plugin is actually a `mini application`, directory of plugin is as below: + +```js +. egg-hello +├── package.json +├── app.js (optional) +├── agent.js (optional) +├── app +│ ├── extend (optional) +│ | ├── helper.js (optional) +│ | ├── request.js (optional) +│ | ├── response.js (optional) +│ | ├── context.js (optional) +│ | ├── application.js (optional) +│ | └── agent.js (optional) +│ ├── service (optional) +│ └── middleware (optional) +│ └── mw.js +├── config +| ├── config.default.js +│ ├── config.prod.js +| ├── config.test.js (optional) +| ├── config.local.js (optional) +| └── config.unittest.js (optional) +└── test + └── middleware + └── mw.test.js +``` + +It is almost the same as the application directory, what're the differences? + +1. Plugin have no independant router or controller. This is because: + + - Usually routers are strongly bound to application, it is not fit here. + - An application might have plenty of dependant plugins, routers of plugin are very possible conflict with others. It would be a disaster. + - If you really need a general router, you should implement it as middleware of the plugin. + +2. The specific information of plugin should be declared in the `package.json` of `eggPlugin`: + + - `{String} name` - plugin name(required), it must be unique, it will be used in the config of the dependencies of plugins. + - `{Array} dependencies` - strong dependent plugins list of the current plugin(if one of these plugins here is not found, application's startup will fail). + - `{Array} optionalDependencies` - optional dependencies list of this plugin.(if these plugins are not activated, only warnings would be occurred, and will not affect the startup of the application). + - `{Array} env` - this option is available only when specifying the environment. For the list of env, please refer to [env](../basics/env.md). This is optional, most time you can leave it. + + ```json + { + "name": "egg-rpc", + "eggPlugin": { + "name": "rpc", + "dependencies": ["registry"], + "optionalDependencies": ["vip"], + "env": ["local", "test", "unittest", "prod"] + } + } + ``` + +3. No `plugin.js`: + + - `eggPlugin.dependencies` is for declaring dependencies only, not for importing, nor activating. + - If you want to manage multiple plugins, you should do it in[upper framework](./framework.md) + +## Dependencies Management of Plugins + +The dependencies are managed by plugin himself, which is different from middlewares. Before loading plugins, application will read `eggPlugin > dependencies` and `eggPlugin > optionalDependencies` from `package.json`, and then sort out the loading orders according to their relationships, for example, the loading order of the following plugins is `c => b => a`: + +```json +// plugin a +{ + "name": "egg-plugin-a", + "eggPlugin": { + "name": "a", + "dependencies": [ "b" ] + } +} + +// plugin b +{ + "name": "egg-plugin-b", + "eggPlugin": { + "name": "b", + "optionalDependencies": [ "c" ] + } +} + +// plugin c +{ + "name": "egg-plugin-c", + "eggPlugin": { + "name": "c" + } +} +``` + +**Attention: The values in `dependencies` and `optionalDependencies` are the `eggPlugin.name` of plugins, not `package.name`.** + +The `dependencies` and `optionalDependencies` are learnt from `npm`, most time we use `dependencies`, which is recommended. There are about two situations to apply the `optionalDependencies`: + +- Only be dependant in specific environment: for example, an authentication plugin, only depends on the mock plugin in development environment. +- Weakly depending, for example: A depends on B, but without B, A can take other choices. + +Attention: if you are using `optionalDependencies`, framework won't verify the activation of these dependencies, they are only for sorting loading orders. In such situation, the plugin will go through other ways such as `interface detection` to decide processing logic. + +## What can Plugin Do? + +We've discussed what plugin is. Now what can it do? + +### Built-in Objects API Extensions + +Extend the built-in objects of the framework, just like the application + +- `app/extend/request.js` - extends Koa#Request object +- `app/extend/response.js` - extends Koa#Response object +- `app/extend/context.js` - extends Koa#Context object +- `app/extend/helper.js ` - extends Helper object +- `app/extend/application.js` - extends Application object +- `app/extend/agent.js` - extends Agent object + +### Insert Custom Middlewares + +1. First, define and implement middleware under directory `app/middleware`: + + ```js + 'use strict'; + + const staticCache = require('koa-static-cache'); + const assert = require('assert'); + const mkdirp = require('mkdirp'); + + module.exports = (options, app) => { + assert.strictEqual( + typeof options.dir, + 'string', + 'Must set `app.config.static.dir` when static plugin enable', + ); + + // ensure directory exists + mkdirp.sync(options.dir); + + app.loggers.coreLogger.info( + '[egg-static] starting static serve %s -> %s', + options.prefix, + options.dir, + ); + + return staticCache(options); + }; + ``` + +2. Insert middleware to the appropriate position in `app.js`(e.g. insert static middleware before bodyParser): + + ```js + const assert = require('assert'); + + module.exports = (app) => { + // insert static middleware before bodyParser + const index = app.config.coreMiddleware.indexOf('bodyParser'); + assert(index >= 0, 'bodyParser highly needed'); + + app.config.coreMiddleware.splice(index, 0, 'static'); + }; + ``` + +### Initialization on Application Starting + +- If you want to read some local config before startup: + + ```js + // ${plugin_root}/app.js + const fs = require('fs'); + const path = require('path'); + + module.exports = (app) => { + app.customData = fs.readFileSync(path.join(app.config.baseDir, 'data.bin')); + + app.coreLogger.info('read data ok'); + }; + ``` + +- If you want to do some async starting business, you can do it with `app.beforeStart` API: + + ```js + // ${plugin_root}/app.js + const MyClient = require('my-client'); + + module.exports = (app) => { + app.myClient = new MyClient(); + app.myClient.on('error', (err) => { + app.coreLogger.error(err); + }); + app.beforeStart(async () => { + await app.myClient.ready(); + app.coreLogger.info('my client is ready'); + }); + }; + ``` + +- You can add initialization business of agent with `agent.beforeStart` API: + + ```js + // ${plugin_root}/agent.js + const MyClient = require('my-client'); + + module.exports = (agent) => { + agent.myClient = new MyClient(); + agent.myClient.on('error', (err) => { + agent.coreLogger.error(err); + }); + agent.beforeStart(async () => { + await agent.myClient.ready(); + agent.coreLogger.info('my client is ready'); + }); + }; + ``` + +### Setup Schedule Task + +1. Setup dependencies of schedule plugin in `package.json`: + + ```json + { + "name": "your-plugin", + "eggPlugin": { + "name": "your-plugin", + "dependencies": ["schedule"] + } + } + ``` + +2. Create a new file in `${plugin_root}/app/schedule/` directory to edit your schedule task: + + ```js + exports.schedule = { + type: 'worker', + cron: '0 0 3 * * *', + // interval: '1h', + // immediate: true, + }; + + exports.task = async (ctx) => { + // your logic code + }; + ``` + +### Best Practice of Global Instance Plugins + +Some plugins are made to introduce existing service into framework, like [egg-mysql], [egg-oss].They all need to create corresponding instances in applications. We notice that there are some common problems when developing plugins of this kind: + +- Use different instances of the same service in one application (e.g: connect to two different MySQL databases). +- Dynamically initialize connection after getting config from other service (e.g: get the MySQL server address from configuration center and then create connection). + +If each plugin makes their own implementation, all sorts of configs and initializations will be chaotic. So the framework supplies the `app.addSingleton(name, creator)` API to unify the creation of this kind of services. Note that while using the `app.addSingleton(name, creator)` method, the configuration file must have the `client` or `clients` key configuration as the `config` to the `creator` function. + +#### Ways of writing plugins + +We simplify the [egg-mysql] plugin to see how to write it: + +```js +// egg-mysql/app.js +module.exports = (app) => { + // The first parameter mysql defines the field mounted to app, we can access MySQL singleton instance via `app.mysql` + // The second parameter createMysql accepts two parameters (config, app), and then returns a MySQL instance + app.addSingleton('mysql', createMysql); +}; + +/** + * @param {Object} config The config is processed by the framework. If the application is configured with multiple MySQL instances, each config would be passed in and call multiple createMysql + * @param {Application} app the current application + * @return {Object} return the created MySQL instance + */ +function createMysql(config, app) { + assert(config.host && config.port && config.user && config.database); + // create instance + const client = new Mysql(config); + + // check before start the application + app.beforeStart(async () => { + const rows = await client.query('select now() as currentTime;'); + app.coreLogger.info( + `[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`, + ); + }); + + return client; +} +``` + +The initialization function also supports `Async function`, convenient for some special plugins that need to be asynchronous to get some configuration files. + +```js +async function createMysql(config, app) { + // get mysql configurations asynchronous + const mysqlConfig = await app.configManager.getMysqlConfig(config.mysql); + assert( + mysqlConfig.host && + mysqlConfig.port && + mysqlConfig.user && + mysqlConfig.database, + ); + // create instance + const client = new Mysql(mysqlConfig); + + // check before start the application + const rows = await client.query('select now() as currentTime;'); + app.coreLogger.info( + `[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`, + ); + + return client; +} +``` + +As you can see, all we need to do for this plugin is passing the fields that need to be mounted and the corresponding initialization function. Framework will be in charge of managing all the configs and the ways to access the instances. + +#### Application Layer Usage Case + +##### Single Instance + +1. Declare MySQL config in config file: + + ```js + // config/config.default.js + module.exports = { + mysql: { + client: { + host: 'mysql.com', + port: '3306', + user: 'test_user', + password: 'test_password', + database: 'test', + }, + }, + }; + ``` + +2. Access database through `app.mysql` directly: + + ```js + // app/controller/post.js + class PostController extends Controller { + async list() { + const posts = await this.app.mysql.query(sql, values); + }, + } + ``` + +##### Multiple Instances + +1. We need to configure MySQL in the config file, but different from single instance, we need to add `clients` in the config to declare the configuration of different instances. Meanwhile, the `default` field can be used to configure the shared configuration in multiple instances (e.g: `host` and `port`). In this case, we should use `get` function to specify the corresponding instance(e.g: use `app.mysql.get('db1').query()` instead of using `app.mysql.query()` directly to get an `undefined`). + + ```js + // config/config.default.js + exports.mysql = { + clients: { + // clientId, access the client instance by app.mysql.get('clientId') + db1: { + user: 'user1', + password: 'upassword1', + database: 'db1', + }, + db2: { + user: 'user2', + password: 'upassword2', + database: 'db2', + }, + }, + // default configuration for all databases + default: { + host: 'mysql.com', + port: '3306', + }, + }; + ``` + +2. Access the corresponding instance by `app.mysql.get('db1')`: + + ```js + // app/controller/post.js + class PostController extends Controller { + async list() { + const posts = await this.app.mysql.get('db1').query(sql, values); + }, + } + ``` + +##### Dynamically Instantiating + +Instead of declaring the configuration in the configuration file in advance, we can dynamically initialize an instance at the runtime of the application. + +```js +// app.js +module.exports = (app) => { + app.beforeStart(async () => { + // get MySQL config from configuration center { host, post, password, ... } + const mysqlConfig = await app.configCenter.fetch('mysql'); + // create MySQL instance dynamically + app.database = app.mysql.createInstanceAsync(mysqlConfig); + }); +}; +``` + +Access the instance through `app.database` + +```js +// app/controller/post.js +class PostController extends Controller { + async list() { + const posts = await this.app.database.query(sql, values); + }, +} +``` + +**Attention: when creating the instance dynamically, framework would read the `default` configuration from the config file as the default.** + +### Plugin Locate Rule + +When loading the plugins in the framework, it will follow the rules below: + +- If there is the path configuration, load them in path directly. +- If there is no path configuration, search them with the package name, the search orders are: + + 1. `node_modules` directory of the application root + 2. `node_modules` directory of the dependencies + 3. `node_modules` of current directory(generally for unit test compatibility) + +### Plugin Specification + +It's well welcomed to your contributions to the new plugins, but also hope you follow some of following specifications: + +- Naming Rules: + - `npm` packages must append prefix `egg-`,and all letters must be lowercase, e.g: `egg-xxx`. The long names should be concatenated with middle-lines: `egg-foo-bar`. + - The corresponding plugin should be named in camel-case. The name should be translated according to the middle-lines of the `npm` name:`egg-foo-bar` => `fooBar`. + - The use of middle-lines is not compulsive, e.g: userservice(egg-userservice) and user-service(egg-user-service) are both acceptable. +- `package.json` Rules: + + - Add `eggPlugin` property according to the details discussed before. + - For convenient index, add `egg`,`egg-plugin`,`eggPlugin` in `keywords`: + + ```json + { + "name": "egg-view-nunjucks", + "version": "1.0.0", + "description": "view plugin for egg", + "eggPlugin": { + "name": "nunjucks", + "dep": ["security"] + }, + "keywords": [ + "egg", + "egg-plugin", + "eggPlugin", + "egg-plugin-view", + "egg-view", + "nunjucks" + ] + } + ``` + +## Why Do Not Use the `npm` Package Name as the Plugin Name? + +Egg defines the plugin name through the `eggPlugin.name`, it is only unique in application or framework, that means **many npm packages might get the same plugin name**, why design in this way? + +First, Egg plugin does not only support npm packages, but also supports plugins-searching in local directory. In Chapter [progressive](../intro/progressive.md) we've mentioned how to make progress by using these two configurations. Directories are more friendly to unit tests. So, Egg can not ensure uniqueness through npm package names. + +What's more, Egg can use this feature to make an adapter, for example, the plugin defined in[Template Develop Spec](./view-plugin.md#PluginNameSpecification) was named as view, but there are plugins named `egg-view-nunjucks` and `egg-view-react`, the users only need to change the plugin and modify the templates, no need to modify the controllers, because all these plugins have implemented the same APIs. + +**Giving the same plugin name and the same API to the same plugin can make quick switch between them**. This is really really useful in template and database. + +[egg-boilerplate-plugin]: https://github.com/eggjs/egg-boilerplate-plugin +[egg-mysql]: https://github.com/eggjs/egg-mysql +[egg-oss]: https://github.com/eggjs/egg-oss diff --git a/site/docs/advanced/plugin.zh-CN.md b/site/docs/advanced/plugin.zh-CN.md new file mode 100644 index 0000000000..3372cf9775 --- /dev/null +++ b/site/docs/advanced/plugin.zh-CN.md @@ -0,0 +1,485 @@ +--- +title: 插件开发 +order: 2 +--- + +插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问: + +- Koa 已经有了中间件的机制,为啥还要插件呢? +- 中间件、插件、应用它们之间是什么关系,有什么区别? +- 我该怎么使用一个插件? +- 如何编写一个插件? + +在 [使用插件](../basics/plugin.md) 章节我们已经讨论过前几点,接下来我们来看看如何开发一个插件。 + +## 插件开发 + +### 使用脚手架快速开发 + +你可以直接使用 [egg-boilerplate-plugin] 脚手架来快速上手。 + +```bash +$ mkdir egg-hello && cd egg-hello +$ npm init egg --type=plugin +$ npm i +$ npm test +``` + +## 插件的目录结构 + +一个插件其实就是一个“迷你的应用”,下面展示的是一个插件的目录结构,和应用(app)几乎一样。 + +```plaintext +.egg-hello +├── package.json +├── app.js(可选) +├── agent.js(可选) +├── app +│ ├── extend(可选) +│ │ ├── helper.js(可选) +│ │ ├── request.js(可选) +│ │ ├── response.js(可选) +│ │ ├── context.js(可选) +│ │ ├── application.js(可选) +│ │ └── agent.js(可选) +│ ├── service(可选) +│ └── middleware(可选) +│ └── mw.js +├── config +│ ├── config.default.js +│ ├── config.prod.js +│ ├── config.test.js(可选) +│ ├── config.local.js(可选) +│ └── config.unittest.js(可选) +└── test + └── middleware + └── mw.test.js +``` + +那区别在哪儿呢? + +1. 插件没有独立的 router 和 controller。这主要出于几点考虑: + + - 路由一般和应用强绑定的,不具备通用性。 + - 一个应用可能依赖很多个插件,如果插件支持路由可能会导致路由冲突。 + - 如果确实有统一路由的需求,可以考虑在插件里通过中间件来实现。 + +2. 插件需要在 `package.json` 中的 `eggPlugin` 节点指定插件特有的信息: + + - `{String} name` - 插件名(必须配置),具有唯一性,配置依赖关系时会指定依赖插件的 name。 + - `{Array} dependencies` - 当前插件强依赖的插件列表(如果依赖的插件没找到,应用启动失败)。 + - `{Array} optionalDependencies` - 当前插件的可选依赖插件列表(如果依赖的插件未开启,只会 warning,不会影响应用启动)。 + - `{Array} env` - 只有在指定运行环境才能开启,具体有哪些环境可以参考 [运行环境](../basics/env.md)。此配置是可选的,一般情况下都不需要配置。 + + ```json + { + "name": "egg-rpc", + "eggPlugin": { + "name": "rpc", + "dependencies": ["registry"], + "optionalDependencies": ["vip"], + "env": ["local", "test", "unittest", "prod"] + } + } + ``` + +3. 插件没有 `plugin.js`: + + - `eggPlugin.dependencies` 只是用于声明依赖关系,而不是引入插件或开启插件。 + - 如果期望统一管理多个插件的开启和配置,可以在 [上层框架](./framework.md) 处理。 + +## 插件的依赖管理 + +和中间件不同,插件是自己管理依赖的。应用在加载所有插件前会预先从它们的 `package.json` 中读取 `eggPlugin > dependencies` 和 `eggPlugin > optionalDependencies` 节点,然后根据依赖关系计算出加载顺序,举个例子,下面三个插件的加载顺序就应该是 `c => b => a`。 + +```json +// plugin a +{ + "name": "egg-plugin-a", + "eggPlugin": { + "name": "a", + "dependencies": ["b"] + } +} + +// plugin b +{ + "name": "egg-plugin-b", + "eggPlugin": { + "name": "b", + "optionalDependencies": ["c"] + } +} + +// plugin c +{ + "name": "egg-plugin-c", + "eggPlugin": { + "name": "c" + } +} +``` + +**注意:`dependencies` 和 `optionalDependencies` 的取值是另一个插件的 `eggPlugin.name`,而不是 `package name`。** + +`dependencies` 和 `optionalDependencies` 是从 npm 借鉴来的概念,大多数情况下我们都使用 `dependencies`,这也是我们最推荐的依赖方式。那什么时候可以用 `optionalDependencies` 呢?大致就两种: + +- 只在某些环境下才依赖,比如:一个鉴权插件,只在开发环境依赖一个 mock 数据的插件。 +- 弱依赖,比如:A 依赖 B,但是如果没有 B,A 有相应的降级方案。 + +需要特别强调的是:如果采用 `optionalDependencies`,那么框架不会校验依赖的插件是否开启,它的作用仅仅是计算加载顺序。所以,这时候依赖方需要通过“接口探测”等方式来决定相应的处理逻辑。 +## 插件能做什么? + +上面给出了插件的定义,那插件到底能做什么? + +### 扩展内置对象的接口 + +在插件相应的文件内对框架内置对象进行扩展,和应用一样: + +- `app/extend/request.js` - 扩展 Koa#Request 类 +- `app/extend/response.js` - 扩展 Koa#Response 类 +- `app/extend/context.js` - 扩展 Koa#Context 类 +- `app/extend/helper.js` - 扩展 Helper 类 +- `app/extend/application.js` - 扩展 Application 类 +- `app/extend/agent.js` - 扩展 Agent 类 + +### 插入自定义中间件 + +1. 首先在 `app/middleware` 目录下定义好中间件实现: + + ```js + 'use strict'; + + const staticCache = require('koa-static-cache'); + const assert = require('assert'); + const mkdirp = require('mkdirp'); + + module.exports = (options, app) => { + assert.strictEqual( + typeof options.dir, + 'string', + 'Must set `app.config.static.dir` when static plugin enable', + ); + + // 确保目录存在 + mkdirp.sync(options.dir); + + app.loggers.coreLogger.info( + '[egg-static] starting static serve %s -> %s', + options.prefix, + options.dir, + ); + + return staticCache(options); + }; + ``` + +2. 在 `app.js` 中将中间件插入到合适的位置(例如:下面将 static 中间件放到 bodyParser 之前): + + ```js + const assert = require('assert'); + + module.exports = (app) => { + // 将 static 中间件放到 bodyParser 之前 + const index = app.config.coreMiddleware.indexOf('bodyParser'); + assert(index >= 0, 'bodyParser 中间件必须存在'); + + app.config.coreMiddleware.splice(index, 0, 'static'); + }; + ``` + +### 在应用启动时做一些初始化工作 + +- 我在启动前想读取一些本地配置: + + ```js + // ${plugin_root}/app.js + const fs = require('fs'); + const path = require('path'); + + module.exports = (app) => { + app.customData = fs.readFileSync(path.join(app.config.baseDir, 'data.bin')); + + app.coreLogger.info('Data read successfully'); + }; + ``` + +- 如果有异步启动逻辑,可以使用 `app.beforeStart` API: + + ```js + // ${plugin_root}/app.js + const MyClient = require('my-client'); + + module.exports = (app) => { + app.myClient = new MyClient(); + app.myClient.on('error', (err) => { + app.coreLogger.error(err); + }); + app.beforeStart(async () => { + await app.myClient.ready(); + app.coreLogger.info('My client is ready'); + }); + }; + ``` + +- 也可以添加 agent 启动逻辑,使用 `agent.beforeStart` API: + + ```js + // ${plugin_root}/agent.js + const MyClient = require('my-client'); + + module.exports = (agent) => { + agent.myClient = new MyClient(); + agent.myClient.on('error', (err) => { + agent.coreLogger.error(err); + }); + agent.beforeStart(async () => { + await agent.myClient.ready(); + agent.coreLogger.info('My client is ready'); + }); + }; + ``` +### 设置定时任务 + +1. 在 `package.json` 里设置依赖 schedule 插件 + + ```json + { + "name": "your-plugin", + "eggPlugin": { + "name": "your-plugin", + "dependencies": ["schedule"] + } + } + ``` + +2. 在 `${plugin_root}/app/schedule/` 目录下新建文件,编写你的定时任务。 + + ```js + exports.schedule = { + type: 'worker', + cron: '0 0 3 * * *', + // interval: '1h', + // immediate: true + }; + + exports.task = async (ctx) => { + // 你的逻辑代码 + }; + ``` + +### 全局实例插件的最佳实践 + +许多插件的目的都是将一些已有的服务引入到框架中,如`egg-mysql`、`egg-oss`。它们都需要在 app 上创建对应的实例。而在开发这一类插件时,我们发现存在一些普遍性的问题: + +- 在一个应用中同时使用同一个服务的不同实例(连接到两个不同的 MySQL 数据库)。 +- 从其他服务获取配置后动态初始化连接(从配置中心获取到 MySQL 服务地址后再建立连接)。 + +如果让插件各自实现,可能会出现各种奇怪的配置方式和初始化方式,所以框架提供了 `app.addSingleton(name, creator)` 方法来统一这类服务的创建。需要注意的是,在使用 `app.addSingleton(name, creator)` 方法时,配置文件中一定要有 `client` 或者 `clients` 为 key 的配置。 + +#### 插件写法 + +以下代码展示了如何编写这类插件,它是对 `egg-mysql` 插件实现的简化: + +```js +// egg-mysql/app.js +module.exports = app => { + app.addSingleton('mysql', createMysql); +}; + +/** + * @param {Object} config 框架处理后的配置项,如应用配置了多个 MySQL 实例,每个配置项会分别传入并多次调用 createMysql + * @param {Application} app 当前应用 + * @return {Object} 返回创建的 MySQL 实例 + */ +function createMysql(config, app) { + assert(config.host && config.port && config.user && config.database); + // 创建实例 + const client = new Mysql(config); + + // 应用启动前检查 + app.beforeStart(async () => { + const rows = await client.query('select now() as currentTime;'); + app.coreLogger.info(`[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`); + }); + + return client; +} +``` + +初始化方法也支持 `async function`,便于有些需要异步获取配置文件的特殊插件: + +```js +async function createMysql(config, app) { + // 异步获取 mysql 配置 + const mysqlConfig = await app.configManager.getMysqlConfig(config.mysql); + assert(mysqlConfig.host && mysqlConfig.port && mysqlConfig.user && mysqlConfig.database); + // 创建实例 + const client = new Mysql(mysqlConfig); + + // 应用启动前检查 + const rows = await client.query('select now() as currentTime;'); + app.coreLogger.info(`[egg-mysql] init instance success, rds currentTime: ${rows[0].currentTime}`); + + return client; +} +``` + +可以看到,插件中我们只需要提供要挂载的字段和服务的初始化方法,所有配置管理、实例获取方式由框架封装并统一提供。 +#### 应用层使用方案 + +##### 单实例 + +1. 在配置文件中声明 MySQL 的配置。 + + ```js + // config/config.default.js + module.exports = { + mysql: { + client: { + host: 'mysql.com', + port: '3306', + user: 'test_user', + password: 'test_password', + database: 'test', + }, + }, + }; + ``` + +2. 直接通过 `app.mysql` 访问数据库。 + + ```js + // app/controller/post.js + class PostController extends Controller { + async list() { + const posts = await this.app.mysql.query(sql, values); + } + } + ``` + +##### 多实例 + +1. 同样需要在配置文件中声明 MySQL 的配置,不过和单实例时不同,配置项中需要有一个 `clients` 字段,分别声明不同实例的配置。同时,可以通过 `default` 字段配置多个实例中共享的配置(如 host 和 port)。需要注意的是,在这种情况下要用 `get` 方法指定相应的实例。(例如:使用 `app.mysql.get('db1').query()`,而不是直接使用 `app.mysql.query()`,否则可能得到一个 `undefined` )。 + + ```js + // config/config.default.js + exports.mysql = { + clients: { + // clientId,可通过 app.mysql.get('clientId') 访问客户端实例 + db1: { + user: 'user1', + password: 'upassword1', + database: 'db1', + }, + db2: { + user: 'user2', + password: 'upassword2', + database: 'db2', + }, + }, + // 所有数据库的默认配置 + default: { + host: 'mysql.com', + port: '3306', + }, + }; + ``` + +2. 通过 `app.mysql.get('db1')` 获取对应的实例并使用。 + + ```js + // app/controller/post.js + class PostController extends Controller { + async list() { + const posts = await this.app.mysql.get('db1').query(sql, values); + } + } + ``` + +##### 动态创建实例 + +我们可以不需要将配置提前声明在配置文件中,而是在应用运行时动态初始化一个实例。 + +```js +// app.js +module.exports = app => { + app.beforeStart(async () => { + // 从配置中心获取 MySQL 配置 { host, port, password, ... } + const mysqlConfig = await app.configCenter.fetch('mysql'); + // 动态创建 MySQL 实例 + app.database = await app.mysql.createInstanceAsync(mysqlConfig); + }); +}; +``` + +通过 `app.database` 使用这个实例。 + +```js +// app/controller/post.js +class PostController extends Controller { + async list() { + const posts = await this.app.database.query(sql, values); + } +} +``` + +**注意,在动态创建实例时,框架还会读取配置中 `default` 字段的配置作为默认配置项。** + +### 插件的寻址规则 + +框架加载插件时,遵循以下寻址规则: + +- 如果配置了 `path`,直接按照 `path` 加载。 +- 没有 `path` 时,根据包名(package name)查找,查找顺序依次是: + 1. 应用根目录下的 `node_modules` + 2. 应用依赖框架路径下的 `node_modules` + 3. 当前路径下的 `node_modules`(主要是兼容单元测试场景) +### 插件规范 + +我们非常欢迎你贡献新的插件,同时也希望你遵守下面一些规范: + +- 命名规范 + - `npm` 包名应以 `egg-` 开头,且应为全小写,例如:`egg-xx`。比较长的词组应使用中划线:`egg-foo-bar`。 + - 对应的插件名应使用小驼峰式命名。小驼峰式的转换规则以 `npm` 包名中的中划线为准,例如 `egg-foo-bar` => `fooBar`。 + - 对于既可以加中划线也可以不加的情况,不做强制约定,例如:`userservice`(`egg-userservice`)或 `user-service`(`egg-user-service`)都可。 + +- `package.json` 书写规范 + + - 按照上面的文档添加 `eggPlugin` 节点。 + - 在 `keywords` 里添加 `egg`、`egg-plugin`、`eggPlugin` 等关键字,便于索引。 + +```json +{ + "name": "egg-view-nunjucks", + "version": "1.0.0", + "description": "view plugin for egg", + "eggPlugin": { + "name": "nunjucks", + "dep": ["security"] + }, + "keywords": [ + "egg", + "egg-plugin", + "eggPlugin", + "egg-plugin-view", + "egg-view", + "nunjucks" + ] +} +``` + +## 为何不使用 npm 包名来做插件名? + +Egg 通过 `eggPlugin.name` 来定义插件名,只需应用或框架具备唯一性,也就是说**多个 npm 包可能有相同的插件名**。为什么这么设计呢? + +首先,Egg 插件不仅支持 npm 包,还支持通过目录来寻找插件。在[渐进式开发](../intro/progressive.md)章节提到了如何使用这两个配置进行代码演进。目录对单元测试也更为友好。所以,Egg 无法通过 npm 包名来确保唯一性。 + +更重要的是,Egg 通过这种特性来做适配器。例如,[模板开发规范](./view-plugin.md#插件命名规范)定义的插件名为 `view`,存在 `egg-view-nunjucks`、`egg-view-react` 等插件,使用者只需要更换插件和修改模板,无需修改 Controller,因为所有的模板插件都实现了相同的 API。 + +**将相同功能的插件赋予相同的插件名,以及提供相同的 API,可以快速进行切换**。这种做法在模板、数据库等领域非常适用。 + + +[egg-boilerplate-plugin]: https://github.com/eggjs/egg-boilerplate-plugin +[egg-mysql]: https://github.com/eggjs/egg-mysql +[egg-oss]: https://github.com/eggjs/egg-oss diff --git a/site/docs/advanced/view-plugin.md b/site/docs/advanced/view-plugin.md new file mode 100644 index 0000000000..af52222dab --- /dev/null +++ b/site/docs/advanced/view-plugin.md @@ -0,0 +1,188 @@ +--- +title: View Plugin Development +order: 5 +--- + +In most cases, we need to read the data, render the template and then present it to the user. The framework does not force to use one template engine, but allows developers to select the [template](../core/view.md) by themselves. For details, see [Template Rendering](../core/view.md). + +This article describes the framework's specification constraints on the View plugin, and we can use this to encapsulate the corresponding template engine plugin. The following takes [egg-view-ejs] as an example. + +## Plugin Directory Structure + +```bash +egg-view-ejs +├── config +│ ├── config.default.js +│ └── config.local.js +├── lib +│ └── view.js +├── app.js +├── test +├── History.md +├── README.md +└── package.json +``` + +## Plugin Naming Convention + +- Follow the [plugin development specification](./plugin.md) +- According to the convention, the names of plugins start with `egg-view-` +- `package.json` is configured as follows. Plugins are named after the template engine, such as ejs + +```json +{ + "name": "egg-view-ejs", + "eggPlugin": { + "name": "ejs" + }, + "keywords": ["egg", "egg-plugin", "egg-view", "ejs"] +} +``` + +- The configuration item is also named after the template engine + +```js +// config/config.default.js +module.exports = { + ejs: {}, +}; +``` + +## View Base Class + +The next step is to provide a View base class that will be instantiated on each request. + +The base class of the View needs to provide `render` and `renderString` methods and supports generator and async functions (it can also be a function that returns a Promise). The `render` method is used to render files, and the `renderString` method is used to render template strings. + +The following is a simplified code that can be directly [view source](https://github.com/eggjs/egg-view-ejs/blob/master/lib/view.js) + +```js +const ejs = require('ejs'); + +Mmdule.exports = class EjsView { + render(filename, locals, viewOptions) { + + const config = Object.assign({}, this.config, viewOptions, { filename }); + + return new Promise((resolve, reject) => { + // Asynchronous API call + ejs.renderFile(filename, locals, config, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); + } + + renderString(tpl, locals, viewOptions) { + const config = Object.assign({}, this.config, viewOptions, { cache: null }); + try { + // Synchronous API call + return Promise.resolve(ejs.render(tpl, locals, config)); + } catch (err) { + return Promise.reject(err); + } + } +}; +``` + +### Parameters + +The three parameters of the `render` method are: + +- `filename`: is the path to the complete file. The framework determines if the file exists when looking for the file. It does not need to be processed here. +- `locals`: The data needs rendering. It comes from `app.locals`, `ctx.locals` and calls `render` methods. The framework also has built in `ctx`, `request`, `ctx.helper` objects. +- `viewOptions`: The incoming configuration of the user, which can override the default configuration of the template engine. This can be considered based on the characteristics of the template engine. For example, the cache is enabled by default but a page does not need to be cached. + +The three parameters of the `renderString` method: + +- `tpl`: template string, not file path. +- `locals`: same with `render`. +- `viewOptions`: same with `render`. + +## Plugin Configuration + +According to the naming conventions mentioned above, the configuration name is generally the name of the template engine, such as ejs. + +The configuration of the plugin mainly comes from the configuration of the template engine, and the configuration items can be defined according to the specific conditions, such as the [configuration of ejs](https://github.com/mde/ejs#options). + +```js +// config/config.default.js +module.exports = { + ejs: { + cache: true, + }, +}; +``` + +### Helper + +The framework provides `ctx.helper` for developer use, but in some cases we want to override the helper method and only take effect when the template is rendered. + +In template rendering, we often need to output a user-supplied html fragment, in which case, we often use the `helper.shtml` provided by the `egg-security` plugin. + +```html +
    {{ helper.shtml(data.content) | safe }}
    +``` + +However, as shown in the code above, we need to use `| safe` to tell the template engine that the html is safe and it doesn't need to run `escape` again. + +This is more cumbersome to use and easy to forget, so we can package it: + +- First provide a helper subclass: + +```js +// {plugin_root}/lib/helper.js +module.exports = (app) => { + return class ViewHelper extends app.Helper { + // safe is injected by [egg-view-nunjucks] and will not be escaped during rendering. + // Otherwise, the template call shtml will be escaped + shtml(str) { + return this.safe(super.shtml(str)); + } + }; +}; +``` + +- Use a custom helper when rendering: + +```js +// {plugin_root}/lib/view.js +const ViewHelper = require('./helper'); + +module.exports = class MyCustomView { + render(filename, locals) { + locals.helper = new ViewHelper(this.ctx); // call Nunjucks render + } +}; +``` + +You can [view](https://github.com/eggjs/egg-view-nunjucks/blob/2ee5ee992cfd95bc0bb5b822fbd72a6778edb118/lib/view.js#L11) the specific code here + +### Security Related + +Templates and security are related and [egg-security] also provides some methods for the template. The template engine can be used according to requirements. + +First declare a dependency on [egg-security]: + +```json +{ + "name": "egg-view-nunjucks", + "eggPlugin": { + "name": "nunjucks", + "dep": ["security"] + } +} +``` + +Besides, the framework provides [app.injectCsrf](../core/security.md#appinjectcsrfstr) and [app.injectNonce](../core/security.md#appinjectnonncestr), for more information on [security section](../core/security.md). + +### Unit Tests + +As a high-quality plugin, perfect unit testing is indispensable, and we also provide lots of auxiliary tools to make it painless for plugin developers to write tests with, see [unit testing](../core/unittest.md) and [plugin](./plugin.md) docs. + +[egg-security]: https://github.com/eggjs/egg-security +[egg-view-nunjucks]: https://github.com/eggjs/egg-view-nunjucks +[egg-view-ejs]: https://github.com/eggjs/egg-view-ejs diff --git a/site/docs/advanced/view-plugin.zh-CN.md b/site/docs/advanced/view-plugin.zh-CN.md new file mode 100644 index 0000000000..e2a1617fe7 --- /dev/null +++ b/site/docs/advanced/view-plugin.zh-CN.md @@ -0,0 +1,185 @@ +--- +title: View 插件开发 +order: 5 +--- + +绝大多数情况下,我们都需要读取数据后渲染模板,然后呈现给用户。框架并不强制使用某种模板引擎,由开发者自行选型,具体参见[模板渲染](../core/view.md)。 + +本文将阐述框架对 View 插件的规范约束。我们可以依此来封装对应的模板引擎插件。以下以 [egg-view-ejs](https://github.com/eggjs/egg-view-ejs) 为例。 + +## 插件目录结构 +```bash +egg-view-ejs +├── config +│ ├── config.default.js +│ └── config.local.js +├── lib +│ └── view.js +├── app.js +├── test +├── History.md +├── README.md +└── package.json +``` + +## 插件命名规范 + +- 遵循[插件开发规范](./plugin.md)。 +- 插件命名约定以 `egg-view-` 开头。 +- `package.json` 的配置如下,插件名以模板引擎命名,例如 ejs: + +```json +{ + "name": "egg-view-ejs", + "eggPlugin": { + "name": "ejs" + }, + "keywords": ["egg", "egg-plugin", "egg-view", "ejs"] +} +``` + +- 配置项也以模板引擎命名: + +```js +// config/config.default.js +exports.ejs = {}; +``` + +## View 基类 + +接下来需提供一个 View 基类,这个类会在每次请求时实例化。 + +View 基类需要提供 `render` 和 `renderString` 两个方法,支持 generator function 和 async function(也可以是函数返回一个 Promise)。`render` 方法用于渲染文件,而 `renderString` 方法用于渲染模板字符串。 + +以下为简化代码,您可以直接[查看源码](https://github.com/eggjs/egg-view-ejs/blob/master/lib/view.js): + +```js +const ejs = require('ejs'); + +Mmdule.exports = class EjsView { + render(filename, locals, viewOptions) { + + const config = Object.assign({}, this.config, viewOptions, { filename }); + + return new Promise((resolve, reject) => { + // 异步调用 API + ejs.renderFile(filename, locals, config, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); + } + + renderString(tpl, locals, viewOptions) { + const config = Object.assign({}, this.config, viewOptions, { cache: null }); + try { + // 同步调用 API + return Promise.resolve(ejs.render(tpl, locals, config)); + } catch (err) { + return Promise.reject(err); + } + } +}; +``` + +### 参数 + +`render` 方法的参数: + - `filename`:是完整文件路径,框架查找文件时已确认文件是否存在,因此这里不需要处理。 + - `locals`:渲染所需数据,来源包括 `app.locals`、`ctx.locals` 以及调用 `render` 方法传入的数据。框架还内置了 `ctx`、`request` 和 `ctx.helper` 这几个对象。 + - `viewOptions`:用户传入的配置,可以覆盖模板引擎的默认配置。这个可根据模板引擎的特征考虑是否支持。例如,默认开启了缓存,而某个页面不需要缓存。 + +`renderString` 方法的三个参数: + - `tpl`: 模板字符串,没有文件路径。 + - `locals`: 同 `render`。 + - `viewOptions`: 同 `render`。 + +## 插件配置 + +根据上述的命名约定,配置名通常为模板引擎的名称,例如 ejs。 + +插件的配置主要来源于模板引擎的配置,可根据具体情况定义配置项目,如 [ejs 的配置](https://github.com/mde/ejs#options)。 + +```js +// config/config.default.js +module.exports = { + ejs: { + cache: true + } +}; +``` + +### helper + +框架本身提供了 `ctx.helper` 供开发者使用。但在某些情况下,我们希望覆盖 helper 方法,使其仅在模板渲染时生效。 + +在模板渲染中,我们经常需要输出用户提供的 HTML 片段,这通常需要使用 `egg-security` 插件提供的 `helper.shtml` 方法进行清洗: + +```html +
    {{ helper.shtml(data.content) | safe }}
    +``` + +但如上所示,我们需要加上 `| safe` 来告知模板引擎,该 HTML 是安全的,无需再次 `escape`,可以直接渲染。 + +这样使用起来比较繁琐,而且容易忘记。所以,我们可以进行封装: + +- 首先提供一个 helper 子类: + +```js +// {plugin_root}/lib/helper.js +module.exports = (app) => { + return class ViewHelper extends app.Helper { + // `safe` 是由 [egg-view-nunjucks] 注入的,在渲染时不会进行转义。 + // 否则在模板调用 `shtml` 时,内容会被转义。 + shtml(str) { + return this.safe(super.shtml(str)); + } + }; +}; +``` + +- 在渲染时使用我们自定义的 helper: + +```js +// {plugin_root}/lib/view.js +const ViewHelper = require('./helper'); + +module.exports = class MyCustomView { + render(filename, locals) { + locals.helper = new ViewHelper(this.ctx); + + // 调用 Nunjucks 的 render 方法 + } +}; +``` + +具体代码可以在[这里](https://github.com/eggjs/egg-view-nunjucks/blob/2ee5ee992cfd95bc0bb5b822fbd72a6778edb118/lib/view.js#L11)查看。 + +### 安全相关 + +模板与安全密不可分。[egg-security] 也为模板提供了一些方法。模板引擎可以根据需求使用这些方法。 + +首先声明对 [egg-security] 的依赖: + +```json +{ + "name": "egg-view-nunjucks", + "eggPlugin": { + "name": "nunjucks", + "dep": ["security"] + } +} +``` + +除此之外,框架还提供了 [app.injectCsrf](../core/security.md#appinjectcsrfstr) 与 [app.injectNonce](../core/security.md#appinjectnoncestr) 方法。更多内容可查看[安全章节](../core/security.md)。 + +### 单元测试 + +为了确保插件的高质量,完善的单元测试是不可或缺的。我们也提供了很多辅助工具,以帮助插件开发者毫无障碍地编写测试。具体内容请参见[单元测试](../core/unittest.md)与[插件](./plugin.md)相关章节。 + +[egg-security]: https://github.com/eggjs/egg-security +[egg-view-nunjucks]: https://github.com/eggjs/egg-view-nunjucks +[egg-view-ejs]: https://github.com/eggjs/egg-view-ejs diff --git a/site/docs/basics/app-start.md b/site/docs/basics/app-start.md new file mode 100644 index 0000000000..7ae4b05c65 --- /dev/null +++ b/site/docs/basics/app-start.md @@ -0,0 +1,86 @@ +--- +title: Application Startup Configuration +order: 12 +--- + +When the application starts up, we often need to set up some initialization logic. The application bootstraps with those specific configurations. It is in a healthy state and be able to take external service requests after those configurations successfully applied. Otherwise, it failed. + +The framework provides a unified entry file (`app.js`) for boot process customization. This file need returns a Boot class. We can define the initialization process in the startup application by defining the lifecycle method in the Boot class. + +The framework has provided you several functions to handle during the whole [life cycle](../advanced/loader.md#life-cycles): + +- `configWillLoad`: All the config files are ready to load, so this is the LAST chance to modify them. +- `configDidLoad`: When all the config files have been loaded. +- `didLoad`: When all the files have been loaded. +- `willReady`: When all the plug-ins are ready. +- `didReady`: When all the workers are ready. +- `serverDidReady`: When the server is ready. +- `beforeClose`: Before the application is closed. + +We can defined Boot class in `app.js`. Below we take a few examples of lifecycle functions commonly used in application development: + +```js +// app.js +class AppBootHook { + constructor(app) { + this.app = app; + } + + configWillLoad() { + // The config file has been read and merged, but it has not yet taken effect + // This is the last time the application layer modifies the configuration + // Note: This function only supports synchronous calls. + + // For example: the password in the parameter is encrypted, decrypt it here + this.app.config.mysql.password = decrypt(this.app.config.mysql.password); + // For example: insert a middleware into the framework's coreMiddleware + const statusIdx = this.app.config.coreMiddleware.indexOf('status'); + this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit'); + } + + async didLoad() { + // All configurations have been loaded + // Can be used to load the application custom file, start a custom service + + // Example: Creating a custom app example + this.app.queue = new Queue(this.app.config.queue); + await this.app.queue.init(); + + // For example: load a custom directory + this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', { + fieldClass: 'tasksClasses', + }); + } + + async willReady() { + // All plugins have been started, but the application is not yet ready + // Can do some data initialization and other operations + // Application will start after these operations executed succcessfully + + // For example: loading data from the database into the in-memory cache + this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); + } + + async didReady() { + // Application already ready + + const ctx = await this.app.createAnonymousContext(); + await ctx.service.Biz.request(); + } + + async serverDidReady() { + // http / https server has started and begins accepting external requests + // At this point you can get an instance of server from app.server + + this.app.server.on('timeout', (socket) => { + // handle socket timeout + }); + } +} + +module.exports = AppBootHook; +``` + +**Note: It is not recommended to do long-time operations in the custom lifecycle function, because the framework has a startup timeout detection.** + +If your Egg's life-cycle functions are old, we suggest you upgrading to the "class-method" mode. For more you can refer to [Upgrade your event functions in your lifecycle](../advanced/loader-update.md). diff --git a/site/docs/basics/app-start.zh-CN.md b/site/docs/basics/app-start.zh-CN.md new file mode 100644 index 0000000000..34123fb508 --- /dev/null +++ b/site/docs/basics/app-start.zh-CN.md @@ -0,0 +1,84 @@ +--- +title: 启动自定义 +order: 12 +--- + +我们常常需要在应用启动期间进行一些初始化工作,待初始化完成后,应用才可以启动成功,并开始对外提供服务。 + +框架提供了统一的入口文件(`app.js`)进行启动过程自定义。这个文件需要返回一个 Boot 类。我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。 + +框架提供了以下 [生命周期函数](../advanced/loader.md#life-cycles) 供开发人员处理: +- 配置文件即将加载,这是最后动态修改配置的时机(`configWillLoad`); +- 配置文件加载完成(`configDidLoad`); +- 文件加载完成(`didLoad`); +- 插件启动完毕(`willReady`); +- worker 准备就绪(`didReady`); +- 应用启动完成(`serverDidReady`); +- 应用即将关闭(`beforeClose`)。 + +我们可以在 `app.js` 中定义这个 Boot 类。下面我们抽取几个在应用开发中常用的生命周期函数为例: + +```js +// app.js +class AppBootHook { + constructor(app) { + this.app = app; + } + + configWillLoad() { + // 此时 config 文件已经被读取并合并,但还并未生效 + // 这是应用层修改配置的最后机会 + // 注意:此函数只支持同步调用 + + // 例如:参数中的密码是加密的,在此处进行解密 + this.app.config.mysql.password = decrypt(this.app.config.mysql.password); + // 例如:插入一个中间件到框架的 coreMiddleware 之间 + const statusIdx = this.app.config.coreMiddleware.indexOf('status'); + this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit'); + } + + async didLoad() { + // 所有配置已经加载完毕 + // 可以用来加载应用自定义的文件,启动自定义服务 + + // 例如:创建自定义应用的实例 + this.app.queue = new Queue(this.app.config.queue); + await this.app.queue.init(); + + // 例如:加载自定义目录 + this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', { + fieldClass: 'tasksClasses', + }); + } + + async willReady() { + // 所有插件已启动完毕,但应用整体尚未 ready + // 可进行数据初始化等操作,这些操作成功后才启动应用 + + // 例如:从数据库加载数据到内存缓存 + this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); + } + + async didReady() { + // 应用已启动完毕 + + const ctx = await this.app.createAnonymousContext(); + await ctx.service.Biz.request(); + } + + async serverDidReady() { + // http/https 服务器已启动,开始接收外部请求 + // 此时可以从 app.server 获取 server 实例 + + this.app.server.on('timeout', socket => { + // 处理 socket 超时 + }); + } +} + +module.exports = AppBootHook; +``` + +**注意:在自定义生命周期函数中,不建议进行耗时的操作,因为框架会有启动的超时检测。** + +如果你的 Egg 框架的生命周期函数是旧版本的,建议你将其升级到类方法模式;详情请查看[升级你的生命周期事件函数](../advanced/loader-update.md)。 \ No newline at end of file diff --git a/site/docs/basics/config.md b/site/docs/basics/config.md new file mode 100644 index 0000000000..5ff318f8ca --- /dev/null +++ b/site/docs/basics/config.md @@ -0,0 +1,141 @@ +--- +title: Configuration +order: 4 +--- + +This framework provides powerful and extensible configuration function, including automatically merging applications, plugins, and framework's configuration. In addition, it allows users to overwrite configuration in sequence and maintain different configs depending on different environments. The result (i.e. merged config) can be accessed from `app.config `. + +Here are some common control tactics: + +1. Using platform to manage configurations: while building a new application, you can put the current environment configuration into package and trigger the configuration as long as you run this application. But this certain application won't be able to build several deployments at once, and you will get into trouble whenever you want to use the configuration in localhost. +2. Using platform to manage configurations: you can pass the current environment configuration via environment variables while starting. This is a relatively elegant approach with higher requirement on operation and support from configuration platform. Moreover, The configuration environment has same flaws as first method. +3. Using code to manage configurations: you can add some environment configurations in codes and pass them to current environment arguments while starting. However, it doesn't allow you to configure globally and you need to alter your code whenever you want to change the configuration. + +we choose the last strategy, namely **configure with code**, The change of configuration should be also released after reviewing. The application package itself is capable to be deployed in several environments, only need to specify the running environment. + +### Multiple Environment Configuration + +This framework supports loading configuration according to the environment and defining configuration files of multiple environments. For more details, please check [env](../basics/env.md). + +``` +config +|- config.default.js +|- config.prod.js +|- config.unittest.js +|- config.local.js +``` + +`config.default.js` is the default file for configuration, and all environments will load this file. Besides, this is usually used as default configuration file for development environment. + +The corresponding configuration file(named configuration) will be loaded simultaneously when you set up env. The named configuration and the default configuration will combine(use [extend2](https://www.npmjs.com/package/extend2) deep clone) into a configuration eventually. And the same name will be overwritten. For example, `prod` environment will load `config.prod.js` and `config.default.js`. As a result, `config.prod.js` will overwrite the configuration with identical name in `config.default.js`. + +### How to Write Configuration + +The configuration file returns an object which could overwrite some configurations in the framework. Application can put its own business configuration into it for convenient management. + +```js +// configure the catalog of logger,the default configuration of logger is provided by framework +module.exports = { + logger: { + dir: '/home/admin/logs/demoapp', + }, +}; +``` + +The configuration file can simplify to `exports.key = value` format + +```js +exports.keys = 'my-cookie-secret-key'; +exports.logger = { + level: 'DEBUG', +}; +``` + +The configuration file can also return a function which could receive a parameter called `appInfo` + +```js +// put the catalog of logger to the catalog of codes +const path = require('path'); +module.exports = (appInfo) => { + return { + logger: { + dir: path.join(appInfo.baseDir, 'logs'), + }, + }; +}; +``` + +The build-in appInfo contains: + +| appInfo | elaboration | +| ------- | ------------------------------------------------------------------------------------------------------------- | +| pkg | package.json | +| name | Application name, same as pkg.name | +| baseDir | The directory of codes | +| HOME | User directory, e.g, the account of admin is /home/admin | +| root | The application root directory, if the environment is local or unittest, it is baseDir. Otherwise, it is HOME | + +`appInfo.root` is an elegant adaption. for example, we tend to use `/home/admin/logs` as the catalog of log in the server environment, while we don't want to pollute the user catalog in local development. This adaptation is very good at solving this problem. + +Choose the appropriate style according to the specific situation, but please make sure you don't make mistake like the code below: + +```js +// config/config.default.js +exports.someKeys = 'abc'; +module.exports = (appInfo) => { + const config = {}; + config.keys = '123456'; + return config; +}; +``` + +### Sequence of Loading Configurations + +Applications, plugin components and framework are able to define those configs. Even though the structure of catalog is identical but there is priority (application > framework > plugin). Besides, the running environment has the higher priority. + +Here is one sequence of loading configurations under "prod" environment, in which the following configuration will overwrite the previous configuration with the same name. + + -> plugin config.default.js + -> framework config.default.js + -> application config.default.js + -> plugin config.prod.js + -> framework config.prod.js + -> application config.prod.js + +**Note: there will be plugin loading sequence, but the approximate order is similar. For specific logic, please check the [loader](../advanced/loader.md) .** + +### Rules of Merging + +Configs are merged using deep copy from [extend2] module, which is forked from [extend] and process array in a different way. + +```js +const a = { + arr: [1, 2], +}; +const b = { + arr: [3], +}; +extend(true, a, b); +// => { arr: [ 3 ] } +``` + +As demonstrated above, the framework will overwrite arrays instead of merging them. + +### Configuration Result + +The final merged config will be dumped to `run/application_config.json`(for worker process) and `run/agent_config.json`(for agent process) when the framework started, which can help analyzing problems. + +Some fields are hidden in the config file, mainly including 2 types: + +- like passwords, secret keys and other security related fields which can be configured in `config.dump.ignore` and only [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) type is accepted. See [Default Configs](https://github.com/eggjs/egg/blob/master/config/config.default.js) +- like Function, Buffer, etc. whose content converted by `JSON.stringify` will be specially large. + +`run/application_config_meta.json` (for worker process)and `run/agent_config_meta.json` (for agent process) will also be dumped in order to check which file defines the property, see below + +```json +{ + "logger": { + "dir": "/path/to/config/config.default.js" + } +} +``` diff --git a/site/docs/basics/config.zh-CN.md b/site/docs/basics/config.zh-CN.md new file mode 100644 index 0000000000..300911e9b3 --- /dev/null +++ b/site/docs/basics/config.zh-CN.md @@ -0,0 +1,144 @@ +--- +title: Config 配置 +order: 4 +--- + +框架提供了强大且可扩展的配置功能,可以自动合并应用、插件、框架的配置,按顺序覆盖,且可以根据环境维护不同的配置。合并后的配置可直接从 `app.config` 获取。 + +配置的管理有多种方案,以下列举一些常见的方案: + +1. 使用平台管理配置,应用构建时将当前环境的配置放入包内,启动时指定该配置。但应用就无法一次构建多次部署,而且本地开发环境想使用配置会变得很麻烦。 +2. 使用平台管理配置,在启动时将当前环境的配置通过环境变量传入,这是比较优雅的方式,但框架对运维的要求会比较高,需要部署平台支持,同时开发环境也有相同的痛点。 +3. 使用代码管理配置,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可。但无法全局配置,必须修改代码。 + +我们选择了最后一种配置方案,**配置即代码**,配置的变更也应该经过审核后才能发布。应用包本身是可以部署在多个环境的,只需要指定运行环境即可。 +### 多环境配置 + +框架支持根据环境来加载配置,定义多个环境的配置文件,具体环境请查看[运行环境配置](./env.md)。 + +``` +config +|- config.default.js +|- config.prod.js +|- config.unittest.js +`- config.local.js +``` + +`config.default.js` 为默认的配置文件,所有环境都会加载这个配置文件,一般也会作为开发环境的默认配置文件。 + +当指定 `env` 时,会同时加载默认配置和对应的配置(具名配置)文件。具名配置和默认配置将合并(使用 [extend2](https://www.npmjs.com/package/extend2) 深拷贝)成最终配置,具名配置项会覆盖默认配置文件的同名配置。例如,`prod` 环境会加载 `config.prod.js` 和 `config.default.js` 文件,`config.prod.js` 会覆盖 `config.default.js` 的同名配置。 +### 配置写法 + +配置文件返回的是一个对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。 + +```js +// 配置 logger 文件的目录,logger 默认配置由框架提供 +module.exports = { + logger: { + dir: '/home/admin/logs/demoapp', + }, +}; +``` + +配置文件也可以简化地写成 `exports.key = value` 形式: + +```js +exports.keys = 'my-cookie-secret-key'; +exports.logger = { + level: 'DEBUG', +}; +``` + +配置文件也可以返回一个函数,该函数可以接受 `appInfo` 参数: + +```js +// 将 logger 目录放到代码目录下 +const path = require('path'); +module.exports = (appInfo) => { + return { + logger: { + dir: path.join(appInfo.baseDir, 'logs'), + }, + }; +}; +``` + +内置的 `appInfo` 属性包括: + +| appInfo | 说明 | +| ------- | ------------------------------------------------------------ | +| pkg | `package.json` 文件 | +| name | 应用名称,同 `pkg.name` | +| baseDir | 应用代码的目录 | +| HOME | 用户目录,如 admin 账户为 `/home/admin` | +| root | 应用根目录,在 `local` 和 `unittest` 环境下为 `baseDir`,其他都为 `HOME`。 | + +`appInfo.root` 是一个优雅的适配方案。例如,在服务器环境我们通常使用 `/home/admin/logs` 作为日志目录,而在本地开发时为了避免污染用户目录,我们需要一种优雅的适配方案,`appInfo.root` 正好解决了这个问题。 + +请根据具体场合选择合适的写法。但请确保没有完成以下代码: + +```js +// 配置文件 config/config.default.js +exports.someKeys = 'abc'; +module.exports = (appInfo) => { + const config = {}; + config.keys = '123456'; + return config; +}; +``` + +### 配置加载顺序 + +应用、插件、框架都可以定义这些配置,且目录结构都是一致的,但存在优先级(应用 > 框架 > 插件),相对于此运行环境的优先级会更高。 + +比如在 prod 环境中加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。 + +``` +-> 插件 config.default.js +-> 框架 config.default.js +-> 应用 config.default.js +-> 插件 config.prod.js +-> 框架 config.prod.js +-> 应用 config.prod.js +``` + +**注意**:插件之间也会有加载顺序,但大致顺序类似。具体逻辑可[查看加载器](../advanced/loader.md)。 +### 合并规则 + +配置的合并使用 `extend2` 模块进行深度拷贝,`extend2` 来源于 `extend`,但是在处理数组时的表现会有所不同。 + +```js +const a = { + arr: [1, 2], +}; +const b = { + arr: [3], +}; +extend(true, a, b); +// => { arr: [ 3 ] } +``` + +根据上面的例子,框架直接覆盖数组而不是进行合并。 + +### 配置结果 + +框架在启动时会把合并后的最终配置输出到 `run/application_config.json`(worker 进程)和 `run/agent_config.json`(agent 进程)中,以供问题分析。 + +配置文件中会隐藏以下两类字段: + +1. 安全字段,如密码、密钥等。这些字段通过 `config.dump.ignore` 属性进行配置,其类型必须是 [Set]。可参见[默认配置](https://github.com/eggjs/egg/blob/master/config/config.default.js)。 +2. 非字符串化字段,如函数、Buffer 等。这些字段在 `JSON.stringify` 后所生成的内容容量很大。 + +此外,框架还会生成 `run/application_config_meta.json`(worker 进程)和 `run/agent_config_meta.json`(agent 进程)文件。这些文件用于排查配置属性的来源,例如: + +```json +{ + "logger": { + "dir": "/path/to/config/config.default.js" + } +} +``` + +[Set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set +[extend]: https://github.com/justmoon/node-extend +[extend2]: https://github.com/eggjs/extend2 diff --git a/site/docs/basics/controller.md b/site/docs/basics/controller.md new file mode 100644 index 0000000000..c3e0858bdf --- /dev/null +++ b/site/docs/basics/controller.md @@ -0,0 +1,1096 @@ +--- +title: Controller +order: 7 +--- + +## What is Controller + +[The previous chapter](./router.md) says router is mainly used to describe the relationship between the request URL and the Controller that processes the request eventually, so what is a Controller used for? + +Simply speaking, a Controller is used for **parsing user's input and send back the relative result after processing**, for example: + +- In [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) interfaces, Controller accepts parameters from users and sends selected results back to user or modifies data in the database. +- In the HTML page requests, Controller renders related templates to HTML according to different URLs requested and then sends back to users. +- In the proxy servers, Controller transfers user's requests to other servers and sends back process results to users in return. + +The framework recommends that the Controller layer is responsible for processing request parameters(verification and transformation) from user's requests, then calls related business methods in [Service](./service.md), encapsulates and sends back business result: + +1. retrieves parameters passed by HTTP. +1. verifies and assembles parameters. +1. calls the Service to handle business, if necessary, transforms Service process results to satisfy user's requirement. +1. sends results back to user by HTTP. + +## How To Write Controller + +All Controller files must be put under `app/controller` directory, which can support multi-level directory. when accessing, cascading access can be done through directory names. Controllers can be written in various patterns depending on various project scenarios and development styles. + +### Controller Class(Recommended) + +You can write a Controller by defining a Controller class: + +```js +// app/controller/post.js +const Controller = require('egg').Controller; +class PostController extends Controller { + async create() { + const { ctx, service } = this; + const createRule = { + title: { type: 'string' }, + content: { type: 'string' }, + }; + // verify parameters + ctx.validate(createRule); + // assemble parameters + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // calls Service to handle business + const res = await service.post.create(req); + // set response content and status code + ctx.body = { id: res.id }; + ctx.status = 201; + } +} +module.exports = PostController; +``` + +We've defined a `PostController` class above and every method of this Controller can be used in Router, we can locate it from `app.controller` according to the file name and the method name. + +```js +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.post('createPost', '/api/posts', controller.post.create); +}; +``` + +Multi-level directory is supported, for example, put the above code into `app/controller/sub/post.js`, then we could mount it by: + +```js +// app/router.js +module.exports = (app) => { + app.router.post('createPost', '/api/posts', app.controller.sub.post.create); +}; +``` + +The defined Controller class will initialize a new object for every request when accessing the server, and some of the following attributes will be attached to `this` since the Controller classes in the project are inherited from `egg.Controller`. + +- `this.ctx`: the instance of [Context](./extend.md#context) for current request, through which we can access many attributes and methods encapsulated by the framework to handle current request conveniently. +- `this.app`: the instance of [Application](./extend.md#application) for current request, through which we can access global objects and methods provided by the framework. +- `this.service`: [Service](./service.md) defined by the application, through which we can access the abstract business layer, equivalent to `this.ctx.service`. +- `this.config`: the application's run-time [config](./config.md). +- `this.logger`:logger with `debug`,`info`,`warn`,`error`, use to print different level log, is almost the same as [context logger](../core/logger.md#context-logger), but it will append Controller file path for quickly track. + +#### Customized Controller Base Class + +Defining a Controller class helps us not only abstract the Controller layer codes better(e.g. some unified processing can be abstracted as private) but also encapsulate methods that are widely used in the application by defining a customized Controller base class. + +```js +// app/core/base_controller.js +const { Controller } = require('egg'); +class BaseController extends Controller { + get user() { + return this.ctx.session.user; + } + + success(data) { + this.ctx.body = { + success: true, + data, + }; + } + + notFound(msg) { + msg = msg || 'not found'; + this.ctx.throw(404, msg); + } +} +module.exports = BaseController; +``` + +Now we can use base class' methods by inheriting from `BaseController`: + +```js +//app/controller/post.js +const Controller = require('../core/base_controller'); +class PostController extends Controller { + async list() { + const posts = await this.service.listByUser(this.user); + this.success(posts); + } +} +``` + +### Methods Style Controller (It's not recommended, only for compatibility) + +Every Controller is an async function, whose argument is the instance of the request [Context](./extend.md#context) and through which we can access many attributes and methods encapsulated by the framework conveniently. + +For example, when we define a Controller relative to `POST /api/posts`, we create a `post.js` file under `app/controller` directory. + +```js +// app/controller/post.js +exports.create = async (ctx) => { + const createRule = { + title: { type: 'string' }, + content: { type: 'string' }, + }; + // verify parameters + ctx.validate(createRule); + // assemble parameters + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // calls Service to handle business + const res = await ctx.service.post.create(req); + // set response content and status code + ctx.body = { id: res.id }; + ctx.status = 201; +}; +``` + +In the above example, we introduce some new concepts, however it's still intuitive and understandable. We'll explain these new concepts in detail soon. + +## HTTP Basics + +Since Controller is probably the only place to interact with HTTP protocol when developing business logics, it's necessary to have a quick look at how HTTP protocol works before going on. + +If we send a HTTP request to access the previous example Controller: + +``` +curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8' +``` + +The HTTP request sent by curl looks like this: + +``` +POST /api/posts HTTP/1.1 +Host: localhost:3000 +Content-Type: application/json; charset=UTF-8 + +{"title": "controller", "content": "what is controller"} +``` + +The first line of the request contains three information, first two of which are commonly used: + +- method: it's `POST` in this example. +- path: it's `/api/posts`, if the user's request contains query, it will also be placed here. + +From the second line to the place where the first empty line appears is the Header part of the request which includes many useful attributes. as you may see, Host, Content-Type and `Cookie`, `User-Agent`, etc. There are two headers in this request: + +- `Host`: when we send a request in the browser, the domain is resolved to server IP by DNS and, as well, the domain and port are sent to the server in the Host header by the browser. +- `Content-Type`: when we have a body in our request, the Content-Type is provided to describe the type of our request body. + +The whole following content is the request body, which can be brought by POST, PUT, DELETE and other methods. and the server resolves the request body according to Content-Type. + +When the sever finishes to process the request, a HTTP response is sent back to the client: + +``` +HTTP/1.1 201 Created +Content-Type: application/json; charset=utf-8 +Content-Length: 8 +Date: Mon, 09 Jan 2017 08:40:28 GMT +Connection: keep-alive + +{"id": 1} +``` + +The first line contains three segments, among which the [status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) is used mostly, in this case, it's 201 which means the server has created a resource successfully. + +Similar to the request, the header part begins at the second line and ends at the place where the next empty line appears, in this case, they are Content-Type and Content-Length indicating the response format is JSON and the length is 8 bytes. + +The remaining part is the actual content of this response. + +## Acquire HTTP Request Parameters + +It can be seen from the above HTTP request examples that there are many places can be used to put user's request data. The framework provides many convenient methods and attributes by binding the Context instance to Controllers to acquire parameters sent by users through HTTP request. + +### `query` + +Usually the Query String, string following `?` in the URL, is used to send parameters by request of GET type. For example, `category=egg&language=node` in `GET /posts?category=egg&language=node` is the parameter that user sends. We can acquire this parsed parameter body through `ctx.query`: + +```js +class PostController extends Controller { + async listPosts() { + const query = this.ctx.query; + // { + // category: 'egg', + // language: 'node', + // } + } +} +``` + +If duplicated keys exist in Query String, only the first value of this key is used by `ctx.query` and the subsequent appearance will be omitted. That is to say, for request `GET /posts?category=egg&category=koa`, what `ctx.query` acquires is `{ category: 'egg' }`. + +This is for unity reason, because we usually do not design users to pass parameters with same keys in Query String then we write codes like below: + +```js +const key = ctx.query.key || ''; +if (key.startsWith('egg')) { + // do something +} +``` + +Or if someone passes parameters with same keys in Query String on purpose, system error may be thrown. To avoid this, the framework guarantee that the parameter must be a string type whenever it is acquired from `ctx.query`. + +#### `queries` + +Sometimes our system is designed to accept same keys sent by users, like `GET /posts?category=egg&id=1&id=2&id=3`. For this situation, the framework provides `ctx.queries` object to parse Query String and put duplicated data into an array: + +```js +// GET /posts?category=egg&id=1&id=2&id=3 +class PostController extends Controller { + async listPosts() { + console.log(this.ctx.queries); + // { + // category: [ 'egg' ], + // id: [ '1', '2', '3' ], + // } + } +} +``` + +All key on the `ctx.queries` will be an array type if it has a value. + +### Router Params + +In [Router](./router.md) part, we say Router is allowed to declare parameters which can be acquired by `ctx.params`. + +```js +// app.get('/projects/:projectId/app/:appId', 'app.listApp'); +// GET /projects/1/app/2 +class AppController extends Controller { + async listApp() { + assert.equal(this.ctx.params.projectId, '1'); + assert.equal(this.ctx.params.appId, '2'); + } +} +``` + +### `body` + +Although we can pass parameters through URL, but constraints exist: + +- [the browser limits the maximum length of a URL](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers), so too many parameters cannot be passed. +- the server records the full request URL to log files so it is not safe to pass sensitive data through URL. + +In the above HTTP request message example, we can learn, following the header, there's a body part that can be used to put parameters for POST, PUT and DELETE, etc. The `Content-Type` will be sent by clients(browser) in the same time to tell the server the type of request body when there is a body in a general request. Two mostly used data formats are JSON and Form in Web developing for transferring data. + +The [bodyParser](https://github.com/koajs/bodyparser) middleware is built in by the framework to parse the request body of these two kinds of formats into an object mounted to `ctx.request.body`. Since it's not recommended by the HTTP protocol to pass a body by GET and HEAD methods, `ctx.request.body` cannot be used for GET and HEAD methods. + +```js +// POST /api/posts HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"title": "controller", "content": "what is controller"} +class PostController extends Controller { + async listPosts() { + assert.equal(this.ctx.request.body.title, 'controller'); + assert.equal(this.ctx.request.body.content, 'what is controller'); + } +} +``` + +The framework configures some default parameters for bodyParser and has the following features: + +- when Content-Type is `application/json`, `application/json-patch+json`, `application/vnd.api+json` and `application/csp-report`, it parses the request body as JSON format and limits the maximum length of the body down to `100kb`. +- when Content-Type is `application/x-www-form-urlencoded`, it parses the request body as Form format and limits the maximum length of the body down to `100kb`. +- when parses successfully, the body must be an Object(also can be an array). + +The mostly adjusted config field is the maximum length of the request body for parsing which can be configured in `config/config.default.js` to overwrite the default value of the framework: + +```js +module.exports = { + bodyParser: { + jsonLimit: '1mb', + formLimit: '1mb', + }, +}; +``` + +If user request exceeds the maximum length for parsing that we configured, the framework will throw an exception whose status code is `413`; if request body fails to be parsed(e.g. wrong JSON), an exception with status code `400` will be thrown. + +**Note: when adjusting the maximum length of the body for bodyParser, if we have a reverse proxy(Nginx) in front of our application, we may need to adjust its configuration, so that the reverse proxy also supports the same length of request body.** + +**A common mistake is to confuse `ctx.request.body` and `ctx.body`(which is alias for `ctx.response.body`).** + +### Acquiring the Submitted Files + +The `body` in the request can carry parameters as well as files. Generally speaking, our browsers always send files in `multipart/form-data`, and we now have two kinds of ways supporting submitting and acquiring files with the help of the framework's plugin [Multipart](https://github.com/eggjs/egg-multipart). + +- #### `File` Mode: + If you have no ideas about Nodejs's Stream at all, the `File` mode suits you well: + +1. In your config file, enable `file` mode first: + +```js +// config/config.default.js +exports.multipart = { + mode: 'file', +}; +``` + +2. Submitting / Acquiring Files: + +1) For Single File: + +Your HTML static front-end codes should look like this below: + +```html +
    + title: file: + +
    +``` + +The corresponding backend codes are: + +```js +// app/controller/upload.js +const Controller = require('egg').Controller; +const fs = require('fs/promises'); + +module.exports = class extends Controller { + async upload() { + const { ctx } = this; + const file = ctx.request.files[0]; + const name = 'egg-multipart-test/' + path.basename(file.filename); + let result; + try { + // process file (e.g: upload to cloud storage) + result = await ctx.oss.put(name, file.filepath); + } finally { + // need to remove the tmp file + await fs.unlink(file.filepath); + } + + ctx.body = { + url: result.url, + // get all field values + requestBody: ctx.request.body, + }; + } +}; +``` + +2. For Multiple Files: + +For multiple files, with the help of `ctx.request.files`, we can loop each of them and do what process we like: + +Your HTML static front-end codes should look like this below: + +```html +
    + title: file1: file2: + + +
    +``` + +The corresponding backend codes are: + +```js +// app/controller/upload.js +const Controller = require('egg').Controller; +const fs = require('fs/promises'); + +module.exports = class extends Controller { + async upload() { + const { ctx } = this; + console.log(ctx.request.body); + console.log('got %d files', ctx.request.files.length); + for (const file of ctx.request.files) { + console.log('field: ' + file.fieldname); + console.log('filename: ' + file.filename); + console.log('encoding: ' + file.encoding); + console.log('mime: ' + file.mime); + console.log('tmp filepath: ' + file.filepath); + let result; + try { + // process file (e.g: upload to cloud storage) + result = await ctx.oss.put( + 'egg-multipart-test/' + file.filename, + file.filepath, + ); + } finally { + // need to remove the tmp file + await fs.unlink(file.filepath); + } + console.log(result); + } + } +}; +``` + +- #### `Stream` Mode + If you are very familiar with `Stream` in Nodejs, you can choose this way. In a controller, we can fetch the uploaded files through `ctx.getFileStream()`. + +1. For Single File: + +```html +
    + title: file: + +
    +``` + +```js +const path = require('path'); +const sendToWormhole = require('stream-wormhole'); +const Controller = require('egg').Controller; + +class UploaderController extends Controller { + async upload() { + const ctx = this.ctx; + const stream = await ctx.getFileStream(); + const name = 'egg-multipart-test/' + path.basename(stream.filename); + let result; + try { + // process file (e.g: upload to cloud storage) + result = await ctx.oss.put(name, stream); + } catch (err) { + // You MUST consume the file stream, otherwises the browser cannot response any more + await sendToWormhole(stream); + throw err; + } + + ctx.body = { + url: result.url, + // All the fields in the form can be fetched through `stream.fields` + fields: stream.fields, + }; + } +} + +module.exports = UploaderController; +``` + +To acquire the uploaded files easily, there're two conditions at least: + +- Only ONE file per time. +- The field of uploading file MUST be after the other fields in a form, otherwise you cannot get other fields after getting the file stream. + +2. For Multiple Files: + +For multiple files, you should do the following instead of using `ctx.getFileStream()`: + +```js +const sendToWormhole = require('stream-wormhole'); +const Controller = require('egg').Controller; + +class UploaderController extends Controller { + async upload() { + const ctx = this.ctx; + const parts = ctx.multipart(); + let part; + // parts() return a promise + while ((part = await parts()) != null) { + if (part.length) { + // arrays are busboy fields + console.log('field: ' + part[0]); + console.log('value: ' + part[1]); + console.log('valueTruncated: ' + part[2]); + console.log('fieldnameTruncated: ' + part[3]); + } else { + if (!part.filename) { + // When a user clicks `upload` before choosing a file, + // `part` will be file stream, but `part.filename` is empty. + // We must handler this by notifying the user that he/she should + // choose a file before submitting + return; + } + // otherwise, it's a fully-filled stream + console.log('field: ' + part.fieldname); + console.log('filename: ' + part.filename); + console.log('encoding: ' + part.encoding); + console.log('mime: ' + part.mime); + let result; + try { + // process file (e.g: upload to cloud storage) + result = await ctx.oss.put( + 'egg-multipart-test/' + part.filename, + part, + ); + } catch (err) { + // You MUST consume the file stream, otherwises the browser cannot response any more + await sendToWormhole(part); + throw err; + } + console.log(result); + } + } + console.log('and we are done parsing the form!'); + } +} + +module.exports = UploaderController; +``` + +The framework also has the limits for the safety of uploading files, the default white list is: + +```js +// images +'.jpg', '.jpeg', // image/jpeg +'.png', // image/png, image/x-png +'.gif', // image/gif +'.bmp', // image/bmp +'.wbmp', // image/vnd.wap.wbmp +'.webp', +'.tif', +'.psd', +// text +'.svg', +'.js', '.jsx', +'.json', +'.css', '.less', +'.html', '.htm', +'.xml', +// tar +'.zip', +'.gz', '.tgz', '.gzip', +// video +'.mp3', +'.mp4', +'.avi', +``` + +Users can add new file extensions in `config/config.default.js`, or rewrite a whole white list: + +- Newly-added a file extension: + +```js +module.exports = { + multipart: { + fileExtensions: ['.apk'], // Add support for apk files + }, +}; +``` + +- Overwriting a whole white list: + +```js +module.exports = { + multipart: { + whitelist: ['.png'], // ONLY files of png is allowed + }, +}; +``` + +**Notice:`fileExtensions` will be IGNORED when `whitelist` is overwritten.** + +For more tech details about this, please refer [Egg-Multipart](https://github.com/eggjs/egg-multipart). + +### `header` + +Apart from URL and request body, some parameters can be sent by request header. The framework provides some helper attributes and methods to retrieve them. + +- `ctx.headers`, `ctx.header`, `ctx.request.headers`, `ctx.request.header`: these methods are equivalent and all of them get the whole header object. +- `ctx.get(name)`, `ctx.request.get(name)`: get the value of one parameter from the request header, if the parameter does not exist, an empty string will be returned. +- We recommend you use `ctx.get(name)` rather than `ctx.headers['name']` because the former handles upper/lower case automatically. + +Since header is special, some of which are given specific meanings by the `HTTP` protocol (like `Content-Type`, `Accept`), some are set by the reverse proxy as a convention (X-Forwarded-For), and the framework provides some convenient getters for them as well, for more details please refer to [API](https://eggjs.org/api/). + +Specially when we set `config.proxy = true` to deploy the application behind the reverse proxy (Nginx), some Getters' internal process may be changed. + +#### `ctx.host` + +Reads the header's value configured by `config.hostHeaders` firstly, if fails, then it tries to get the value of host header, if fails again, it returns an empty string. + +`config.hostHeaders` defaults to `x-forwarded-host`. + +#### `ctx.protocol` + +When you get protocol through this Getter, it checks whether current connection is an encrypted one or not, if it is, it returns HTTPS. + +When current connection is not an encrypted one, it reads the header's value configured by `config.protocolHeaders` to check HTTP or HTTPS, if it fails, we can set a safe-value(defaults to HTTP) through `config.protocol` in the configuration. + +`config.protocolHeaders` defaults to `x-forwarded-proto`. + +#### `ctx.ips` + +A IP address list of all intermediate equipments that a request go through can be get by `ctx.ips`, only when `config.proxy = true`, it reads the header's value configured by `config.ipHeaders` instead, if fails, it returns an empty array. + +`config.ipHeaders` defaults to `x-forwarded-for`. + +#### `ctx.ip` + +The IP address of the sender of the request can be get by `ctx.ip`, it reads from `ctx.ips` firstly, if `ctx.ips` is empty, it returns the connection sender's IP address. + +**Note: `ip` and `ips` are different, if `config.proxy = false`, `ip` returns the connection sender's IP address while `ips` returns an empty array.** + +### Cookie + +All HTTP requests are stateless but, on the contrary, our Web applications usually need to know who sends the requests. To make it through, the HTTP protocol designs a special request header: [Cookie](https://en.wikipedia.org/wiki/HTTP_cookie). With the response header (set-cookie), the server is able to send a few data to the client, the browser saves these data according to the protocol and brings them along with the next request(according to the protocol and for safety reasons, only when accessing websites that match the rules specified by Cookie does the browser bring related Cookies). + +Through `ctx.cookies`, we can conveniently and safely set and get Cookie in Controller. + +```js +class CookieController extends Controller { + async add() { + const ctx = this.ctx; + let count = ctx.cookies.get('count'); + count = count ? Number(count) : 0; + ctx.cookies.set('count', ++count); + ctx.body = count; + } + + async remove() { + const ctx = this.ctx; + const count = ctx.cookies.set('count', null); + ctx.status = 204; + } +} +``` + +Although Cookie is only a header in HTTP, multiple key-value pairs can be set in the format of `foo=bar;foo1=bar1;`. + +In Web applications, Cookie is usually used to send the identity information of the client, so it has many safety related configurations which can not be ignored, [Cookie](../core/cookie-and-session.md#cookie) explains the usage and safety related configurations of Cookie in detail and is worth being read in depth. + +#### Configuration + +There are mainly these attributes below can be used to configure default Cookie options in `config.default.js`: + +```js +module.exports = { + cookies: { + // httpOnly: true | false, + // sameSite: 'none|lax|strict', + }, +}; +``` + +e.g.: Configured application level Cookie [SameSite](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html) property to `Lax`. + +```js +module.exports = { + cookies: { + sameSite: 'lax', + }, +}; +``` + +### Session + +By using Cookie, we can create an individual Session specific to every user to store user identity information, which will be encrypted then stored in Cookie to perform session persistence across requests. + +The framework builds in [Session](https://github.com/eggjs/egg-session) plugin, which provides `ctx.session` for us to get or set current user's Session. + +```js +class PostController extends Controller { + async fetchPosts() { + const ctx = this.ctx; + // get data from Session + const userId = ctx.session.userId; + const posts = await ctx.service.post.fetch(userId); + // set value to Session + ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1; + ctx.body = { + success: true, + posts, + }; + } +} +``` + +It's quite intuitional to use Session, just get or set directly, if you want to delete it, you can assign it to `null`: + +```js +class SessionController extends Controller { + async deleteSession() { + this.ctx.session = null; + } +} +``` + +Like Cookie, Session has many safety related configurations and functions etc., so it's better to read [Session](../core/cookie-and-session.md#session) in depth in ahead. + +#### Configuration + +There are mainly these attributes below can be used to configure Session in `config.default.js`: + +```js +module.exports = { + key: 'EGG_SESS', // the name of key-value pairs, which is specially used by Cookie to store Session + maxAge: 86400000, // Session maximum valid time +}; +``` + +## Parameter Validation + +After getting parameters from user requests, in most cases, it is inevitable to validate these parameters. + +With the help of the convenient parameter validation mechanism provided by [Validate](https://github.com/eggjs/egg-validate) plugin, with which we can do all kinds of complex parameter validations. + +```js +// config/plugin.js +exports.validate = { + enable: true, + package: 'egg-validate', +}; +``` + +Validate parameters directly through `ctx.validate(rule, [body])`: + +```js +class PostController extends Controller { + async create() { + // validate parameters + // if the second parameter is absent, `ctx.request.body` is validated automatically + this.ctx.validate({ + title: { type: 'string' }, + content: { type: 'string' }, + }); + } +} +``` + +When the validation fails, an exception will be thrown immediately with an error code of 422 and an errors field containing the detailed information why it fails. You can capture this exception through `try catch` and handle it all by yourself. + +```js +class PostController extends Controller { + async create() { + const ctx = this.ctx; + try { + ctx.validate(createRule); + } catch (err) { + ctx.logger.warn(err.errors); + ctx.body = { success: false }; + return; + } + } +} +``` + +### Validation Rules + +The parameter validation is done by [Parameter](https://github.com/node-modules/parameter#rule), and all supported validation rules can be found in its document. + +#### Customizing Validation Rules + +In addition to built-in validation types introduced in the previous section, sometimes we hope to customize several validation rules to make the development more convenient and now customized rules can be added through `app.validator.addRule(type, check)`. + +```js +// app.js +app.validator.addRule('json', (rule, value) => { + try { + JSON.parse(value); + } catch (err) { + return 'must be json string'; + } +}); +``` + +After adding the customized rule, it can be used immediately in Controller to do parameter validation. + +```js +class PostController extends Controller { + async handler() { + const ctx = this.ctx; + // query.test field must be a json string + const rule = { test: 'json' }; + ctx.validate(rule, ctx.query); + } +} +``` + +## Using Service + +We do not prefer to implement too many business logics in Controller, so a [Service](./service.md) layer is provided to encapsulate business logics, which not only increases the reusability of codes but also makes it easy for us to test our business logics. + +In Controller, any method of any Service can be called and, in the meanwhile, Service is lazy loaded which means it is only initialized by the framework on the first time it is accessed. + +```js +class PostController extends Controller { + async create() { + const ctx = this.ctx; + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // using service to handle business logics + const res = await ctx.service.post.create(req); + ctx.body = { id: res.id }; + ctx.status = 201; + } +} +``` + +To write a Service in detail, please refer to [Service](./service.md). + +## Sending HTTP Response + +After business logics are handled, the last thing Controller should do is to send the processing result to users with an HTTP response. + +### Setting Status + +HTTP designs many [Status Code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes), each of which indicates a specific meaning, and setting the status code correctly makes the response more semantic. + +The framework provides a convenient Setter to set the status code: + +```js +class PostController extends Controller { + async create() { + // set status code to 201 + this.ctx.status = 201; + } +} +``` + +As to which status code should be used for a specific case, please refer to status code meanings on [List of HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) + +### Body Setting + +Most data is sent to requesters through the body and, just like the body in the request, the body sent by the response demands a set of corresponding Content-Type to inform clients how to parse data. + +- for a RESTful API controller, we usually send a body whose Content-Type is `application/json`, indicating it's a JSON string. +- for a HTML page controller, we usually send a body whose Content-Type is `text/html`, indicating it's a piece of HTML code. + +**Note: `ctx.body` is alias of `ctx.response.body`, don't confuse with `ctx.request.body`.** + +```js +class ViewController extends Controller { + async show() { + this.ctx.body = { + name: 'egg', + category: 'framework', + language: 'Node.js', + }; + } + + async page() { + this.ctx.body = '

    Hello

    '; + } +} +``` + +Due to the Stream feature of Node.js, we need to return the response by Stream in some cases, e.g., returning a big file, the proxy server returns content from upstream straightforward, the framework also supports setting the body into a Stream directly and handling error events on this stream well in the meanwhile. + +```js +class ProxyController extends Controller { + async proxy() { + const ctx = this.ctx; + const result = await ctx.curl(url, { + streaming: true, + }); + ctx.set(result.header); + // result.res is stream + ctx.body = result.res; + } +} +``` + +#### Rendering Template + +Usually we do not write HTML pages by hand, instead we generate them by a template engine. +Egg itself does not integrate any template engine, but it establishes the [View Plugin Specification](../advanced/view-plugin.md). Once the template engine is loaded, `ctx.render(template)` can be used to render templates to HTML directly. + +```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' }); + } +} +``` + +For detailed examples, please refer to [Template Rendering](../core/view.md). + +#### JSONP + +Sometimes we need to provide API services for pages in a different domain, and, for historical reasons, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) fails to make it through, while [JSONP](https://en.wikipedia.org/wiki/JSONP) does. + +Since misuse of JSONP leads to dozens of security issues, the framework supplies a convenient way to respond JSONP data, encapsulating [JSONP XSS Related Security Precautions](../core/security.md#jsonp-xss), and supporting the validation of CSRF and referrer. + +- `app.jsonp()` provides a middleware for the controller to respond JSONP data. We may add this middleware to the router that needs to support jsonp: + +```js +// app/router.js +module.exports = (app) => { + const jsonp = app.jsonp(); + app.router.get('/api/posts/:id', jsonp, app.controller.posts.show); + app.router.get('/api/posts', jsonp, app.controller.posts.list); +}; +``` + +- We just program as usual in the Controller: + +```js +// app/controller/posts.js +class PostController extends Controller { + async show() { + this.ctx.body = { + name: 'egg', + category: 'framework', + language: 'Node.js', + }; + } +} +``` + +When user's requests access this controller through a corresponding URL, if the query contains the `_callback=fn` parameter, data is returned in JSONP format, otherwise in JSON format. + +##### JSONP Configuration + +By default, the framework determines whether to return data in JSONP format or not depending on the `_callback` parameter in the query, and the method name set by `_callback` must be less than 50 characters. Applications may overwrite the default configuration globally in `config/config.default.js`: + +```js +// config/config.default.js +exports.jsonp = { + callback: 'callback', // inspecting the `callback` parameter in the query + limit: 100, // the maximum size of the method name is 100 characters +}; +``` + +With the configuration above, if a user requests `/api/posts/1?callback=fn`, a JSONP format response is sent, if `/api/posts/1`, a JSON format response is sent. + +Also we can overwrite the default configuration in `app.jsonp()` when creating the middleware and therefore separate configurations is used for separate routers: + +```js +// app/router.js +module.exports = (app) => { + const { router, controller, jsonp } = app; + router.get( + '/api/posts/:id', + jsonp({ callback: 'callback' }), + controller.posts.show, + ); + router.get('/api/posts', jsonp({ callback: 'cb' }), controller.posts.list); +}; +``` + +#### XSS Defense Configuration + +By default, XSS is not defended when responding JSONP, and, in some cases, it is quite dangerous. We classify JSONP APIs into three type grossly: + +1. querying non-sensitive data, e.g. getting the public post list of a BBS. +2. querying sensitive data, e.g. getting the transaction record of a user. +3. submitting data and modifying the database, e.g. create a new order for a user. + +If our JSONP API provides the last two type services and, without any XSS defense, user's sensitive data may be leaked and even user may be phished. Given this, the framework supports the validations of CSRF and referrer by default. + +##### CSRF + +In the JSONP configuration, we could enable the CSRF validation for JSONP APIs simply by setting `csrf: true`. + +```js +// config/config.default.js +module.exports = { + jsonp: { + csrf: true, + }, +}; +``` + +**Note: the CSRF validation depends on the Cookie based CSRF validation provided by [security](../core/security.md).** + +When the CSRF validation is enabled, the client should bring CSRF token as well when it sends a JSONP request, if the page where the JSONP sender belongs to shares the same domain with our services, CSRF token in Cookie can be read(CSRF can be set manually if it is absent), and is brought together with the request. + +##### Validation Reference + +The CSRF way can be used for JSONP request validation only if the main domains are the same, while providing JSONP services for pages in different domains, we can limit JSONP senders into a controllable rang by configuring the referrer whitelist. + +```js +//config/config.default.js +exports.jsonp = { + whiteList: /^https?:\/\/test.com\//, + // whiteList: '.test.com', + // whiteList: 'sub.test.com', + // whiteList: [ 'sub.test.com', 'sub2.test.com' ], +}; +``` + +`whileList` can be configured as regular expression, string and array: + +- Regular Expression: only requests whose Referrer match the regular expression are allowed to access the JSONP API. When composing the regular expression, please also notice the leading `^` and tail `\/` which guarantees the whole domain matches. + +```js +exports.jsonp = { + whiteList: /^https?:\/\/test.com\//, +}; +// matches referrer: +// https://test.com/hello +// http://test.com/ +``` + +- String: two cases exists when configuring the whitelist as a string, if the string begins with a `.`, e.g. `.test.com`, the referrer whitelist indicates all sub-domains of `test.com`, `test.com` itself included. if the string does not begin with a `.`, e.g. `sub.test.com`, it indicates `sub.test.com` one domain only. (both HTTP and HTTPS are supported) + +```js +exports.jsonp = { + whiteList: '.test.com', +}; +// matches domain test.com: +// https://test.com/hello +// http://test.com/ + +// matches subdomain +// https://sub.test.com/hello +// http://sub.sub.test.com/ + +exports.jsonp = { + whiteList: 'sub.test.com', +}; +// only matches domain sub.test.com: +// https://sub.test.com/hello +// http://sub.test.com/ +``` + +- Array: when the whitelist is configured as an array, the referrer validation is passed only if at least one condition represented by array items is matched. + +```js +exports.jsonp = { + whiteList: ['sub.test.com', 'sub2.test.com'], +}; +// matches domain sub.test.com and sub2.test.com: +// https://sub.test.com/hello +// http://sub2.test.com/ +``` + +**If both CSRF and referrer validation are enabled, the request sender passes any one of them passes the JSONP security validation.** + +### Header Setting + +We identify whether the request is successful or not by using the status code and set response content in the body. By setting the response header, extended information can be set as well. + +`ctx.set(key, value)` sets one response header and `ctx.set(headers)` sets many in one time. + +```js +// app/controller/api.js +class ProxyController extends Controller { + async show() { + const ctx = this.ctx; + const start = Date.now(); + ctx.body = await ctx.service.post.get(); + const used = Date.now() - start; + // set one response header + ctx.set('show-response-time', used.toString()); + } +} +``` + +### Redirect + +The framework overwrites koa's native `ctx.redirect` implementation with a security plugin to provide a more secure redirect. + +- `ctx.redirect(url)` Forbids redirect if it is not in the configured whitelist domain name. +- `ctx.unsafeRedirect(url)` does not determine the domain name and redirect directly. Generally, it is not recommended to use it. Use it after clearly understanding the possible risks. + +If you use the `ctx.redirect` method, you need to configure the application configuration file as follows: + +```js +// config/config.default.js +exports.security = { + domainWhiteList: ['.domain.com'], // Security whitelist, starts with `.` +}; +``` + +If the user does not configure the `domainWhiteList` or the `domainWhiteList` array to be empty, then all redirect requests will be released by default, which is equivalent to `ctx.unsafeRedirect(url)`. diff --git a/site/docs/basics/controller.zh-CN.md b/site/docs/basics/controller.zh-CN.md new file mode 100644 index 0000000000..d68544ba05 --- /dev/null +++ b/site/docs/basics/controller.zh-CN.md @@ -0,0 +1,1080 @@ +--- +title: 控制器(Controller) +order: 7 +--- + +## 什么是 Controller + +[前面章节](./router.md) 提到,我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller,那么 Controller 主要有什么职责呢? + +简单地说,Controller 负责**解析用户的输入,处理后返回相应的结果**。例如: + +- 在 [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户,或将用户的请求更新到数据库中。 +- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML,后返回给用户。 +- 在代理服务器中,Controller 将用户的请求转发到其他服务器,之后将那些服务器的处理结果返回给用户。 + +框架推荐的 Controller 层主要流程是:首先对用户通过 HTTP 传递过来的请求参数进行处理(校验、转换),然后调用对应的 [service](./service.md) 方法处理业务,在必要时把 Service 的返回结果处理转换,使之满足用户需求,最后通过 HTTP 将结果响应给用户。具体步骤如下: + +1. 获取用户通过 HTTP 传递过来的请求参数。 +2. 校验、组装参数。 +3. 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用户的需求。 +4. 通过 HTTP 将结果响应给用户。 +## 如何编写 Controller + +所有的 Controller 文件都必须放在 `app/controller` 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。Controller 支持多种形式进行编写,可以根据不同的项目场景和开发习惯来选择。 + +### Controller 类(推荐) + +我们可以通过定义 Controller 类的方式来编写代码: + +```javascript +// app/controller/post.js +const Controller = require('egg').Controller; +class PostController extends Controller { + async create() { + const { ctx, service } = this; + const createRule = { + title: { type: 'string' }, + content: { type: 'string' }, + }; + // 校验参数 + ctx.validate(createRule); + // 组装参数 + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // 调用 Service 进行业务处理 + const res = await service.post.create(req); + // 设置响应内容和响应状态码 + ctx.body = { id: res.id }; + ctx.status = 201; + } +} +module.exports = PostController; +``` + +我们通过上述代码定义了一个 `PostController` 的类,类里面的每一个方法都可以作为一个 Controller 在 Router 中引用。下面是如何在 `app.router` 中根据文件名和方法名定位到它的示例: + +```javascript +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.post('createPost', '/api/posts', controller.post.create); +}; +``` + +Controller 支持多级目录。例如,如果我们将上面的 Controller 代码放到 `app/controller/sub/post.js` 中,那么可以在 router 中这样使用: + +```javascript +// app/router.js +module.exports = app => { + app.router.post('createPost', '/api/posts', app.controller.sub.post.create); +}; +``` + +定义的 Controller 类在每一个请求访问到服务器时实例化一个全新的对象,而项目中的 Controller 类继承于 `egg.Controller`,会有以下几个属性挂在 `this` 上: + +- `this.ctx`:当前请求的上下文 [Context](./extend.md#context) 对象的实例,通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。 +- `this.app`:当前应用 [Application](./extend.md#application) 对象的实例,通过它我们可以拿到框架提供的全局对象和方法。 +- `this.service`:应用定义的 [Service](./service.md),通过它我们可以访问抽象出的业务层,等价于 `this.ctx.service`。 +- `this.config`:应用运行时的[配置项](./config.md)。 +- `this.logger`:logger 对象,上面有四个方法(`debug`、`info`、`warn`、`error`),分别代表打印四个不同级别的日志。使用方法和效果与 [context logger](../core/logger.md#context-logger) 中介绍的相同,但是通过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。 + +#### 自定义 Controller 基类 + +按照类的方式编写 Controller,不仅可以让我们更好地对 Controller 层代码进行抽象(例如,将一些统一的处理抽象为一些私有方法),还可以通过自定义 Controller 基类的方式封装应用中常用的方法。 + +```javascript +// app/core/base_controller.js +const { Controller } = require('egg'); +class BaseController extends Controller { + get user() { + return this.ctx.session.user; + } + + success(data) { + this.ctx.body = { + success: true, + data, + }; + } + + notFound(msg) { + msg = msg || 'not found'; + this.ctx.throw(404, msg); + } +} +module.exports = BaseController; +``` + +在编写应用的 Controller 时,可以继承 BaseController,直接使用基类上的方法: + +```javascript +// app/controller/post.js +const Controller = require('../core/base_controller'); +class PostController extends Controller { + async list() { + const posts = await this.service.listByUser(this.user); + this.success(posts); + } +} +``` + +### Controller 方法(不推荐使用,只是为了兼容) + +每一个 Controller 都是一个 `async function`,其入参为请求的上下文 [Context](./extend.md#context) 对象的实例。通过它,我们可以拿到框架封装好的各种便捷属性和方法。 + +例如,我们编写一个对应到 `POST /api/posts` 接口的 Controller,我们需要在 `app/controller` 目录下创建一个 `post.js` 文件: + +```javascript +// app/controller/post.js +exports.create = async ctx => { + const createRule = { + title: { type: 'string' }, + content: { type: 'string' }, + }; + // 校验参数 + ctx.validate(createRule); + // 组装参数 + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // 调用 Service 进行业务处理 + const res = await ctx.service.post.create(req); + // 设置响应内容和响应状态码 + ctx.body = { id: res.id }; + ctx.status = 201; +}; +``` + +以上是一个简单直观的例子,我们引入了一些新的概念,但它们都是易于理解的。我们将在后面对它们进行更详细的介绍。 +## HTTP 基础 + +由于控制器(Controller)基本上是业务开发中唯一与 HTTP 协议打交道的地方,在继续深入了解之前,我们首先要简单了解一下 HTTP 协议本身。 + +假设我们发起一个 HTTP 请求来访问前面例子中提及的 Controller: + +``` +curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8' +``` + +通过 curl 发出的 HTTP 请求内容就如下: + +``` +POST /api/posts HTTP/1.1 +Host: localhost:3000 +Content-Type: application/json; charset=UTF-8 + +{"title": "controller", "content": "what is controller"} +``` + +请求的第一行包含三个信息,我们较为常用的是前两个: + +- 方法(method):这个请求中 method 的值是 `POST`。 +- 路径(path):值为 `/api/posts`,如果用户请求中含 query,则也会在此出现。 + +从第二行开始至第一个空行之前,都是请求的头部(Headers)部分。这里有众多常用属性,如 Host、Content-Type,以及 `Cookie`、`User-Agent` 等。在本次请求中有两个头信息: + +- `Host`:浏览器发起请求时,会使用域名通过 DNS 解析找到服务器的 IP 地址,浏览器还会将域名和端口号放进 Host 头内发送给服务器。 +- `Content-Type`:请求中如有请求体(body),通常伴随 Content-Type,标明请求体格式。 + +之后内容为请求体,POST、PUT、DELETE 等方法可附带请求体,服务端根据 Content-Type 解析请求体。 + +服务器处理请求后,会发送一个 HTTP 响应给客户端: + +``` +HTTP/1.1 201 Created +Content-Type: application/json; charset=utf-8 +Content-Length: 8 +Date: Mon, 09 Jan 2017 08:40:28 GMT +Connection: keep-alive + +{"id": 1} +``` + +响应的首行也包括三部分,其中常用的主要是[响应状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)。本例中为 201,意味服务端成功创建了资源。 + +响应头从第二行至下一个空行,这里的 Content-Type 和 Content-Length 表明响应格式为 JSON,长度 8 字节。 + +最后部分即响应实际内容。 +## 获取 HTTP 请求参数 + +从上述 HTTP 请求示例中, 我们可以看到, 多个位置可以放置用户的请求数据。框架通过在 Controller 上绑定的 Context 实例, 提供了多种便捷方法和属性, 以获取用户通过 HTTP 请求发送过来的参数。 + +### Query + +在 URL 中 `?` 后的部分是 Query String。这部分经常用于 GET 类型请求中传递参数。例如,`GET /posts?category=egg&language=node` 中的 `category=egg&language=node` 就是用户传递的参数。我们可以通过 `ctx.query` 获取解析后的这个参数对象。 + +```js +class PostController extends Controller { + async listPosts() { + const query = this.ctx.query; + // 输出: + // { + // category: 'egg', + // language: 'node' + // } + } +} +``` + +当 Query String 中的 key 重复时,`ctx.query` 只取第一次出现的值,后续的都会被忽略。例如 `GET /posts?category=egg&category=koa`,通过 `ctx.query` 获取的值将是 `{ category: 'egg' }`。 + +之所以这样处理是为了保持一致性。一般我们不会设计让用户传递相同 key 的 Query String,所以经常编写如下代码: + +```js +const key = ctx.query.key || ''; +if (key.startsWith('egg')) { + // 执行相应操作 +} +``` + +如果有人故意在 Query String 中带上重复的 key 请求,就会引发系统异常。因此框架确保从 `ctx.query` 获取的参数一旦存在,一定是字符串类型。 + +#### Queries + +有些系统会设计为让用户传递相同的 key,例如 `GET /posts?category=egg&id=1&id=2&id=3`。框架提供了 `ctx.queries` 对象,它同样解析了 Query String,但不会丢弃任何重复数据,而是将它们放进一个数组。 + +```js +// GET /posts?category=egg&id=1&id=2&id=3 +class PostController extends Controller { + async listPosts() { + console.log(this.ctx.queries); + // 输出: + // { + // category: [ 'egg' ], + // id: [ '1', '2', '3' ] + // } + } +} +``` + +`ctx.queries` 中所有 key 的值,如果存在, 必然是数组类型。 + +### Router Params + +在 [Router](./router.md) 文档中,我们介绍了可以在 Router 上声明参数,所有参数可通过 `ctx.params` 获取。 + +```js +// app.get('/projects/:projectId/app/:appId', 'app.listApp'); +// GET /projects/1/app/2 +class AppController extends Controller { + async listApp() { + assert.equal(this.ctx.params.projectId, '1'); + assert.equal(this.ctx.params.appId, '2'); + } +} +``` + +### Body + +我们也可以通过 URL 传参,但存在一些限制: + +- [浏览器对 URL 长度有限制](http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers);若参数过多可能无法传递。 +- 服务端通常会将完整 URL 记录到日志中;敏感数据如果通过 URL 传递可能不安全。 + +在 HTTP 请求报文中,Header 之后有一个 Body 部分。POST、PUT 和 DELETE 等方法常在此传递参数。请求中若有 Body,则客户端(浏览器)会发送 `Content-Type` 告知服务端该请求 body 的格式。WEB 应用数据传输中最常见的格式有 JSON 和 Form。 + +框架内置了 [bodyParser](https://github.com/koajs/bodyparser) 中间件,可解析这两种格式请求的 body 并挂载到 `ctx.request.body` 上。`GET`、`HEAD` 方法不建议传递 body,因此无法按此方法获取内容。 + +```js +// POST /api/posts HTTP/1.1 +// Host: localhost:3000 +// Content-Type: application/json; charset=UTF-8 +// +// {"title": "controller", "content": "what is controller"} +class PostController extends Controller { + async listPosts() { + assert.equal(this.ctx.request.body.title, 'controller'); + assert.equal(this.ctx.request.body.content, 'what is controller'); + } +} +``` + +框架为 bodyParser 中间件配置了默认参数。配置后具备以下特性: + +- Content-Type 为 `application/json`,`application/json-patch+json`,`application/vnd.api+json` 和 `application/csp-report` 时,按 json 格式解析请求 body,限制最大长度 `100kb`。 +- Content-Type 为 `application/x-www-form-urlencoded` 时,按 form 格式解析请求 body,限制最大长度 `100kb`。 +- 若解析成功,body 必然是 Object(Array)。 + +通常我们会调整配置项以变更解析时允许的最大长度,可在 `config/config.default.js` 中修改默认值。 + +```js +module.exports = { + bodyParser: { + jsonLimit: '1mb', + formLimit: '1mb', + }, +}; +``` + +如果请求 body 超过配置的最大长度,会抛出状态码 `413` 的异常;body 解析失败(如错误 JSON)会抛出状态码 `400` 的异常。 + +**注意:调整 bodyParser 支持的 body 长度时,如果应用之前有一层反向代理(如 Nginx),同样需要调整配置确保支持相等长度的请求 body。** + +**常见错误:将 `ctx.request.body` 与 `ctx.body` 混淆,后者实际上是 `ctx.response.body` 的简写。** +### 获取上传的文件 + +请求体除了可以带参数之外,还可以发送文件。通常情况下,浏览器会通过 `Multipart/form-data` 格式发送文件。通过内置的 [Multipart](https://github.com/eggjs/egg-multipart) 插件,框架支持获取用户上传的文件。我们为你提供了两种方式: + +#### File 模式 + +如果你不熟悉 Node.js 中的 Stream 用法,那么 File 模式非常适合你: + +1)在 config 文件中启用 `file` 模式: + +```javascript +// config/config.default.js +exports.multipart = { + mode: 'file', +}; +``` + +2)上传/接收文件: + +1. 上传/接收单个文件: + +你的前端静态页面代码可能如下所示: + +```html +
    + title: + file: + +
    +``` + +对应的后端代码如下: + +```javascript +// app/controller/upload.js +const Controller = require('egg').Controller; +const fs = require('fs/promises'); +const path = require('path'); // 补上缺失的 path 模块 + +class UploadController extends Controller { + async upload() { + const { ctx } = this; + const file = ctx.request.files[0]; + const name = 'egg-multipart-test/' + path.basename(file.filename); + let result; + try { + // 处理文件,例如上传到云采存储 + result = await ctx.oss.put(name, file.filepath); + } finally { + // 注意删除临时文件 + await fs.unlink(file.filepath); + } + + ctx.body = { + url: result.url, + // 获取全部字段值 + requestBody: ctx.request.body, + }; + } +} + +module.exports = UploadController; +``` + +2. 上传/接收多个文件: + +对于多个文件,可以使用 `ctx.request.files` 数组进行遍历,然后分别处理每个文件。以下是你的前端静态页面的代码: + +```html +
    + title: + file1: + file2: + +
    +``` + +对应的后端代码如下: + +```javascript +// app/controller/upload.js +const Controller = require('egg').Controller; +const fs = require('fs/promises'); +const path = require('path'); // 补上缺失的 path 模块 + +class UploadController extends Controller { + async upload() { + const { ctx } = this; + console.log(ctx.request.body); + console.log(`共收到 ${ctx.request.files.length} 个文件`); + for (const file of ctx.request.files) { + console.log(`字段名: ${file.fieldname}`); + console.log(`文件名: ${file.filename}`); + console.log(`编码: ${file.encoding}`); + console.log(`MIME 类型: ${file.mime}`); + console.log(`临时文件路径: ${file.filepath}`); + let result; + try { + // 处理文件,例如上传到云采存储 + result = await ctx.oss.put( + 'egg-multipart-test/' + file.filename, + file.filepath, + ); + } finally { + // 注意删除临时文件 + await fs.unlink(file.filepath); + } + console.log(result); + } + } +} + +module.exports = UploadController; +``` + +以上代码包涵了前端的表单代码以及后端处理上传文件的代码。在服务器端,我们首先获取上传文件的信息,然后将文件上传到指定的储存系统,例如云储存。随后,我们确保了临时文件被删除,防止占用服务器空间。 +#### Stream 模式 + +如果你对 Node 中的 Stream 模式非常熟悉,那么你可以选择此模式。在 Controller 中,我们可以通过 `ctx.getFileStream()` 接口获取到上传的文件流。 + +1. 上传/接受单个文件: + +```html +
    + title: file: + +
    +``` + +```js +const path = require('path'); +const sendToWormhole = require('stream-wormhole'); +const Controller = require('egg').Controller; + +class UploaderController extends Controller { + async upload() { + const ctx = this.ctx; + const stream = await ctx.getFileStream(); + const name = 'egg-multipart-test/' + path.basename(stream.filename); + // 文件处理,上传到云存储等等 + let result; + try { + result = await ctx.oss.put(name, stream); + } catch (err) { + // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 + await sendToWormhole(stream); + throw err; + } + + ctx.body = { + url:result.url, + // 所有表单字段都能通过 `stream.fields` 获取到 + fields:stream.fields + }; + } +} + +module.exports = UploaderController; +``` + +要通过 `ctx.getFileStream` 便捷地获取到用户上传的文件,需要满足两个条件: + +- 只支持上传一个文件。 +- 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。 + +2. 上传/接受多个文件: + +如果要获取同时上传的多个文件,不能通过 `ctx.getFileStream()` 来获取,只能通过下面这种方式: + +```js +const sendToWormhole = require('stream-wormhole'); +const Controller = require('egg').Controller; + +class UploaderController extends Controller { + async upload() { + const ctx = this.ctx; + const parts = ctx.multipart(); + let part; + // parts() 返回 promise 对象 + while ((part = await parts()) != null) { + if (part.length) { + // 这是 busboy 的字段 + console.log('field:' + part[0]); + console.log('value:' + part[1]); + console.log('valueTruncated:' + part[2]); + console.log('fieldnameTruncated:' + part[3]); + } else { + if (!part.filename) { + // 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空) + // 需要做出处理,例如给出错误提示消息 + return; + } + // part 是上传的文件流 + console.log('field:' + part.fieldname); + console.log('filename:' + part.filename); + console.log('encoding:' + part.encoding); + console.log('mime:' + part.mime); + // 文件处理,上传到云存储等等 + let result; + try { + result = await ctx.oss.put('egg-multipart-test/' + part.filename, part); + } catch (err) { + // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 + await sendToWormhole(part); + throw err; + } + console.log(result); + } + } + console.log('and we are done parsing the form!'); + } +} + +module.exports = UploaderController; +``` + +为了保证文件上传的安全,框架限制了支持的文件格式。框架默认支持的白名单如下: + +```js +// images +'.jpg', '.jpeg', // image/jpeg +'.png', // image/png,image/x-png +'.gif', // image/gif +'.bmp', // image/bmp +'.wbmp', // image/vnd.wap.wbmp +'.webp', +'.tif', +'.psd', +// text +'.svg', +'.js', '.jsx', +'.json', +'.css', '.less', +'.html', '.htm', +'.xml', +// tar +'.zip', +'.gz', '.tgz', '.gzip', +// video +'.mp3', +'.mp4', +'.avi' +``` + +用户可以通过在 `config/config.default.js` 中的配置来新增支持的文件扩展名,或者重写整个白名单。 + +- 新增支持的文件扩展名: + +```js +module.exports = { + multipart: { + fileExtensions: ['.apk'] // 增加对 '.apk' 扩展名的文件支持 + } +}; +``` + +- 覆盖整个白名单: + +```js +module.exports = { + multipart: { + whitelist: ['.png'] // 覆盖整个白名单,只允许上传 '.png' 格式 + } +}; +``` + +**注意:当重写了 whitelist 时,fileExtensions 不生效。** + +欲了解更多有关的技术细节和信息,请参阅 [Egg-Multipart](https://github.com/eggjs/egg-multipart)。 +### Header + +除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取: + +- `ctx.headers`、`ctx.header`、`ctx.request.headers`、`ctx.request.header`:这几个方法是等价的,都用于获取整个 header 对象。 +- `ctx.get(name)`、`ctx.request.get(name)`:用于获取请求 header 中的一个字段的值。如果这个字段不存在,会返回空字符串。 +- 我们建议使用 `ctx.get(name)` 而不是 `ctx.headers['name']`,因为前者会自动处理字段名大小写。 + +由于 header 的特殊性,某些字段如 `Content-Type`、`Accept` 等有明确的 HTTP 协议含义,有些如 `X-Forwarded-For` 则由反向代理设定。框架对这些字段提供了便捷的 getter,详细信息参见 [API](https://eggjs.org/api/) 文档。 + +特别地,若通过 `config.proxy = true` 设定了应用部署在反向代理(如 Nginx)之后,某些 Getter 的内部处理将发生改变。 + +#### `ctx.host` + +此 Getter 优先读取 `config.hostHeaders` 中配置的 header 值。若无法获取,则尝试读取 `host` 这个 header 的值。若仍旧获取不到,则返回空字符串。 + +`config.hostHeaders` 的默认配置为 `x-forwarded-host`。 + +#### `ctx.protocol` + +通过此 Getter 获取协议类型时,首先判断当前连接是否为加密连接(即使用 HTTPS)。若是加密连接,返回 `https`。 + +对于非加密连接,首先尝试从 `config.protocolHeaders` 中读取 header 值以判断是 HTTP 还是 HTTPS。若读不到值,则可通过 `config.protocol` 设置默认值,默认为 `http`。 + +`config.protocolHeaders` 的默认配置为 `x-forwarded-proto`。 + +#### `ctx.ips` + +通过 `ctx.ips` 获取请求经过的所有中间设备的 IP 地址列表。只在 `config.proxy = true` 时,才会从 `config.ipHeaders` 中读取 header 值。若获取不到,则为空数组。 + +`config.ipHeaders` 的默认配置为 `x-forwarded-for`。 + +#### `ctx.ip` + +`ctx.ip` 用于获取请求发起方的 IP 地址。优先从 `ctx.ips` 中获取,若 `ctx.ips` 为空,则使用连接上的 IP 地址。 + +**注:`ip` 与 `ips` 存在区别。当 `config.proxy = false` 时,`ip` 会返回当前连接发起者的 IP 地址,而 `ips` 会为空数组。** + +### Cookie + +HTTP 请求本质上是无状态的,但 Web 应用通常需要知道请求者的身份。为此,HTTP 协议设计了 Cookie([Cookie](https://en.wikipedia.org/wiki/HTTP_cookie)),允许服务端通过响应头(set-cookie)向客户端发送数据。浏览器则会将数据保存,并在下次请求同一服务时发送,以确保安全性。 + +通过 `ctx.cookies`,可在 Controller 中安全地设置和读取 Cookie。 + +```js +class CookieController extends Controller { + async add() { + const ctx = this.ctx; + let count = ctx.cookies.get('count'); + count = count ? Number(count) : 0; + ctx.cookies.set('count', ++count); + ctx.body = count; + } + + async remove() { + const ctx = this.ctx; + ctx.cookies.set('count', null); + ctx.status = 204; + } +} +``` + +Cookie 通常用于传递客户端身份信息,因此包含众多安全设置。详细用法和安全选项详见 [Cookie 文档](../core/cookie-and-session.md#cookie)。 + +#### 配置 + +Cookie 相关配置位于 `config.default.js`: + +```js +module.exports = { + cookies: { + // httpOnly: true | false, + // sameSite: 'none|lax|strict', + }, +}; +``` + +例如,配置 Cookie [SameSite](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html) 属性为 `lax`: + +```js +module.exports = { + cookies: { + sameSite: 'lax', + }, +}; +``` + +### Session + +Cookie 可以存储每个用户的 Session 来保持跨请求的用户身份。这些信息加密后存储在 Cookie 中。 + +框架内置了 [Session](https://github.com/eggjs/egg-session) 插件,通过 `ctx.session` 访问或修改用户 Session: + +```js +class PostController extends Controller { + async fetchPosts() { + const ctx = this.ctx; + // 读取 Session + const userId = ctx.session.userId; + const posts = await ctx.service.post.fetch(userId); + // 修改 Session + ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1; + ctx.body = { + success: true, + posts, + }; + } +} +``` + +Session 的操作直观:直接读取、修改或将其赋值为 `null` 删除: + +```js +class SessionController extends Controller { + async deleteSession() { + this.ctx.session = null; + } +} +``` + +Session 的安全选项和用法详见 [Session 文档](../core/cookie-and-session.md#session)。 + +#### 配置 + +Session 相关配置也位于 `config.default.js`: + +```js +module.exports = { + key: 'EGG_SESS', // Session Cookie 名称 + maxAge: 86400000, // Session 最长有效期 +}; +``` +## 参数校验 + +在获取用户请求的参数后,不可避免要进行一些校验。 + +借助 [Validate](https://github.com/eggjs/egg-validate) 插件,提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验。 + +```js +// config/plugin.js +exports.validate = { + enable: true, + package: 'egg-validate', +}; +``` + +通过 `ctx.validate(rule, [body])` 直接对参数校验: + +```js +class PostController extends Controller { + async create() { + // 校验参数 + // 如果不传第二个参数,会自动校验 `ctx.request.body` + this.ctx.validate({ + title: { type: 'string' }, + content: { type: 'string' } + }); + } +} +``` + +校验异常时,会直接抛出异常,异常状态码为 422,`errors` 字段包含了详细的验证不通过信息。想要自行处理检查异常,可以通过 `try catch` 捕获。 + +```js +class PostController extends Controller { + async create() { + const ctx = this.ctx; + try { + ctx.validate(createRule); + } catch (err) { + ctx.logger.warn(err.errors); + ctx.body = { success: false }; + return; + } + } +} +``` + +### 校验规则 + +参数校验通过 [Parameter](https://github.com/node-modules/parameter#rule) 完成,支持的校验规则在模块文档中查询。 + +#### 自定义校验规则 + +除了上一节介绍的内置校验类型,有时需自定义校验规则,可以通过 `app.validator.addRule(type, check)` 新增自定义规则。 + +```js +// app.js +app.validator.addRule('json', (rule, value) => { + try { + JSON.parse(value); + } catch (err) { + return '必须是 JSON 字符串'; + } +}); +``` + +添加完自定义规则后,可在 Controller 中用这条规则进行参数校验。 + +```js +class PostController extends Controller { + async handler() { + const ctx = this.ctx; + // query.test 字段必须是 JSON 字符串 + const rule = { test: 'json' }; + ctx.validate(rule, ctx.query); + } +} +``` + +## 调用 Service + +我们希望 Controller 中业务逻辑不太复杂,提供了 [Service](./service.md) 层,封装业务逻辑,提高代码复用性,便于测试。 + +Controller 可调用任何 Service 上的任何方法,Service 是懒加载的,只有使用时框架才实例化。 + +```js +class PostController extends Controller { + async create() { + const ctx = this.ctx; + const author = ctx.session.userId; + const req = Object.assign(ctx.request.body, { author }); + // 调用 service 处理业务 + const res = await ctx.service.post.create(req); + ctx.body = { id: res.id }; + ctx.status = 201; + } +} +``` + +Service 具体写法,查看 [Service](./service.md) 章节。 +## 发送 HTTP 响应 + +当业务逻辑完成之后,Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。 + +### 设置 status + +HTTP 设计了非常多的[状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes),每一个状态码都代表了一个特定的含义,通过设置正确的状态码,可以让响应更符合语义。 + +框架提供了一个便捷的 Setter 来进行状态码的设置。 + +```js +class PostController extends Controller { + async create() { + // 设置状态码为 201 + this.ctx.status = 201; + } +} +``` + +具体什么场景设置什么样的状态码,可以参考 [List of HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) 中各个状态码的含义。 + +### 设置 body + +绝大部分的数据都是通过 body 发送给请求方的,同请求中的 body 一样,响应中发送的 body 也需要有配套的 Content-Type 告知客户端如何对数据进行解析。 + +- 作为一个 RESTful 的 API 接口 controller 我们通常会返回 Content-Type 为 `application/json` 格式的 body,内容是一个 JSON 字符串。 +- 作为一个 html 页面的 controller 我们通常会返回 Content-Type 为 `text/html` 格式的 body,内容是 html 代码段。 + +**注意:`ctx.body` 是 `ctx.response.body` 的简写,不要与 `ctx.request.body` 混淆。** + +```js +class ViewController extends Controller { + async show() { + this.ctx.body = { + name: 'egg', + category: 'framework', + language: 'Node.js' + }; + } + + async page() { + this.ctx.body = '

    Hello

    '; + } +} +``` + +由于 Node.js 的流式特性,我们还有很多场景需要通过 Stream 返回响应,例如返回一个大文件,代理服务器直接返回上游的内容。框架也支持直接将 body 设置成一个 Stream,并会同时处理好这个 Stream 上的错误事件。 + +```js +class ProxyController extends Controller { + async proxy() { + const ctx = this.ctx; + const result = await ctx.curl(url, { + streaming: true + }); + ctx.set(result.header); + // result.res 是一个 stream + ctx.body = result.res; + } +} +``` + +#### 渲染模板 + +通常来说,我们不会手写 HTML 页面,而是通过模板引擎进行生成。框架自身没有集成任何一个模板引擎,但是约定了 [View 插件的规范](../advanced/view-plugin.md),通过接入的模板引擎,可以直接使用 `ctx.render(template)` 来渲染模板生成 html。 + +```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' }); + } +} +``` + +具体示例可以查看[模板渲染](../core/view.md)。 + +#### JSONP + +有时我们需要给非本域的页面提供接口服务,又由于一些历史原因无法通过 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) 实现,可以通过 [JSONP](https://en.wikipedia.org/wiki/JSONP) 来进行响应。 + +由于 JSONP 如果使用不当会导致非常多的安全问题,所以框架中提供了便捷的响应 JSONP 格式数据的方法,封装了 [JSONP XSS 相关的安全防范](../core/security.md#jsonp-xss),并支持进行 CSRF 校验和 referrer 校验。 + +- 通过 `app.jsonp()` 提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中,我们给需要支持 JSONP 的路由加上这个中间件: + +```js +// app/router.js +module.exports = app => { + const jsonp = app.jsonp(); + app.router.get('/api/posts/:id', jsonp, app.controller.posts.show); + app.router.get('/api/posts', jsonp, app.controller.posts.list); +}; +``` + +- 在 Controller 中,只需要按常规编写即可: + +```js +// app/controller/posts.js +class PostController extends Controller { + async show() { + this.ctx.body = { + name: 'egg', + category: 'framework', + language: 'Node.js' + }; + } +} +``` + +用户请求对应的 URL 访问到这个 controller 的时候,如果 query 中有 `_callback=fn` 参数,将会返回 JSONP 格式的数据;否则返回 JSON 格式的数据。 + +##### JSONP 配置 + +框架默认通过 query 中的 `_callback` 参数作为识别是否返回 JSONP 格式数据的依据,并且 `_callback` 中设置的方法名长度最多只允许 50 个字符。应用可以在 `config/config.default.js` 中全局覆盖默认的配置: + +```js +// config/config.default.js +exports.jsonp = { + callback: 'callback', // 识别 query 中的 `callback` 参数 + limit: 100 // 函数名最长为 100 个字符 +}; +``` + +通过上述方式配置之后,如果用户请求 `/api/posts/1?callback=fn`,响应为 JSONP 格式;如果用户请求 `/api/posts/1`,响应格式为 JSON。 + +我们同样可以在 `app.jsonp()` 创建中间件时覆盖默认的配置,以实现不同路由使用不同配置的目的: + +```js +// app/router.js +module.exports = app => { + const { router, controller, jsonp } = app; + router.get( + '/api/posts/:id', + jsonp({ callback: 'callback' }), + controller.posts.show + ); + router.get('/api/posts', jsonp({ callback: 'cb' }), controller.posts.list); +}; +``` + +##### 跨站防御配置 + +默认配置下,响应 JSONP 时不会进行任何跨站攻击的防范。在某些情况下,这是很危险的。我们初步将 JSONP 接口分为三种类型: + +1. 查询非敏感数据,例如获取一个论坛的公开文章列表。 +2. 查询敏感数据,例如获取一个用户的交易记录。 +3. 提交数据并修改数据库,例如为某一个用户创建一笔订单。 + +如果我们的 JSONP 接口提供下面两类服务,在不做任何跨站防御的情况下,可能泄露用户敏感数据甚至导致用户被钓鱼。因此框架默认为 JSONP 提供了 CSRF 校验支持和 referrer 校验支持。 + +###### CSRF + +在 JSONP 配置中,我们只需打开 `csrf: true`,即可对 JSONP 接口开启 CSRF 校验。 + +```js +// config/config.default.js +module.exports = { + jsonp: { + csrf: true + } +}; +``` + +**注意,CSRF 校验依赖于 [security](../core/security.md) 插件提供的基于 Cookie 的 CSRF 校验。** + +在开启 CSRF 校验时,客户端在发起 JSONP 请求时,也应带上 CSRF token。如果发起 JSONP 的请求方所在的页面和我们的服务在同一个主域名之下,可以读取到 Cookie 中的 CSRF token(在 CSRF token 缺失时,也可以自行设置 CSRF token 到 Cookie 中),并在请求时携带该 token。 + +##### Referrer 校验 + +如果在同一个主域之下,可以通过开启 CSRF 的方式来校验 JSONP 请求的来源。而如果想对其他域名的网页提供 JSONP 服务,我们可以通过配置 referrer 白名单的方式来限制 JSONP 的请求方在可控范围之内。 + +```javascript +// config/config.default.js +exports.jsonp = { + whiteList: /^https?:\/\/test.com\// + // whiteList: '.test.com' + // whiteList: 'sub.test.com' + // whiteList: ['sub.test.com', 'sub2.test.com'] +}; +``` + +`whiteList` 可以配置为正则表达式、字符串或者数组: + +- 正则表达式:此时只有请求的 referrer 匹配该正则时才允许访问 JSONP 接口。在设置正则表达式的时候,注意开头的 `^` 和结尾的 `\/`,保证匹配到完整的域名。 + +```javascript +exports.jsonp = { + whiteList: /^https?:\/\/test.com\// +}; +// Matches referrer: +// https://test.com/hello +// http://test.com/ +``` + +- 字符串:设置字符串形式的白名单时分为两种。当字符串以 `.` 开头,例如 `.test.com` 时,代表 referrer 白名单为 `test.com` 的所有子域名,包括 `test.com` 自身。当字符串不以 `.` 开头,例如 `sub.test.com`,则代表 referrer 白名单为 `sub.test.com` 这一个域名。(同时支持 HTTP 和 HTTPS) + +```javascript +exports.jsonp = { + whiteList: '.test.com' +}; +// Matches domain test.com: +// https://test.com/hello +// http://test.com/ + +// Matches subdomain +// https://sub.test.com/hello +// http://sub.sub.test.com/ + +exports.jsonp = { + whiteList: 'sub.test.com' +}; +// Only matches domain sub.test.com: +// https://sub.test.com/hello +// http://sub.test.com/ +``` + +- 数组:当设置的白名单为数组时,代表只要满足数组中任意一个元素的条件即可通过 referrer 校验。 + +```javascript +exports.jsonp = { + whiteList: ['sub.test.com', 'sub2.test.com'] +}; +// Matches domain sub.test.com and sub2.test.com: +// https://sub.test.com/hello +// http://sub2.test.com/ +``` + +**当 CSRF 和 referrer 校验同时开启时,请求发起方只需满足任意一个条件即可通过 JSONP 的安全校验。** + +### 设置 Header + +我们通过状态码标识请求是否成功及其状态,而响应体(body)中则设置响应的内容。通过响应头(Header),我们还可以设置一些扩展信息。 + +通过 `ctx.set(key, value)` 方法可以设置一个响应头,使用 `ctx.set(headers)` 设置多个 Header。 + +```javascript +// app/controller/api.js +class ProxyController extends Controller { + async show() { + const { ctx } = this; + const start = Date.now(); + ctx.body = await ctx.service.post.get(); + const used = Date.now() - start; + // 设置一个响应头 + ctx.set('show-response-time', used.toString()); + } +} +``` + +### 重定向 + +框架通过安全插件(security)覆盖了 Koa 原生的 `ctx.redirect` 实现,以增加重定向的安全性。 + +- `ctx.redirect(url)` 如果不在配置的白名单域名内,则禁止跳转。 +- `ctx.unsafeRedirect(url)` 不判断域名,直接跳转。不建议使用,除非已明确了解可能带来的风险。 + +如果使用 `ctx.redirect` 方法,需要在应用的配置文件中进行如下配置: + +```javascript +// config/config.default.js +exports.security = { + domainWhiteList: ['.domain.com'] // 安全白名单,以 "." 开头 +}; +``` + +如果没有配置 `domainWhiteList` 或 `domainWhiteList` 数组为空,则默认允许所有跳转请求,等同于使用 `ctx.unsafeRedirect(url)`。 \ No newline at end of file diff --git a/site/docs/basics/env.md b/site/docs/basics/env.md new file mode 100644 index 0000000000..21643182e7 --- /dev/null +++ b/site/docs/basics/env.md @@ -0,0 +1,55 @@ +--- +title: Runtime Environment +order: 3 +--- + +An web application itself should be stateless and has the ability to set its own according to the runtime environment. + +## Configure Runtime Environment + +Egg has two ways to configure runtime environment: + +1. Use `config/env` file, usually we use the build tools to generate this file, the content of this file is just an env value, such as `prod`. + +``` +// config/env +prod +``` + +2. Defining the runtime environment via `EGG_SERVER_ENV` when you start the application is more convenient, for example, use the code below to start the application in the production environment. + +```shell +EGG_SERVER_ENV=prod npm start +``` + +## Access to the Runtime Environment in Application + +Egg provides a variable `app.config.env` to represent the current runtime environment of application. + +## Configurations of Runtime Environment + +Different running environment corresponds to different configurations, read [Configuration of Config](./config.md) in detail. + +## Difference from `NODE_ENV` + +Lots of Node.js applications use `NODE_ENV` to distinguish the runtime environment, but `EGG_SERVER_ENV` distinguishes the environments much more specific. Generally speaking, there are local environment, test environment, production environment during the application development. In addition to the local development environment and the test environment, other environments are collectively referred to as the **Server Environment** and their `NODE_ENV` should be set to `production`. What's more, npm will use this variable and will not install the devDependencies when you deploy applications, so `production` should also be applied. + +Default mapping of `EGG_SERVER_ENV` and `NODE_ENV` (will generate `EGG_SERVER_ENV` from `NODE_ENV` setting if `EGG_SERVER_ENV` is not specified) + +| NODE_ENV | EGG_SERVER_ENV | remarks | +| ---------- | -------------- | ----------------------------- | +| | local | local development environment | +| test | unittest | unit test environment | +| production | prod | production environment | + +For example, `EGG_SERVER_ENV` will be set to prod when `NODE_ENV` is set as `production` and `EGG_SERVER_ENV` is not specified. + +## Environment Customization + +In normal development process, it's not limit to these environments mentioned above. So you can customize environment for your development process. + +For example, if you want to add SIT (System integration testing) to development process, you can set `EGG_SERVER_ENV` to `sit` (also recommend to set `NODE_ENV = production`), the framework will load `config/config.sit.js` when launching, and the runtime environment `app.config.env` will be `sit`. + +## Difference from Koa + +We are using `app.env` to distinguish the environments in Koa, and the default value for `app.env` is `process.env.NODE_ENV`. But in Egg (and frameworks base on Egg), we put all the configurations in `app.config`, so we should use `app.config.env` to distinguish the environments, `app.env` is no longer used. diff --git a/site/docs/basics/env.zh-CN.md b/site/docs/basics/env.zh-CN.md new file mode 100644 index 0000000000..f67b41b205 --- /dev/null +++ b/site/docs/basics/env.zh-CN.md @@ -0,0 +1,52 @@ +--- +title: 运行环境 +order: 3 +--- + +一个 Web 应用本身应该是无状态的,并拥有根据运行环境设置自身的能力。 + +## 指定运行环境 + +框架有两种方式指定运行环境: + +1. 通过 `config/env` 文件指定,该文件的内容就是运行环境,如 `prod`。一般通过构建工具来生成这个文件。 + +```plaintext +// config/env +prod +``` + +2. 通过 `EGG_SERVER_ENV` 环境变量指定运行环境更加方便,比如在生产环境启动应用: + +```shell +EGG_SERVER_ENV=prod npm start +``` + +## 应用内获取运行环境 + +框架提供了变量 `app.config.env`,来表示应用当前的运行环境。 +## 运行环境相关配置 + +不同的运行环境会对应不同的配置,具体请阅读 [Config 配置](./config.md)。 +## 与 `NODE_ENV` 的区别 + +很多 Node.js 应用会使用 `NODE_ENV` 来区分运行环境,但 `EGG_SERVER_ENV` 区分得更加精细。一般的项目开发流程包括本地开发环境、测试环境、生产环境等,除了本地开发环境和测试环境外,其他环境可统称为**服务器环境**。服务器环境的 `NODE_ENV` 应该为 `production`。而且 npm 也会使用这个变量,在应用部署时,一般不会安装 devDependencies,所以这个值也应该为 `production`。 + +框架默认支持的运行环境及映射关系(如果未指定 `EGG_SERVER_ENV`,会根据 `NODE_ENV` 来匹配)如下表所示: + +| `NODE_ENV` | `EGG_SERVER_ENV` | 说明 | +|--------------|------------------|--------------| +| (不设置) | local | 本地开发环境 | +| test | unittest | 单元测试 | +| production | prod | 生产环境 | + +例如,当 `NODE_ENV` 为 `production` 而 `EGG_SERVER_ENV` 未指定时,框架会将 `EGG_SERVER_ENV` 设置成 `prod`。 +## 自定义环境 + +Egg 框架支持开发者根据实际需要自定义开发环境。 + +假如你需要在开发流程中加入 SIT 集成测试环境,只需将环境变量 `EGG_SERVER_ENV` 设为 `sit`。同时,建议设置 `NODE_ENV` 为 `production`,这样在启动项目时,Egg 会加载 `config/config.sit.js` 配置文件,并将运行时环境的 `app.config.env` 设为 `sit`。 + +## 与 Koa 的区别 + +在 Koa 中,我们通过 `app.env` 来判断运行环境,其默认值为 `process.env.NODE_ENV`。然而在 Egg(及基于 Egg 的框架)中,配置统一放置于 `app.config`,因此需要通过 `app.config.env` 来区分环境。不再使用 `app.env`。 diff --git a/site/docs/basics/extend.md b/site/docs/basics/extend.md new file mode 100644 index 0000000000..a293605d57 --- /dev/null +++ b/site/docs/basics/extend.md @@ -0,0 +1,236 @@ +--- +title: Extend EGG +order: 11 +--- + +Egg.js is extensible and it provides multiple extension points to enhance the functionality of itself: + +- Application +- Context +- Request +- Response +- Helper + +We could use the extension APIs to help us to develop, or extend the objects given above to enhance the functionality of egg as well while programming. + +## Application + +The object `app` is just the same aspect as the global application object in Koa. There should be only one `app` in your application, and it will be created by egg when the application is started. + +### Access Method + +- `ctx.app` +- You can access the Application object by using `this.app` in Controller, Middleware, Helper, Service. For instance, `this.app.config` will help you access the Config object. +- The `app` object would be injected into the entry function as the first argument in `app.js`, like this: + + ```js + // app.js + module.exports = (app) => { + // here you can use the app object + }; + ``` + +### How to Extend + +Egg will merge the object defined in `app/extend/application.js` with the prototype of Application object in Koa, then generate object `app` which is based on the extended prototype when application is started. + +#### Extend Methods + +If we want to create a method `app.foo()`, we can do it like this: : + +```js +// app/extend/application.js +module.exports = { + foo(param) { + // `this` points to the object app, you can access other methods or property of app with this + }, +}; +``` + +#### Extend Properties + +Generally speaking, the calculation of properties only need to be done once, therefore we have to do some cache, otherwise it will degrade performance of the app as too much calculation would be going to do when accessing those properties several times. + +So, it's recommended to use Symbol + Getter. + +For example, if we would like to add a Getter property `app.bar`: + +```js +// app/extend/application.js +const BAR = Symbol('Application#bar'); + +module.exports = { + get bar() { + // `this` points to the app object, you can access other methods or property of app with this + if (!this[BAR]) { + // It should be more complex in real situation + this[BAR] = this.config.xx + this.config.yy; + } + return this[BAR]; + }, +}; +``` + +## Context + +Context means the context in Koa, which is a **Request Level** object. That is to say, every request from client will generate an Context instance. We usually write Context as `ctx` in short. In all the doc, both Context and `ctx` means the context object in Koa. + +### Access Method + +- `this` in middleware is ctx, such as `this.cookies.get('foo')`。 +- There are two different ways to write controller. If you use class to describe controller, you can use `this.ctx` to access Context. Or if you write as method, you can access Context with `ctx` directly. +- `this` in helper, service points to the helper object and service object themselves. Simply use `this.ctx` to access Context object, such as `this.ctx.cookies.get('foo')`. + +### How to extend + +Egg will merge the object defined in `app/extend/context.js` with the prototype of Context object in Koa. And it will generate a `ctx` object which is based on the extended prototype when deal with request. + +#### Extend Methods + +For instance, we could add a method `ctx.foo()` in the following way: + +```js +// app/extend/context.js +module.exports = { + foo(param) { + // `this` points to the ctx object, you can access other methods or property of ctx + }, +}; +``` + +#### Extend Properties + +Generally speaking, the calculation of properties only need to do once, therefore we have to do some cache, otherwise it will degrade performance of the app as too much calculation would be going to do when access those properties several times. + +So, it's recommended to use Symbol + Getter. + +For example, if we would like to add a Getter property `ctx.bar`: + +```js +// app/extend/context.js +const BAR = Symbol('Context#bar'); + +module.exports = { + get bar() { + // `this` points to the ctx object, you can access other methods or property of ctx + if (!this[BAR]) { + // For example, we can get from header, but it should be more complex in real situation. + this[BAR] = this.get('x-bar'); + } + return this[BAR]; + }, +}; +``` + +## Request + +Request object is the same as that in Koa, which is a **Request Level** object. It provides a great number of methods to help to access the properties and methods you need. + +### Access Method + +```js +ctx.request; +``` + +So many properties and methods in `ctx` can also be accessed in `request` object. For those properties and methods, it is just the same to access them by using either `ctx` or `request`, such as `ctx.url === ctx.request.url`. + +Here are the properties and methods in `ctx` which can also be accessed by Request aliases: [Koa - Request aliases](http://koajs.com/#request-aliases) + +### How to Extend + +Egg will merge the object defined in `app/extend/request.js` and the prototype of `request` object built in egg. And it will generate a `request` object which is based on the extended prototype when deal with request. + +For instance, we could add a property `request.foo` in the following way: + +```js +// app/extend/request.js +module.exports = { + get foo() { + return this.get('x-request-foo'); + }, +}; +``` + +## Response + +Response object is the same as that in Koa, which is a **Request Level** object. It provides a great number of methods to help to access the properties and methods you need. + +### Access Method + +```js +ctx.response; +``` + +So many properties and methods in `ctx` can also be accessed in `response` object. For those properties and methods, it is just the same to access them by using either `ctx` or `response`. For example `ctx.status = 404` is the same as `ctx.response.status = 404`. + +Here are the properties and methods in `ctx` which can also be accessed by Response aliases: [Koa Response aliases](http://koajs.com/#response-aliases) + +### How to Extend + +Egg will merge the object defined in `app/extend/response.js` and the prototype of `response` object build in egg. And it will generate a `response` object which is based on the extended prototype after dealt with request. + +For instance, we could add a setter `request.foo` in the following way: + +```js +// app/extend/response.js +module.exports = { + set foo(value) { + this.set('x-response-foo', value); + }, +}; +``` + +Then we can use the setter like this: `this.response.foo = 'bar';` + +## Helper + +Function Helper can provides some useful utility functions. + +We can put some utility functions we use ofter into helper.js as an individual function. Then we can write the complex codes in JavaScript, avoiding to write them everywhere. Besides, such a simple function like Helper allows to write test case much easier. + +Egg has had some build-in Helper functions. We can write our own Helper as well. + +### Access Method + +Access helper object with `ctx.helper`, for example: + +```js +// Assume that home router has already defined in app/router.js +app.get('home', '/', 'home.index'); + +// Use helper to calculate the specific url path +ctx.helper.pathFor('home', { by: 'recent', limit: 20 }); +// => /?by=recent&limit=20 +``` + +### How to Extend + +Egg will merge the object defined in `app/extend/helper.js` and the prototype of `helper` object build in egg. And it will generate a `helper` object which is based on the extended prototype after dealt with request. + +For instance, we could add a method `helper.foo()` in the following way: + +```js +// app/extend/helper.js +module.exports = { + foo(param) { + // // `this` points to the helper object, you can access other methods or property of helper + // this.ctx => context object + // this.app => application object + }, +}; +``` + +## Extend according to environment + +Besides, we can extend the framework in an optional way according to the environment. For example, if you want `mockXX()` only be able to accessed when doing unittest: + +```js +// app/extend/application.unittest.js +module.exports = { + mockXX(k, v) {}, +}; +``` + +This file will only be required under unittest environment. + +Similarly, we could extend egg in this way for other object,such as Application, Context, Request, Response and Helper. See more on [environment](./env.md) diff --git a/site/docs/basics/extend.zh-CN.md b/site/docs/basics/extend.zh-CN.md new file mode 100644 index 0000000000..6ba5acf684 --- /dev/null +++ b/site/docs/basics/extend.zh-CN.md @@ -0,0 +1,236 @@ +--- +title: 框架扩展 +order: 11 +--- + +框架提供了多种扩展点,以扩展自身的功能: + +- Application +- Context +- Request +- Response +- Helper + +在开发中,我们既可以使用已有的扩展 API 来方便开发,也可以对以上对象进行自定义扩展,以进一步加强框架的功能。 + +## Application + +`app` 对象指的是 Koa 的全局应用对象,全局只有一个,在应用启动时被创建。 + +### 访问方式 + +- `ctx.app` + + `ctx.app` 提供了一种访问全局 `app` 对象的方式。 + +- Controller,Middleware,Helper,Service 中都可以通过 `this.app` 访问到 Application 对象。例如,通过 `this.app.config` 可以访问配置对象。 + +- 在 `app.js` 中,`app` 对象会作为第一个参数注入到入口函数中。 + +```js +// app.js +module.exports = app => { + // 使用 app 对象 +}; +``` + +### 扩展方式 + +框架会将 `app/extend/application.js` 中定义的对象与 Koa Application 的 prototype 对象进行合并。在应用启动时会基于扩展后的 prototype 生成 `app` 对象。 + +#### 方法扩展 + +例如,我们要增加一个 `app.foo()` 方法: + +```js +// app/extend/application.js +module.exports = { + foo(param) { + // this 就是 app 对象,在其中可以调用 app 上的其他方法,或访问属性 + }, +}; +``` + +#### 属性扩展 + +通常,属性的计算只需执行一次。因此,需要实现缓存以免多次访问属性时重复计算,这会降低应用性能。 + +推荐使用 Symbol 加 Getter 的模式实现属性缓存。 + +例如,我们增加一个 `app.bar` 属性的 Getter: + +```js +// app/extend/application.js +const BAR = Symbol('Application#bar'); + +module.exports = { + get bar() { + // this 是 app 对象,在其中可以调用 app 上的其他方法,或访问属性 + if (!this[BAR]) { + // 实际情况比这更复杂 + this[BAR] = this.config.xx + this.config.yy; + } + return this[BAR]; + }, +}; +``` +## Context + +Context 指的是 Koa 的请求上下文,这是请求级别的对象,每次请求生成一个 Context 实例,通常我们也简写成 `ctx`。在所有的文档中,Context 和 `ctx` 都是指 Koa 的上下文对象。 + +### 访问方式 + +- middleware 中 `this` 就是 ctx,例如 `this.cookies.get('foo')`。 +- controller 有两种写法,类的写法通过 `this.ctx`,方法的写法直接通过 `ctx` 入参。 +- helper,service 中的 this 指向 helper,service 对象本身,使用 `this.ctx` 访问 context 对象,例如 `this.ctx.cookies.get('foo')`。 + +### 扩展方式 + +框架会将 `app/extend/context.js` 中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象。 + +#### 方法扩展 + +例如,我们要增加一个 `ctx.foo()` 方法: + +```js +// app/extend/context.js +module.exports = { + foo(param) { + // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性 + } +}; +``` + +#### 属性扩展 + +一般来说,属性的计算在同一次请求中只需要进行一次,那么一定要实现缓存,否则在同一次请求中多次访问属性时会计算多次,这样会降低应用性能。 + +推荐的方式是使用 Symbol + Getter 的模式。 + +例如,增加一个 `ctx.bar` 属性 Getter: + +```js +// app/extend/context.js +const BAR = Symbol('Context#bar'); + +module.exports = { + get bar() { + // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性 + if (!this[BAR]) { + // 例如,从 header 中获取,实际情况肯定更复杂 + this[BAR] = this.get('x-bar'); + } + return this[BAR]; + } +}; +``` +## Request 对象 + +Request 对象和 Koa 的 Request 对象相同,是 **请求级别** 的对象,它提供了大量请求相关的属性和方法供使用。 + +### 访问方式 + +```js +ctx.request; +``` + +`ctx` 上的很多属性和方法都被代理到 `request` 对象上,对于这些属性和方法使用 `ctx` 和使用 `request` 访问它们是等价的,例如 `ctx.url === ctx.request.url`。 + +Koa 内置的代理 `request` 的属性和方法列表可参阅:[Koa - Request aliases](http://koajs.com/#request-aliases)。 + +### 扩展方式 + +框架会将 `app/extend/request.js` 中定义的对象与内置 `request` 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 `request` 对象。 + +例如,增加一个 `request.foo` 属性 Getter: + +```js +// app/extend/request.js +module.exports = { + get foo() { + return this.get('x-request-foo'); + }, +}; +``` + +## Response + +Response 对象和 Koa 的 Response 对象相同,是 **请求级别** 的对象,提供了众多响应相关的属性和方法供使用。 + +### 访问方式 + +```js +ctx.response; +``` + +`ctx` 上的许多属性和方法都代理到了 `response` 对象上,因此直接通过 `ctx` 访问这些属性和方法与通过 `response` 访问是等价的。例如,`ctx.status = 404` 和 `ctx.response.status = 404` 是等价的。 + +参考 Koa 官方文档中列出的内置代理 `response` 的属性和方法列表:[Koa Response aliases](http://koajs.com/#response-aliases)。 + +### 扩展方式 + +框架会将 `app/extend/response.js` 中定义的对象与内置 `response` 的 prototype 对象合并,在处理请求时基于扩展后的 prototype 生成 `response` 对象。 + +例如,要增加一个 `response.foo` 属性的 setter: + +```js +// app/extend/response.js +module.exports = { + set foo(value) { + this.set('x-response-foo', value); + }, +}; +``` + +现在可以这样使用:`this.response.foo = 'bar';`。 + +## Helper + +Helper 函数用来提供一些实用的 utility 函数。 + +它的作用在于我们可以将一些常用的动作抽离在 `helper.js` 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。 + +框架内置了一些常用的 Helper 函数。我们也可以编写自定义的 Helper 函数。 + +### 访问方式 + +通过 `ctx.helper` 访问到 helper 对象,例如: + +```js +// 假设在 app/router.js 中定义了 home router +app.get('home', '/', 'home.index'); + +// 使用 helper 计算指定 url path +ctx.helper.pathFor('home', { by: 'recent', limit: 20 }); +// => /?by=recent&limit=20 +``` + +### 扩展方式 + +框架会把 `app/extend/helper.js` 中定义的对象与内置 `helper` 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 `helper` 对象。 + +例如,增加一个 `helper.foo()` 方法: + +```js +// app/extend/helper.js +module.exports = { + foo(param) { + // this 是 helper 对象,在其中可以调用其他 helper 方法 + // this.ctx => context 对象 + // this.app => application 对象 + }, +}; +``` + +## 按照环境进行扩展 + +在 `unittest` 环境中,你可以选择性地扩展应用程序。例如,只在 `unittest` 现场提供 `mockXX()` 方法,便于进行 mock 测试。 + +```js +// app/extend/application.unittest.js +module.exports = { + mockXX(k, v) {}, +}; +``` + +这个文件只会在 `unittest` 环境加载。同理,Application、Context、Request、Response 和 Helper 都可以使用这种方式针对某个特定环境进行扩展。更多信息,请参阅[运行环境](./env.md)。 diff --git a/site/docs/basics/index.md b/site/docs/basics/index.md new file mode 100644 index 0000000000..83d7b0b4ce --- /dev/null +++ b/site/docs/basics/index.md @@ -0,0 +1,20 @@ +--- +title: Basic +order: 0 +nav: + title: Basic + order: 4 +--- + +- [Structure](./basics/structure.md) +- [Framework Built-in Objects](./basics/objects.md) +- [Runtime Environment](./basics/env.md) +- [Configuration](./basics/config.md) +- [Middleware](./basics/middleware.md) +- [Router](./basics/router.md) +- [Controller](./basics/controller.md) +- [Service](./basics/service.md) +- [Plugin](./basics/plugin.md) +- [Scheduled Tasks](./basics/schedule.md) +- [Extend EGG](./basics/extend.md) +- [Application Startup Configuration](./basics/app-start.md) diff --git a/site/docs/basics/index.zh-CN.md b/site/docs/basics/index.zh-CN.md new file mode 100644 index 0000000000..61321d3eaf --- /dev/null +++ b/site/docs/basics/index.zh-CN.md @@ -0,0 +1,20 @@ +--- +title: 基础功能 +order: 0 +nav: + title: 基础功能 + order: 4 +--- + +- [目录结构](./basics/structure.md) +- [内置对象](./basics/objects.md) +- [运行环境](./basics/env.md) +- [配置](./basics/config.md) +- [中间件](./basics/middleware.md) +- [路由](./basics/router.md) +- [控制器](./basics/controller.md) +- [服务](./basics/service.md) +- [插件](./basics/plugin.md) +- [定时任务](./basics/schedule.md) +- [框架拓展](./basics/extend.md) +- [启动自定义](./basics/app-start.md) diff --git a/site/docs/basics/middleware.md b/site/docs/basics/middleware.md new file mode 100644 index 0000000000..ba286c521d --- /dev/null +++ b/site/docs/basics/middleware.md @@ -0,0 +1,251 @@ +--- +title: Middleware +order: 5 +--- + +In [the previous chapter](../intro/egg-and-koa.md), we say that Egg is based on Koa, so the form of middleware in Egg is the same as in Koa, i.e. they are both based on [the onion model](../intro/egg-and-koa.md#middleware). + +## Writing Middleware + +### How to Write + +Let's take a look at how to write a middleware from a simple gzip example. + +```js +const isJSON = require('koa-is-json'); +const zlib = require('zlib'); + +async function gzip(ctx, next) { + await next(); + + // convert the response body to gzip after the completion of the execution of subsequent middleware + let body = ctx.body; + if (!body) return; + if (isJSON(body)) body = JSON.stringify(body); + + // set gzip body, correct the response header + const stream = zlib.createGzip(); + stream.end(body); + ctx.body = stream; + ctx.set('Content-Encoding', 'gzip'); +} +``` + +You might find that the middleware's style in the framework is exactly the same as in Koa, yes, any middleware in Koa can be used directly by the framework. + +### Configuration + +Usually the middleware has its own configuration. In the framework, a complete middleware includes the configuration process. A middleware is a single file placed under `app/middleware` directory, which needs to exports a function that accepts two parameters: + +- options: the configuration field of the middleware, `app.config[${middlewareName}]` will be passed in by the framework +- app: the Application instance of current application + +We will do a simple optimization to the gzip middleware above, making it do gzip compression only if the body size is greater than a configured threshold. So, we need to create a new file `gzip.js` in `app/middleware` directory. + +```js +// app/middleware/gzip.js +const isJSON = require('koa-is-json'); +const zlib = require('zlib'); + +module.exports = (options) => { + return async function gzip(ctx, next) { + await next(); + + // convert the response body to gzip after the completion of the execution of subsequent middleware + let body = ctx.body; + if (!body) return; + + // support options.threshold + if (options.threshold && ctx.length < options.threshold) return; + + if (isJSON(body)) body = JSON.stringify(body); + + // set gzip body, correct the response header + const stream = zlib.createGzip(); + stream.end(body); + ctx.body = stream; + ctx.set('Content-Encoding', 'gzip'); + }; +}; +``` + +## Using Middleware + +After writing middleware, we also need to mount it, there are following ways to support: + +### Using Middleware in Application + +We can load customized middleware completely by configuration in the application, and decide their order. +If we need to load the gzip middleware in the above, +we can edit `config.default.js` like this: + +```js +module.exports = { + // configure the middleware you need, which loads in the order of array + middleware: ['gzip'], + + // configure the gzip middleware + gzip: { + threshold: 1024, // skip response body which size is less than 1K + }, +}; +``` + +This config will be merged to `app.config.appMiddleware` at starting up. + +## Using Middleware in Framework and Plugin + +Framework and Plugin don't support to configure `middleware` in `config.default.js`, you should mount it in `app.js`: + +```js +// app.js +module.exports = (app) => { + // put to the first place to count request cost + 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); + }; +}; +``` + +Middlewares which are defined at Application (`app.config.appMiddleware`) and Framework(`app.config.coreMiddleware`) will be merged to `app.middleware` by loader at staring up. + +## Using Middleware in Router + +The middleware configured in the above ways is global, and it will process every request. + +If you do want to take effect only for single route, you could just instantiate and mount it at `app/router.js`: + +```js +module.exports = (app) => { + const gzip = app.middleware.gzip({ threshold: 1024 }); + app.router.get('/needgzip', gzip, app.controller.handler); +}; +``` + +## Default Framework Middleware + +In addition to application layer loading middleware, the framework itself and other plugins will also load many middlewares. All the config fields of these built-in middlewares can be modified by modifying the ones with the same name in the config file, for example [Framework Built-in Plugin](https://github.com/eggjs/egg/tree/master/app/middleware) uses a bodyParser middleware(the framework loader will change the various delimiters in the file name into the camel style), and we can add configs below in `config/config.default.js` to modify the bodyParser: + +```js +module.exports = { + bodyParser: { + jsonLimit: '10m', + }, +}; +``` + +** Note: middleware loaded by the framework and plugins are loaded earlier than those loaded by the application layer, and the application-layer middleware cannot overwrite the default framework middleware. If the application layer loads customized middleware that has the same name with default framework middleware, an error will be reported on starting up. ** + +## Use Koa's Middleware + +Developer is free to use Koa Middleware, all middlewares used in Koa can be directly used in the framework too. + +For example, Koa uses [koa-compress](https://github.com/koajs/compress) in this way: + +```js +const koa = require('koa'); +const compress = require('koa-compress'); + +const app = koa(); + +const options = { threshold: 2048 }; +app.use(compress(options)); +``` + +We can load the middleware according to the framework specification like this: + +```js +// app/middleware/compress.js +// interfaces(`(options) => middleware`) exposed by koa-compress match the framework middleware requirements +module.exports = require('koa-compress'); +``` + +```js +// config/config.default.js +module.exports = { + middleware: ['compress'], + compress: { + threshold: 2048, + }, +}; +``` + +If the third-party Koa middleware do not follow the rule, then you can wrap it yourself: + +```js +// config/config.default.js +module.exports = { + webpack: { + compiler: {}, + others: {}, + }, +}; + +// app/middleware/webpack.js +const webpackMiddleware = require('some-koa-middleware'); + +module.exports = (options, app) => { + return webpackMiddleware(options.compiler, options.others); +}; +``` + +## General Configuration + +These general config fields are supported by middleware loaded by the application layer and built in by the framework: + +- enable: enable the middleware or not +- match: set some rules with which only the request matches can go through this middleware +- ignore: set some rules with which the request matches can't go through this middleware + +### enable + +If our application does not need default bodyParser to resolve the request body, we can configure enable for false to close it. + +```js +module.exports = { + bodyParser: { + enable: false, + }, +}; +``` + +### `match` and `ignore` + +match and ignore share the same parameter but do the opposite things. match and ignore cannot be configured in the same time. + +If we want gzip to be used only by url requests starting with `/static`, the match config field can be set like this: + +```js +module.exports = { + gzip: { + match: '/static', + }, +}; +``` + +match and ignore support various types of configuration ways: + +1. String: when string, it sets the prefix of a url path, and all urls starting with this prefix will be matched. A string array is also accepted. +2. Regular expression: when regular expression, all urls satisfy this regular expression will be matched. +3. Function: when function, the request context will be passed to it and what it returns(true/false) determines whether the request is matched or not. + +```js +module.exports = { + gzip: { + match(ctx) { + // enabled on ios devices + const reg = /iphone|ipad|ipod/i; + return reg.test(ctx.get('user-agent')); + }, + }, +}; +``` + +For more configs about `match` and `ignore`, please refer to [egg-path-matching](https://github.com/eggjs/egg-path-matching). diff --git a/site/docs/basics/middleware.zh-CN.md b/site/docs/basics/middleware.zh-CN.md new file mode 100644 index 0000000000..ac53170237 --- /dev/null +++ b/site/docs/basics/middleware.zh-CN.md @@ -0,0 +1,248 @@ +--- +title: 中间件(Middleware) +order: 5 +--- + +在[前面的章节](../intro/egg-and-koa.md)中,我们介绍了 Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于[洋葱圈模型](../intro/egg-and-koa.md#middleware)。每次我们编写一个中间件,就相当于在洋葱外面包了一层。 + +## 编写中间件 + +### 写法 + +我们首先来通过编写一个简单的 gzip 中间件,了解中间件的写法。 + +```js +// 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` 一个普通的函数,接受两个参数: + +- `options`:中间件的配置项,框架会将 `app.config[${middlewareName}]` 传递进来。 +- `app`:当前应用 `Application` 的实例。 + +下面我们对上文中的 gzip 中间件做一个简单的优化,使其支持指定只有当体积大于配置的 `threshold` 时才进行 gzip 压缩。我们在 `app/middleware` 目录下新建 `gzip.js` 文件。 + +```js +// app/middleware/gzip.js +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` 中加入下面的配置就完成了中间件的开启和配置: + +```js +module.exports = { + // 配置需要的中间件,数组顺序即为中间件的加载顺序 + middleware: ['gzip'], + + // 配置 gzip 中间件的配置 + gzip: { + threshold: 1024 // 小于 1k 的响应体不压缩 + } +}; +``` + +该配置最终将在启动时合并到 `app.config.appMiddleware`。 + +### 在框架和插件中使用中间件 + +框架和插件不支持在 `config.default.js` 中匹配 `middleware`,需要通过以下方式: + +```js +// 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` 中实例化和挂载,如下: + +```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` 中编写如下内容: + +```js +module.exports = { + bodyParser: { + jsonLimit: '10mb', + }, +}; +``` + +**注意:框架和插件加载的中间件会在应用层配置的中间件之前被加载。框架默认中间件不能被应用层中间件覆盖。如果应用层有自定义同名中间件,启动时将会报错。** +## 使用 Koa 的中间件 + +在框架里面可以非常容易地引入 Koa 中间件生态。 + +以 [`koa-compress`](https://github.com/koajs/compress) 为例,在 Koa 中使用时: + +```js +const koa = require('koa'); +const compress = require('koa-compress'); + +const app = new koa(); + +const options = { threshold: 2048 }; +app.use(compress(options)); +``` + +我们按照框架的规范来在应用中加载这个 Koa 的中间件: + +```js +// app/middleware/compress.js +// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致 +module.exports = require('koa-compress'); +``` + +```js +// config/config.default.js +module.exports = { + middleware: ['compress'], + compress: { + threshold: 2048, + }, +}; +``` + +如果使用到的 Koa 中间件不符合入参规范,则可以自行处理下: + +```js +// config/config.default.js +module.exports = { + webpack: { + compiler: {}, + others: {}, + }, +}; + +// app/middleware/webpack.js +const webpackMiddleware = require('some-koa-middleware'); + +module.exports = (options, app) => { + return webpackMiddleware(options.compiler, options.others); +}; +``` +## 通用配置 + +无论是应用层加载的中间件还是框架自带中间件,都支持几个通用的配置项: + +- `enable`:控制中间件是否开启。 +- `match`:设置只有符合某些规则的请求才会经过这个中间件。 +- `ignore`:设置符合某些规则的请求不经过这个中间件。 + +### enable + +如果我们的应用并不需要默认的 `bodyParser` 中间件来进行请求体的解析,此时我们可以通过配置 `enable` 为 `false` 来关闭它。 + +```js +module.exports = { + bodyParser: { + enable: false, + }, +}; +``` + +### match 和 ignore + +`match` 和 `ignore` 支持的参数都一样,只是作用完全相反,`match` 和 `ignore` 不允许同时配置。 + +如果我们想让 `gzip` 只针对 `/static` 前缀开头的 url 请求开启,我们可以配置 `match` 选项。 + +```js +module.exports = { + gzip: { + match: '/static', + }, +}; +``` + +`match` 和 `ignore` 支持多种类型的配置方式: + +1. 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀,所有以配置的字符串作为前缀的 url 都会匹配上。当然,你也可以直接使用字符串数组。 +2. 正则:当参数为正则时,直接匹配满足正则验证的 url 的路径。 +3. 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(`true`/`false`)来判断是否匹配。 + +```js +module.exports = { + gzip: { + match(ctx) { + // 只有 iOS 设备才开启 + const reg = /iphone|ipad|ipod/i; + return reg.test(ctx.get('user-agent')); + }, + }, +}; +``` + +有关更多的 `match` 和 `ignore` 配置情况,详见 [egg-path-matching](https://github.com/eggjs/egg-path-matching)。 diff --git a/site/docs/basics/objects.md b/site/docs/basics/objects.md new file mode 100644 index 0000000000..9478c0881e --- /dev/null +++ b/site/docs/basics/objects.md @@ -0,0 +1,322 @@ +--- +title: Framework Built-in Objects +order: 2 +--- + +At this chapter, we will introduce some built-in basic objects in the framework, including four objects (Application, Context, Request, Response) inherited from [Koa] and some objects that extended by the framework (Controller, Service , Helper, Config, Logger), we will often see them in the follow-up documents. + +## Application + +Application is a global application object, an application only instantiates one Application, it is inherited from [Koa.Application], we can mount some global methods and objects on it. We can easily [extend Application object] (./extend.md#Application) in plugin or application. + +### Events + +Framework will emits some events when server running, application developers or plugin developers can listen on these events to do some job like logging. As application developers, we can listen on these events in [app start script](./app-start.md). + +- `server`: every worker will only emit once during the runtime, after HTTP server started, framework will expose HTTP server instance by this event. +- `error`: if any exception catched by onerror plugin, it will emit an `error` event with the exception instance and current context instance(if have), developers can listen on this event to report or logging. +- `request` and `response`: application will emit `request` and `response` event when receive requests and ended responses, developers can listen on these events to generate some digest log. + +```js +// app.js + +module.exports = (app) => { + app.once('server', (server) => { + // websocket + }); + app.on('error', (err, ctx) => { + // report error + }); + app.on('request', (ctx) => { + // log receive request + }); + app.on('response', (ctx) => { + // ctx.starttime is set by framework + const used = Date.now() - ctx.starttime; + // log total cost + }); +}; +``` + +### How to Get + +Application object can be accessed almost anywhere in application, here are a few commonly used access ways: + +Almost all files (Controller, Service, Schedule, etc.) loaded by the [Loader] (../advanced/loader.md) can export a function that is called by the Loader and uses the app as a parameter: + +- [App start script](./app-start.md) + + ```js + // app.js + module.exports = (app) => { + app.cache = new Cache(); + }; + ``` + +- [Controller file](./controller.md) + + ```js + // app/controller/user.js + class UserController extends Controller { + async fetch() { + this.ctx.body = this.app.cache.get(this.ctx.query.id); + } + } + ``` + +Like the [Koa], on the Context object, we can access the Application object via `ctx.app`. Use the above Controller file as an example: + +```js +// app/controller/user.js +class UserController extends Controller { + async fetch() { + this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id); + } +} +``` + +In the instance objects that inherited from the Controller and Service base classes, the Application object can be accessed via `this.app`. + +```js +// app/controller/user.js +class UserController extends Controller { + async fetch() { + this.ctx.body = this.app.cache.get(this.ctx.query.id); + } +} +``` + +## Context + +Context is a **request level object**, inherited from [Koa.Context]. When a request is received every time, the framework instantiates a Context object that encapsulates the information requested by the user and provides a number of convenient ways to get the request parameter or set the response information. The framework will mount all of the [Service] on the Context instance, and some plugins will mount some other methods and objects on it ([egg-sequelize] will mount all the models on the Context). + +### How to Get + +The most common way to get the Context instance is in [Middleware], [Controller], and [Service]. The access method in the Controller is shown in the above example. In the Service, the access way is same as Controller. The access Context method in the Middleware of Egg is same as [Koa] framework gets the Context object in its middleware. + +The [Middleware] of Egg also supports Koa v1 and Koa v2 two different middleware coding formats. use different format, the way to access Context instance is also slightly different: + +```js +// 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); +} +``` + +In addition to the request can get the Context instance, in some non-request scenario we need to access service / model and other objects on the Context instance, we can use`Application.createAnonymousContext ()` method to create an anonymous Context instance: + +```js +// app.js +module.exports = (app) => { + app.beforeStart(async () => { + const ctx = app.createAnonymousContext(); + // preload before app start + await ctx.service.posts.load(); + }); +}; +``` + +Each task in [Schedule](./schedule.md) takes a Context instance as a parameter so that we can more easily execute some schedule business logic: + +```js +// app/schedule/refresh.js +exports.task = async (ctx) => { + await ctx.service.posts.refresh(); +}; +``` + +## Request & Response + +Request is a **request level object**, inherited from [Koa.Request]. Encapsulates the Node.js native HTTP Request object, and provides a set of helper methods to get commonly used parameters of HTTP requests. + +Response is a **request level object**, inherited from [Koa.Response]. Encapsulates the Node.js native HTTP Response object, and provides a set of helper methods to set the HTTP response. + +### How to Get + +We can get the Request(`ctx.request`) and Response (` ctx.response`) instances of the current request on the Context instance. + +```js +// app/controller/user.js +class UserController extends Controller { + async fetch() { + const { app, ctx } = this; + const id = ctx.request.query.id; + ctx.response.body = app.cache.get(id); + } +} +``` + +- [Koa] will proxy some methods and properties of Request and Response on Context, see [Koa.Context]. +- `ctx.request.query.id` and` ctx.query.id` are equivalent in the above example , `ctx.response.body =` and `ctx.body =` are equivalent. +- It should be noted that get the body of POST should use `ctx.request.body` instead of` ctx.body`. + +## Controller + +Egg provides a Controller base class and recommends that all [Controller] inherit from the base class. The Controller base class has the following properties: + +- `ctx` - [Context](#context) instance of the current request. +- `app` - [Application](#application) instance. +- `config` - application [configuration](./config.md). +- `service` - all [service](./ service.md) of application. +- `logger` - the encapsulated logger object for the current controller. + +In the Controller file, there are two ways to use the Controller base class: + +```js +// app/controller/user.js + +// get from egg (recommend) +const Controller = require('egg').Controller; +class UserController extends Controller { + // implement +} +module.exports = UserController; + +// get from app instance +module.exports = (app) => { + return class UserController extends app.Controller { + // implement + }; +}; +``` + +## Service + +Egg provides a Service base class and recommends that all [Service] inherit from the base class. + +The properties of the Service base class are the same as those of the [Controller](#controller) base class, the access method is similar: + +```js +// app/service/user.js + +// get from egg (recommend) +const Service = require('egg').Service; +class UserService extends Service { + // implement +} +module.exports = UserService; + +// get from app instance +module.exports = (app) => { + return class UserService extends app.Service { + // implement + }; +}; +``` + +## Helper + +Helper is used to provide some useful utility functions. Its role is that we can put some commonly used functions into helper.js, so we can use JavaScript to write complex logic, avoid the logic being scattered everywhere, and can be more convenient to write test cases. + +The Helper itself is a class that has the same properties as the [Controller](#controller) base class, and it will be instantiated at each request so that all functions on the Helper can also get context of the current request. + +### How to Get + +We can get the Helper(`ctx.helper`) instance of the current request on the Context instance. + +```js +// 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); + } +} +``` + +In addition, Helper instances can also be accessed in template, for example, we can get `shtml` method provided by [security](../core/security.md) plugin in template. + +``` +// app/view/home.nj +{{ helper.shtml(value) }} +``` + +### Custom helper method + +In application development, we may often customize some helper methods, such as `formatUser` in the above example, we can customize helper method with a way of [framework extension](./extend.md#helper). + +```js +// app/extend/helper.js +module.exports = { + formatUser(user) { + return only(user, ['name', 'phone']); + }, +}; +``` + +## Config + +We recommend application development to follow the principle of configuration and code separation, put hard-coded business in configuration file, and the configuration file supports different runtime environments using different configurations, it is very convenient to use it. the framework, plugin and application-level configurations are available via the Config object. For configuration of Egg, read [Configuration](./config.md) in detail. + +### How to Get + +We can get the config object from the Application instance via `app.config`, or get the config object via` this.config` on the instance of Controller, Service or Helper. + +## Logger + +Egg builds in powerful [logger](../core/logger.md), it is very convenient to print a variety of levels of logs to the corresponding log file, each logger object provides 4 level methods: + +- `logger.debug()` +- `logger.info()` +- `logger.warn()` +- `logger.error()` + +Egg provides a number of Logger object, we simply introduce how to get each Logger and its use scenario. + +### App Logger + +We can get it via `app.logger`. If we want to do some application-level logging, such as logging some data in the startup phase, logging some business informations that are unrelated to request, those can be done by App Logger. + +### App CoreLogger + +We can get it via `app.coreLogger`, and we should not print logs via CoreLogger when developing applications. the framework and plugins need to print application-level logs to make it easier to distinguish from logs printed by applications and logs printed by frameworks, the logs printed by the CoreLogger will be placed in a different file than the Logger. + +### Context Logger + +We can get it via `ctx.logger` from Context instance, we can see from access method, Context Logger must be related to the request, it will take current request related information (such as `[$userId/$ip/$traceId/${cost}ms $method $url]`) in the front of logs, with this information, we can quickly locate requests from logs and concatenate all logs in one request. + +### Context CoreLogger + +We can get it via `ctx.coreLogger`, the difference between the Context Logger is that only plugins and framework will log via it. + +### Controller Logger & Service Logger + +We can get them via `this.logger` in Controller and Service instance, they are essentially a Context Logger, but additional file path will be added to logs, easy to locate the log print location. + +## Subscription + +Subscription is a common model for subscribing, for example, the consumer in message broker or schedule, so we provide the Subscription base class to normalize this model. + +The base class of Subscription can be exported in the following way. + +```js +const Subscription = require('egg').Subscription; + +class Schedule extends Subscription { + // This method should be implemented + // subscribe can be async function or generator function + async subscribe() {} +} +``` + +We recommend plugin developers to implement based on this model, For example, [Schedule](./schedule.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 +[egg-sequelize]: https://github.com/eggjs/egg-sequelize +[middleware]: ./middleware.md +[controller]: ./controller.md +[service]: ./service.md diff --git a/site/docs/basics/objects.zh-CN.md b/site/docs/basics/objects.zh-CN.md new file mode 100644 index 0000000000..5b6ac0a4d7 --- /dev/null +++ b/site/docs/basics/objects.zh-CN.md @@ -0,0 +1,318 @@ +--- +title: 框架内置基础对象 +order: 2 +--- + +在本章中,我们将初步了解框架内部内置的一些基础对象。这些对象包括从 [Koa] 继承而来的 4 个对象(`Application`,`Context`,`Request`,`Response`)以及框架扩展的其他一些对象(`Controller`,`Service`,`Helper`,`Config`,`Logger`)。在后续的文档中,我们会经常遇到它们。 + +## Application + +Application 是全局应用对象。在一个应用中,每个进程只会实例化一个 Application 实例。它继承自 `Koa.Application`,在其上我们可以挂载一些全局的方法和对象。我们可以轻易地在插件或应用中[扩展 Application 对象](./extend.md#Application)。 + +### 事件 + +在框架运行时,会在 Application 实例上触发一些事件,应用开发者或插件开发者可以监听这些事件做一些操作。作为应用开发者,我们一般会在[启动自定义脚本](./app-start.md)中进行监听: + +- `server`:该事件在一个 worker 进程中只会触发一次,在 HTTP 服务完成启动后,会通过这个事件将 HTTP server 暴露出来给开发者。 +- `error`:运行时捕获到任何异常后,都会触发 `error` 事件,将错误对象和关联的上下文(如果有)暴露出来,开发者可以对其进行自定义的日志记录、上报等处理。 +- `request` 和 `response`:应用收到请求和响应请求时,分别会触发 `request` 和 `response` 事件,并将当前请求的上下文暴露出来,开发者可以监听这两个事件进行日志记录。 + +```js +// app.js + +module.exports = (app) => { + app.once('server', (server) => { + // websocket 相关操作 + }); + app.on('error', (err, ctx) => { + // 上报错误 + }); + app.on('request', (ctx) => { + // 记录收到的请求 + }); + app.on('response', (ctx) => { + // ctx.starttime 是由框架设置的 + const used = Date.now() - ctx.starttime; + // 记录请求总耗时 + }); +}; +``` + +### 获取方式 + +Application 对象在编写应用时几乎任何场合都能获取到,下面介绍几个常用的获取方式: + +几乎所有由框架 `Loader` 加载的文件(Controller、Service、Schedule 等),都可以通过导出一个函数来获取 Application 实例,该函数会被 `Loader` 调用,并将 app 作为参数: + +- [启动自定义脚本](./app-start.md) + + ```js + // app.js + module.exports = (app) => { + app.cache = new Cache(); + }; + ``` + +- [Controller 文件](./controller.md) + + ```js + // app/controller/user.js + class UserController extends Controller { + async fetch() { + this.ctx.body = this.app.cache.get(this.ctx.query.id); + } + } + ``` + +与 `Koa` 一样,在 Context 对象上,可以通过 `ctx.app` 访问到 Application 对象。以 Controller 文件为例: + +```js +// app/controller/user.js +class UserController extends Controller { + async fetch() { + this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id); + } +} +``` + +在继承自 Controller、Service 基类的实例中,可以通过 `this.app` 访问到 Application 对象。 + +```js +// app/controller/user.js +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 中获取 Context 的方法与 Controller 中相同,在 Middleware 中获取 Context 实例的方法则与 [Koa] 框架的中间件中使用方法一致。 + +框架的 [Middleware] 支持 Koa v1 和 Koa v2 两种中间件的写法。根据不同的写法,获取 Context 实例的方式略有区别: + +```js +// Koa v1 +function* middleware(next) { + // this 为 Context 的实例 + console.log(this.query); + yield next; +} + +// Koa v2 +async function middleware(ctx, next) { + // ctx 为 Context 的实例 + console.log(ctx.query); +} +``` + +除了在处理请求时可以获得 Context 实例外,还有一些非用户请求的场景下需要访问 service / model 等 Context 实例上的对象。这时我们可以通过 `Application.createAnonymousContext()` 方法创建一个匿名 Context 实例: + +```js +// app.js +module.exports = (app) => { + app.beforeStart(async () => { + const ctx = app.createAnonymousContext(); + // 应用启动前预加载 + await ctx.service.posts.load(); + }); +}; +``` + +在[定时任务](./schedule.md)中,每个 task 都接收一个 Context 实例作为参数,这样我们可以更便利地执行一些定时的业务逻辑: + +```js +// app/schedule/refresh.js +exports.task = async (ctx) => { + await ctx.service.posts.refresh(); +}; +``` +## Request & Response + +Request 是一个**请求级别的对象**,继承自 `[Koa.Request]`。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。 + +Response 是一个**请求级别的对象**,继承自 `[Koa.Response]`。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。 + +### 获取方式 + +可以在 Context 的实例上获取到当前请求的 Request(`ctx.request`) 和 Response(`ctx.response`) 实例。 + +```js +// app/controller/user.js +class UserController extends Controller { + async fetch() { + const { app, ctx } = this; + // 获取请求中的 `id` 参数 + const id = ctx.request.query.id; + // 设置响应体 + ctx.response.body = app.cache.get(id); + } +} +``` + +- `[Koa]` 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 `[Koa.Context]`。 +- 如上面例子中的 `ctx.request.query.id` 和 `ctx.query.id` 是等价的,`ctx.response.body =` 和 `ctx.body =` 也是等价的。 +- 需要注意的是,获取 POST 的 body 应该使用 `ctx.request.body`,而不是 `ctx.body`。 +## Controller + +框架提供了一个 Controller 基类,并推荐所有的 `Controller` 都继承于该基类实现。这个 Controller 基类有下列属性: + +- `ctx` - 当前请求的 `Context` 实例。 +- `app` - 应用的 `Application` 实例。 +- `config` - 应用的[配置](./config.md)。 +- `service` - 应用的所有 [service](./service.md)。 +- `logger` - 为当前 `Controller` 封装的 `logger` 对象。 + +在 `Controller` 文件中,可以通过两种方式来引用 Controller 基类: + +```js +// app/controller/user.js + +// 从 egg 上获取(推荐) +const Controller = require('egg').Controller; +class UserController extends Controller { + // 实现 +} +module.exports = UserController; + +// 从 app 实例上获取 +module.exports = (app) => { + return class UserController extends app.Controller { + // 实现 + }; +}; +``` +## Service + +框架提供了一个 Service 基类,并推荐所有的 Service 都继承于该基类实现。 + +Service 基类的属性和 [Controller](#controller) 基类属性一致,访问方式也类似: + +```js +// app/service/user.js + +// 从 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`)实例。 + +```js +// 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](../core/security.md) 插件提供的 `shtml` 方法。 + +```html + +{{ helper.shtml(value) }} +``` + +### 自定义 Helper 方法 + +在应用开发中,我们可能经常需要自定义一些 Helper 方法。例如上面例子中的 `formatUser`,我们可以通过 [框架扩展](./extend.md#helper) 的形式来自定义 Helper 方法。 + +```js +// app/extend/helper.js +module.exports = { + formatUser(user) { + return only(user, ['name', 'phone']); + }, +}; +``` +## Config + +我们推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中。同时,配置文件支持各个不同的运行环境使用不同的配置,使用起来也非常方便。所有框架、插件和应用级别的配置都可以通过 `Config` 对象获取到。关于框架的配置,可以详细阅读[Config 配置](./config.md)章节。 + +### 获取方式 + +我们可以通过 `app.config` 从 `Application` 实例上获取到 `config` 对象,也可以在 Controller、Service、Helper 的实例上通过 `this.config` 获取到 `config` 对象。 +## Logger + +框架内置了功能强大的[日志功能](../core/logger.md),可以非常方便地打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法: + +- `logger.debug()` +- `logger.info()` +- `logger.warn()` +- `logger.error()` + +在框架中提供了多个 Logger 对象,下面我们简单地介绍一下各个 Logger 对象的获取方式和使用场景。 + +### App Logger + +我们可以通过 `app.logger` 来获取它。如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,记录一些与请求无关的业务信息,都可以通过 App Logger 来完成。 + +### App CoreLogger + +我们可以通过 `app.coreLogger` 来获取它。一般在开发应用时,我们不应该通过 CoreLogger 打印日志。而框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰地区分应用和框架打印的日志。通过 CoreLogger 打印的日志会被放到与 Logger 不同的文件中。 + +### Context Logger + +我们可以从 Context 实例上,通过 `ctx.logger` 获取它。从访问方式上我们可以看出,Context Logger 一定是与请求相关的。它打印的日志都会在前面带上一些当前请求相关的信息(如 `[$userId/$ip/$traceId/${cost}ms $method $url]`)。通过这些信息,我们可以从日志快速定位请求,并串联一次请求中的所有日志。 + +### Context CoreLogger + +我们可以通过 `ctx.coreLogger` 获取它。它与 Context Logger 的区别在于,一般只有插件和框架会通过它来记录日志。 + +### Controller Logger 和 Service Logger + +我们可以在 Controller 和 Service 实例上,通过 `this.logger` 获取它们。它们实质上就是一个 Context Logger,不过在打印日志时,还会额外地加上文件路径,以方便定位日志的打印位置。 + +## 订阅模型 + +订阅模型是一种比较常见的开发模式,例如消息中间件的消费者或调度任务。因此,我们提供了 `Subscription` 基类来规范化这个模式。 + +你可以通过以下方式来引用 `Subscription` 基类: + +```js +const Subscription = require('egg').Subscription; + +class Schedule extends Subscription { + // 需要实现此方法 + // subscribe 可以为 async function 或 generator function + async subscribe() {} +} +``` + +插件开发者可以根据自己的需求,基于它定制订阅规范,例如定时任务就是使用这种规范实现的。 + +相关链接: +- [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) +- [egg-sequelize](https://github.com/eggjs/egg-sequelize) +- [中间件(middleware)](./middleware.md) +- [控制器(controller)](./controller.md) +- [服务(service)](./service.md) diff --git a/site/docs/basics/plugin.md b/site/docs/basics/plugin.md new file mode 100644 index 0000000000..2bb91b711f --- /dev/null +++ b/site/docs/basics/plugin.md @@ -0,0 +1,179 @@ +--- +title: Plugin +order: 9 +--- + +Plugin mechanism is a major feature of our framework. Not only can it ensure that the core of the framework is sufficiently streamlined, stable and efficient, but also can promote the reuse of business logic and the formation of an ecosystem. In the following sections, we will try to answer questions such as + +- Koa already has a middleware mechanism, why plugins? +- What are the differences/relationship between middleware and plugins? +- How do I use a plugin? +- How do I write a plugin? +- ... + +## Why plugins? + +Here are some of the issues we think that can arise when using Koa middleware: + +1. Middleware loading is sequential and it is the user's responsibility to setup the execution sequence since middleware mechanism can not manage the actual order. This, in fact, is not very friendly. When the order is not correct, it can lead to unexpected results. +2. Middleware positioning is geared towards intercepting user requests to add additional logic before or after such as: authentication, security checks, logging and so on. However, in some cases, such functionality can be unrelated to the request, for example, timing tasks, message subscriptions, back-end logic and so on. +3. Some features include very complex initialization logic that needs to be done at application startup. This is obviously not suitable for middleware to achieve. + +To sum up, we need a more powerful mechanism to manage, orchestrate those relatively independent business logic. + +### The Relationship Between Middleware, Plugins and Application + +A plugin is actually a "mini-application", almost the same as an app: + +- It contains [Services](./service.md), [middleware](./middleware.md), [config](./config.md), [framework extensions](./extend.md), etc. +- It does not have separate [Router](./router.md) and [Controller](./controller.md). +- It does not have `plugin.js`, it could only define dependencies with others, but could not deside whether other plugin is enable or not. + +Their relationship is: + +- Applications can be directly introduced into Koa's middleware. +- When it comes to the scene mentioned in the previous section, the app needs to import the plugin. +- The plugin itself can contain middleware. +- Multiple plugins can be wrapped as an [upper frame](../advanced/framework.md). + +## Using Plugins + +Plugins are usually added via the npm module: + +```bash +$ npm i egg-mysql --save +``` + +**Note: We recommend introducing dependencies in the `^` way, and locking versions are strongly discouraged.** + +```json +{ + "dependencies": { + "egg-mysql": "^ 3.0.0" + } +} +``` + +Then you need to declare it in the `config / plugin.js` application or framework: + +```js +// config / plugin.js +// Use mysql plugin +exports.mysql = { + enable: true, + package: 'egg-mysql', +}; +``` + +You can directly use the functionality provided by the plugin: + +```js +app.mysql.query(sql, values); +``` + +### Configuring Plugins + +Each configuration item in `plugin.js` supports: + +- `{Boolean} enable` - Whether to enable this plugin, the default is true +- `{String} package` -`npm` module name, plugin is imported via `npm` module +- `{String} path` - The plugin's absolute path, mutually exclusive with package configuration +- `{Array} env` - Only enable plugin in the specified runtime (environment), overriding the plugin's own configuration in `package.json` + +### Enabling/Disabling plugins + +The application does not need the package or path configuration when using the plugins built in the upper framework. You only need to specify whether they are enabled or not: + +```js +// For the built-in plugin, you can use the following simple way to turn on or off +exports.onerror = false; +``` + +### Environment Configuration + +We also support `plugin.{Env}.js`, which will load plugin configurations based on [Runtime](../basics/env.md). + +For example, if you want to load the plugin `egg-dev` only in the local environment, you can install it to `devDependencies` and adjust the plugin configuration accordingly. + +```js +// npm i egg-dev --save-dev +// package.json +{ +  "devDependencies": { +    "egg-dev": "*" +  } +} +``` + +Then declare in `plugin.local.js`: + +```js +// config / plugin.local.js +exports.dev = { + enable: true, + package: 'egg-dev', +}; +``` + +In this way, `npm i --production` in the production environment does not need to download the`egg-dev` package. + +**Note:** + +- `plugin.default.js` does not exists. Use `local` for dev environments. + +- Use this feature only in the application layer. Do not use it in the framework layer. + +### Package Name and Path + +- The `package` is introduced in the `npm` style which is the most common way to import +- `path` is an absolute path introduced when you want to load the plugin from different location such as when a plugin is still at the development stage or not available on `npm` +- To see the application of these two scenarios, please see [progressive development](../intro/progressive.md). + +```js +// config / plugin.js +const path = require('path'); +exports.mysql = { + enable: true, + path: path.join(__dirname, '../lib/plugin/egg-mysql'), +}; +``` + +## Plugin Configuration + +The plugin will usually contain its own default configuration, you can overwrite this in `config.default.js`: + +```js +// config / config.default.js +exports.mysql = { + client: { + host: 'mysql.com', + port: '3306', + user: 'test_user', + password: 'test_password', + database: 'test', + }, +}; +``` + +Specific consolidation rules can be found in [Configuration](./config.md). + +## Plugin List + +- Framework has default built-in plugins for enterprise applications [Common plugins](https://eggjs.org/zh-cn/plugins/): +   - [onerror](https://github.com/eggjs/egg-onerror) Uniform Exception Handling +   - [Session](https://github.com/eggjs/egg-session) Session implementation +   - [i18n](https://github.com/eggjs/egg-i18n) Multilingual +   - [watcher](https://github.com/eggjs/egg-watcher) File and folder monitoring +   - [multipart](https://github.com/eggjs/egg-multipart) File Streaming Upload +   - [security](https://github.com/eggjs/egg-security) Security +   - [development](https://github.com/eggjs/egg-development) Development Environment Configuration +   - [logrotator](https://github.com/eggjs/egg-logrotator) Log segmentation +   - [schedule](https://github.com/eggjs/egg-schedule) Timing tasks +   - [static](https://github.com/eggjs/egg-static) Static server +   - [jsonp](https://github.com/eggjs/egg-jsonp) jsonp support +   - [view](https://github.com/eggjs/egg-view) Template Engine +- More community plugins can be found on GitHub [egg-plugin](https://github.com/topics/egg-plugin). + +## Developing a Plugin + +See the documentation [plugin development](../advanced/plugin.md). diff --git a/site/docs/basics/plugin.zh-CN.md b/site/docs/basics/plugin.zh-CN.md new file mode 100644 index 0000000000..d2aaa8b67d --- /dev/null +++ b/site/docs/basics/plugin.zh-CN.md @@ -0,0 +1,175 @@ +--- +title: 插件 +order: 9 +--- + +插件机制是我们框架的一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。有人可能会问了: + +- Koa 已经有了中间件的机制,为什么还要插件呢? +- 中间件、插件、应用之间是什么关系,它们之间有什么区别? +- 我该如何使用一个插件? +- 如何编写一个插件? + +接下来我们就来逐一讨论。 +## 为什么要插件 + +我们在使用 Koa 中间件过程中发现了下面一些问题: + +1. 中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。 +2. 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。 +3. 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。 + +综上所述,我们需要一套更加强大的机制,来管理、编排那些相对独立的业务逻辑。 + +### 中间件、插件、应用的关系 + +一个插件其实就是一个“迷你的应用”,和应用(app)几乎一样: + +- 它包含了 [Service](./service.md)、[中间件](./middleware.md)、[配置](./config.md)、[框架扩展](./extend.md)等等。 +- 它没有独立的 [Router](./router.md) 和 [Controller](./controller.md)。 +- 它没有 `plugin.js`,只能声明和其他插件的依赖,而**不能决定**其他插件的开启与否。 + +他们的关系是: + +- 应用可以直接引入 Koa 的中间件。 +- 当遇到上一节提到的场景时,应用需引入插件。 +- 插件本身可以包含中间件。 +- 多个插件可以包装为一个[上层框架](../advanced/framework.md)。 +## 使用插件 + +插件通常通过 npm 模块的方式进行复用: + +```bash +$ npm i egg-mysql --save +``` + +**注意:我们推荐通过 `^` 的方式引入依赖,并且强烈不建议锁定版本。** + +```json +{ + "dependencies": { + "egg-mysql": "^3.0.0" + } +} +``` + +接着,需要在应用或框架的 `config/plugin.js` 中声明: + +```js +// config/plugin.js +// 使用 mysql 插件 +exports.mysql = { + enable: true, + package: 'egg-mysql', +}; +``` + +这样就可以直接使用插件提供的功能: + +```js +app.mysql.query(sql, values); +``` + +### 参数介绍 + +`plugin.js` 中的每个配置项支持: + +- `{Boolean} enable` - 是否开启此插件,默认为 true +- `{String} package` - `npm` 模块名称,通过 `npm` 模块形式引入插件 +- `{String} path` - 插件绝对路径,与 package 配置互斥 +- `{Array} env` - 只有在指定运行环境才能开启,会覆盖该插件自身 `package.json` 中的配置 + +### 开启和关闭 + +在上层框架内置的插件,应用在使用时,可以不配置 package 或者 path,只需指定 enable 即可: + +```js +// 对于内置插件,可采用以下简洁方式开启或关闭 +exports.onerror = false; +``` + +### 根据环境配置 + +同时,我们还支持 `plugin.{env}.js` 的模式,会根据[运行环境](../basics/env.md)加载插件配置。 + +比如,如果定义了一个只在开发环境使用的插件 `egg-dev`,希望只在本地环境加载,可以将其安装到 `devDependencies` 中。 + +```js +// npm i egg-dev --save-dev +// package.json +{ + "devDependencies": { + "egg-dev": "*" + } +} +``` + +接下来,在 `plugin.local.js` 中声明: + +```js +// config/plugin.local.js +exports.dev = { + enable: true, + package: 'egg-dev', +}; +``` + +这样,在生产环境下执行 `npm i --production` 时,就不需要下载 `egg-dev` 包了。 + +**注意:** + +- `plugin.default.js` 不存在 +- **只能在应用层使用,框架层请勿使用。** + +### package 和 path + +- `package` 是 `npm` 方式引入,也是最常见的引入方式 +- `path` 是绝对路径引入,例如应用内部提取了一个插件,但尚未发布至 npm;或者是应用自行改写了框架的某些插件 +- 关于这两种方式的使用场景,可参见[渐进式开发](../intro/progressive.md)文档。 + +```js +// config/plugin.js +const path = require('path'); +exports.mysql = { + enable: true, + path: path.join(__dirname, '../lib/plugin/egg-mysql'), +}; +``` +## 插件配置 + +插件一般会包含自己的默认配置。应用开发者可以在 `config.default.js` 中覆盖对应的配置: + +```js +// config/config.default.js +exports.mysql = { + client: { + host: 'mysql.com', + port: '3306', + user: 'test_user', + password: 'test_password', + database: 'test' + } +}; +``` + +具体的合并规则可以参见[配置](./config.md)。 +## 插件列表 + +- 框架默认内置了企业级应用[常用的插件](https://eggjs.org/zh-cn/plugins/): + - [onerror](https://github.com/eggjs/egg-onerror) 统一异常处理 + - [Session](https://github.com/eggjs/egg-session) Session 实现 + - [i18n](https://github.com/eggjs/egg-i18n) 多语言 + - [watcher](https://github.com/eggjs/egg-watcher) 文件和文件夹监控 + - [multipart](https://github.com/eggjs/egg-multipart) 文件流式上传 + - [security](https://github.com/eggjs/egg-security) 安全 + - [development](https://github.com/eggjs/egg-development) 开发环境配置 + - [logrotator](https://github.com/eggjs/egg-logrotator) 日志切分 + - [schedule](https://github.com/eggjs/egg-schedule) 定时任务 + - [static](https://github.com/eggjs/egg-static) 静态服务器 + - [jsonp](https://github.com/eggjs/egg-jsonp) jsonp 支持 + - [view](https://github.com/eggjs/egg-view) 模板引擎 +- 更多社区的插件可以在 GitHub 上搜索 [egg-plugin](https://github.com/topics/egg-plugin)。 + +## 如何开发一个插件 + +参见文档:[插件开发](../advanced/plugin.md)。 diff --git a/site/docs/basics/router.md b/site/docs/basics/router.md new file mode 100644 index 0000000000..1af5cedaf0 --- /dev/null +++ b/site/docs/basics/router.md @@ -0,0 +1,358 @@ +--- +title: Router +order: 6 +--- + +Router is mainly used to describe the corresponding relationship between the request URL and the Controller that processes the request eventually. All routing rules are unified in the `app/router.js` file by the framework. + +By unifying routing rules, we can avoid the routing logics scattered in many places which may cause many unknown conflicts, and we can more easily check global routing rules. + +## How to Define Router + +- Define the routing rule in `app/router.js` file + +```js +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.get('/user/:id', controller.user.info); +}; +``` + +- Implement the Controller in `app/controller` directory + +```js +// app/controller/user.js +class UserController extends Controller { + async info() { + const { ctx } = this; + ctx.body = { + name: `hello ${ctx.params.id}`, + }; + } +} +``` + +This simplest Router is done by now, when users do the request `GET /user/123`, the info function in `user.js` will be invoked. + +## Router Config in Detail + +Below is the complete definition of router, parameters can be determined depending on different scenes. + +```js +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); +``` + +The complete definition of router includes 5 major parts: + +- verb - actions that users trigger, including get, post and so on, and will be explained in detail later. + - router.head - HEAD + - router.options - OPTIONS + - router.get - GET + - router.put - PUT + - router.post - POST + - router.patch - PATCH + - router.delete - DELETE + - router.del - this is a alias method due to the reservation of delete. + - router.redirect - redirects the request URL. For example, the most common case is to redirect the request accessing the root directory to the homepage. +- router-name defines a alias for the route, and URL can be generated by helper method `pathFor` and `urlFor` provided by Helper. (Optional) +- path-match - URL path of the route. +- middleware1 - multiple Middlewares can be configured in Router. (Optional) +- controller - set the route to map to the specific controller, and the controller can be written in two types: + - `app.controller.user.fetch` - directly point to a controller + - `'user.fetch'` - simplified as a string, + +### Notices + +- multiple Middlewares can be configured to execute serially in Router definition +- Controller must be defined under `app/controller` directory +- multiple Controllers can be defined within one file, and the specific one can be specified in the form of `${fileName}.${functionName}` when defining the routing rule. +- Controller supports sub-directories, and the specific one can be specified in the form of `${directoryName}.${fileName}.${functionName}` when defining the routing rule. + +Here are some examples of writing routing rules: + +```js +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.get('/home', controller.home); + router.get('/user/:id', controller.user.page); + router.post('/admin', isAdmin, controller.admin); + router.post('/user', isLoginUser, hasAdminPermission, controller.user.create); + router.post('/api/v1/comments', controller.v1.comments.create); // app/controller/v1/comments.js +}; +``` + +### RESTful Style URL Definition + +We provide `app.router.resources('routerName', 'pathMatch', 'controller')` to generate [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) structures on a path for convenience if you prefer the RESTful style URL definition. + +```js +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.resources('posts', '/posts', controller.posts); + router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js +}; +``` + +The codes above produce a bunch of CRUD path structures for Controller `app/controller/posts.js`, and the only thing you should do next is to implement related functions in `posts.js`. + +| Method | Path | Route Name | Controller.Action | +| ------ | --------------- | ---------- | ----------------------------- | +| GET | /posts | posts | app.controllers.posts.index | +| GET | /posts/new | new_post | app.controllers.posts.new | +| GET | /posts/:id | post | app.controllers.posts.show | +| GET | /posts/:id/edit | edit_post | app.controllers.posts.edit | +| POST | /posts | posts | app.controllers.posts.create | +| PATCH | /posts/:id | post | app.controllers.posts.update | +| DELETE | /posts/:id | post | app.controllers.posts.destroy | + +```js +// app/controller/posts.js +exports.index = async () => {}; + +exports.new = async () => {}; + +exports.create = async () => {}; + +exports.show = async () => {}; + +exports.edit = async () => {}; + +exports.update = async () => {}; + +exports.destroy = async () => {}; +``` + +Methods that are not needed may not be implemented in `posts.js` and the related URL paths will not be registered to Router neither. + +## Router in Action + +More practical examples will be shown below to demonstrate how to use the router. + +#### Acquiring Parameters + +#### Via Query String + +```js +// 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 +``` + +#### Via Named Parameters + +```js +// 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 +``` + +#### Acquiring Complex Parameters + +Regular expressions, as well, can be used in routing rules to acquire parameters more flexibly: + +```js +// app/router.js +module.exports = (app) => { + app.router.get( + /^\/package\/([\w-.]+\/[\w-.]+)$/, + app.controller.package.detail, + ); +}; + +// app/controller/package.js +exports.detail = async (ctx) => { + // If the request URL is matched by the regular expression, parameters can be acquired from ctx.params according to the capture group orders. + // For the user request below, for example, the value of `ctx.params[0]` is `egg/1.0.0` + ctx.body = `package:${ctx.params[0]}`; +}; + +// curl http://127.0.0.1:7001/package/egg/1.0.0 +``` + +### Acquiring Form Contents + +```js +// 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)}`; +}; + +// simulate a post request. +// curl -X POST http://127.0.0.1:7001/form --data '{"name":"controller"}' --header 'Content-Type:application/json' +``` + +> P.S.: + +> If you perform a POST request directly, an **error** will occur: 'secret is missing'. This error message comes from [koa-csrf/index.js#L69](https://github.com/koajs/csrf/blob/2.5.0/index.js#L69). + +> **Reason**: the framework verifies the CSRF value specially for form POST requests, so please submit the CSRF key as well when you submit a form. Refer to [Keep Away from CSRF Threat](https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范) for more detail. + +> **Note**: the verification is performed because the framework builds in a security plugin [egg-security](https://github.com/eggjs/egg-security) that provides some default security practices and this plugin is enabled by default. In case you want to disable some security protections, just set the enable attribute to false. + +> "Unless you clearly confirm the consequence, it's not recommended to disable functions provided by the security plugin" + +> Here we do the config temporarily in `config/config.default.js` for an example + +``` +exports.security = { + csrf: false +}; +``` + +### Form Verification + +```js +// 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) => { + // throws exceptions if the verification fails + 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' +``` + +### Redirection + +#### Internal Redirection + +```js +// app/router.js +module.exports = (app) => { + app.router.get('index', '/home/index', app.controller.home.index); + app.redirect('/', '/home/index', 302); +}; + +// app/controller/home.js +exports.index = async (ctx) => { + ctx.body = 'hello controller'; +}; + +// curl -L http://localhost:7001 +``` + +#### External Redirection + +```js +// 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 +``` + +### Using Middleware + +A middleware can be used to change the request parameter to uppercase. +Here we just briefly explain how to use the middleware, and refer to [Middleware](./middleware.md) for detail. + +```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 +``` + +### Too Many Routing Maps? + +As described above, we do not recommend that you scatter routing logics all around, or it will bring trouble in trouble shooting. + +If there is a need for some reasons, you can split routing rules like below: + +```js +// 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); +}; +``` + +or using [egg-router-plus](https://github.com/eggjs/egg-router-plus). diff --git a/site/docs/basics/router.zh-CN.md b/site/docs/basics/router.zh-CN.md new file mode 100644 index 0000000000..7f8101a330 --- /dev/null +++ b/site/docs/basics/router.zh-CN.md @@ -0,0 +1,353 @@ +--- +title: 路由(Router) +order: 6 +--- + +Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系,框架约定了 `app/router.js` 文件用于统一所有路由规则。 + +通过统一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突。集中在一起,我们可以更方便地来查看全局的路由规则。 +## 如何定义 Router + +- `app/router.js` 里面定义 URL 路由规则 + +```js +// app/router.js +module.exports = (app) => { + const { router, controller } = app; + router.get('/user/:id', controller.user.info); +}; +``` + +- `app/controller` 目录下面实现 Controller + +```js +// 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 详细定义说明 + +下面是路由的完整定义,参数可以根据场景的不同,自由选择: + +```js +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.head`:HEAD + - `router.options`:OPTIONS + - `router.get`:GET + - `router.put`:PUT + - `router.post`:POST + - `router.patch`:PATCH + - `router.delete`:DELETE + - `router.del`:由于 delete 是一个保留字,所以提供了一个 delete 方法的别名。 + - `router.redirect`:可以对 URL 进行重定向处理,比如我们最经常使用的可以把用户访问的根目录路由到某个主页。 +- `router-name`:给路由设定一个别名,可以通过 Helper 提供的辅助函数 `pathFor` 和 `urlFor` 来生成 URL。(可选) +- `path-match`:路由 URL 路径。 +- `middlewareN`:在 Router 里面可以配置多个 Middleware。(可选) +- `controller`:指定路由映射到的具体 controller 上,controller 可以有两种写法: + - `app.controller.user.fetch`:直接指定一个具体的 controller。 + - `'user.fetch'`:可以简写为字符串形式。 + +### 注意事项 + +- 在 Router 定义中,可以支持多个 Middleware 串联执行。 +- Controller 必须定义在 `app/controller` 目录中。 +- 一个文件里面也可以包含多个 Controller 定义,在定义路由时,可以通过 `${fileName}.${functionName}` 的方式指定对应的 Controller。 +- Controller 支持子目录,在定义路由时,可以通过 `${directoryName}.${fileName}.${functionName}` 的方式指定对应的 Controller。 + +以下是一些路由定义的方式: + +```js +// app/router.js +module.exports = app => { + const { router, controller } = app; + router.get('/home', controller.home); + router.get('/user/:id', controller.user.page); + router.post('/admin', isAdmin, controller.admin); + router.post('/user', isLoginUser, hasAdminPermission, controller.user.create); + router.post('/api/v1/comments', controller.v1.comments.create); // app/controller/v1/comments.js +}; +``` + +### RESTful 风格的 URL 定义 + +如果想用 RESTful 的方式定义路由,我们提供了 `app.router.resources('routerName', 'pathMatch', controller)` 方法,快速在一个路径上生成 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 路由结构。 + +```js +// 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` 里面实现对应的函数即可。 + +| Method | Path | Route Name | Controller.Action | +| ------ | --------------- | ---------- | ----------------------------- | +| GET | /posts | posts | app.controllers.posts.index | +| GET | /posts/new | new_post | app.controllers.posts.new | +| GET | /posts/:id | post | app.controllers.posts.show | +| GET | /posts/:id/edit | edit_post | app.controllers.posts.edit | +| POST | /posts | posts | app.controllers.posts.create | +| PUT | /posts/:id | post | app.controllers.posts.update | +| DELETE | /posts/:id | post | app.controllers.posts.destroy | + +```js +// app/controller/posts.js +exports.index = async () => {}; + +exports.new = async () => {}; + +exports.create = async () => {}; + +exports.show = async () => {}; + +exports.edit = async () => {}; + +exports.update = async () => {}; + +exports.destroy = async () => {}; +``` + +如果我们不需要其中的某些方法,可以省略在 `posts.js` 里面的实现,这样对应的 URL 路径也不会注册到 Router 中。 +## router 实战 + +下面通过更多实际的例子,来说明 `router` 的用法。 + +### 参数获取 + +#### Query String 方式 + +```js +// 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 +``` + +#### 参数命名方式 + +```js +// 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 +``` + +#### 复杂参数的获取 + +路由里面也支持定义正则,可以更加灵活地获取参数: + +```js +// 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 +``` + +### 表单内容的获取 + +```js +// 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](https://github.com/koajs/csrf/blob/2.5.0/index.js#L69)。 + +> **原因**:框架内部针对表单 POST 请求均会验证 CSRF 的值,因此我们在表单提交时,需要带上 CSRF key 进行提交。具体可参考[安全威胁 CSRF 的防范](https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范)。 + +> **注意**:上述校验是因为框架中内置了安全插件 [egg-security](https://github.com/eggjs/egg-security),提供了一些默认的安全实践,并且框架的安全插件默认是开启的。如果需要关闭一些安全防范,直接设置相应选项的 `enable` 属性为 `false` 即可。 + +> 虽然不推荐,但如果确实需要关闭某些安全功能,可以在 `config/config.default.js` 中设置以下代码: + +```javascript +exports.security = { + csrf: false +}; +``` + +### 表单校验 + +```js +// 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' +``` + +### 重定向 + +#### 内部重定向 + +```js +// 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 +``` + +#### 外部重定向 + +```js +// 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 +``` + +### 中间件的使用 + +如果我们想把用户某一类请求的参数都大写,可以通过中间件来实现。 +这里我们仅简单说明如何使用中间件,更多信息请参考[中间件](./middleware.md)。 + +```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.index, + ); +}; + +// curl http://localhost:7001/search?name=egg +``` + +### 太多路由映射? + +如上所述,我们不建议在多个地方分散路由规则,这可能会导致问题排查困难。 + +如果确实存在需求,可以采用如下方法拆分: + +```js +// 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); +}; +``` + +如果需要更好的路由组织方式,也可以直接使用 [egg-router-plus](https://github.com/eggjs/egg-router-plus)。 diff --git a/site/docs/basics/schedule.md b/site/docs/basics/schedule.md new file mode 100644 index 0000000000..734f72bf9c --- /dev/null +++ b/site/docs/basics/schedule.md @@ -0,0 +1,223 @@ +--- +title: Scheduled Tasks +order: 10 +--- + +Although the HTTP Server we developed using the framework is a request-response model, there are still many scenarios that need to execute some scheduled tasks, for example: + +1. Regularly report server application status. +1. Regularly update the local cache from the remote interface. +1. Regularly split files, delete temporary files. + +The framework provides a mechanism to make the development and maintenance of scheduled tasks more elegant. + +## Develop Scheduled Tasks + +All scheduled tasks should be placed in directory `app/schedule`. Each file is an independent scheduled task that could configure the properties and the detail methods to be executed. + +A simple example, to define a scheduled task to update the remote data to the memory cache, we can create a `update_cache.js` file in the directory 'app/schedule` + +```js +const Subscription = require('egg').Subscription; + +class UpdateCache extends Subscription { + // using `schedule` property to set the scheduled task execution interval and other configurations + static get schedule() { + return { + interval: '1m', // 1 minute interval + type: 'all', // specify all `workers` need to execute + }; + } + + // `subscribe` is the function to be executed when the scheduled task is triggered + async subscribe() { + const res = await this.ctx.curl('http://www.api.com/cache', { + dataType: 'json', + }); + this.ctx.app.cache = res.data; + } +} + +module.exports = UpdateCache; +``` + +Can also be abbreviated as + +```js +module.exports = { + schedule: { + interval: '1m', // 1 minute interval + type: 'all', // specify all `workers` need to execute + }, + async task(ctx) { + const res = await ctx.curl('http://www.api.com/cache', { + dataType: 'json', + }); + ctx.app.cache = res.data; + }, +}; +``` + +This scheduled task will be executed every 1 minute on every worker process, the requested remote data will be mounted back to `app.cache`. + +### Task + +- `task` or `subscribe` is compatible with `generator function` and `async function`. +- The parameter of `task` is `ctx`, anonymous Context instance, we could call `service` and others via it. + +### Schedule Mode + +Schedule tasks can specify `interval` or `cron` two different schedule modes. + +#### `interval` + +Configure the scheduled tasks by `schedule.interval`, scheduled tasks will be executed every specified time interval. `interval` can be configured as + +- Integer type, the unit is milliseconds, e.g `5000`. +- String type, will be transformed to milliseconds by [ms](https://github.com/zeit/ms), e.g `5s`. + +```js +module.exports = { + schedule: { + // executed every 10 seconds + interval: '10s', + }, +}; +``` + +#### `cron` + +Configure the scheduled tasks by `schedule.cron`, scheduled tasks will be executed at specified timing according to the cron expressions. cron expressions are parsed by [cron-parser](https://github.com/harrisiirak/cron-parser). + +**Note: cron-parser supports second (which don't support by linux crontab).** + +```bash +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) +``` + +```js +module.exports = { + schedule: { + // executed every three hours (zero minutes and zero seconds) + cron: '0 0 */3 * * *', + }, +}; +``` + +### Type + +The framework scheduled tasks support two types by default, worker and all. Both worker and all support the above two schedule modes, except when it comes time to execute, the worker who executes the scheduled tasks is different: + +- `worker` type: only one worker per machine executes this scheduled task, the worker to execute is random. +- `all` type: each worker on each machine executes this scheduled task. + +### Other Parameters + +In addition to the parameters just introduced, scheduled task also supports these parameters: + +- `cronOptions`: configure cron time zone and so on, reference [cron-parser](https://github.com/harrisiirak/cron-parser#options) +- `immediate`: when this parameter is set to true, this scheduled task will be executed immediately after the application is started and ready. +- `disable`: when this parameter is set to true, this scheduled task will not be executed. +- `env`: env list to decide whether start this task at current env. + +### Logging + +Schedule log will be written to `${appInfo.root}/logs/{app_name}/egg-schedule.log`, but won't be logged to terminal by default, you could customize via `config.customLogger.scheduleLogger`. + +```js +// config/config.default.js +config.customLogger = { + scheduleLogger: { + // consoleLevel: 'NONE', + // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), + }, +}; +``` + +### Dynamically Configure Scheduled Tasks + +Sometimes we need to configure the parameters of scheduled tasks. Scheduled tasks support another development style: + +```js +module.exports = (app) => { + return { + schedule: { + interval: app.config.cacheTick, + type: 'all', + }, + async task(ctx) { + const res = await ctx.curl('http://www.api.com/cache', { + contentType: 'json', + }); + ctx.app.cache = res.data; + }, + }; +}; +``` + +## Manually Execute Scheduled Tasks + +We can run a scheduled task via `app.runSchedule(schedulePath)`. `app.runSchedule` reads a scheduled task file path (either a relative path in `app/schedule` or a complete absolute path), executes the corresponding scheduled task, and returns a Promise. + +There are some scenarios we may need to manually execute scheduled tasks, for example + +- Executing scheduled tasks manually for more elegant unit testing of scheduled tasks. + +```js +const mm = require('egg-mock'); +const assert = require('assert'); + +it('should schedule work fine', async () => { + const app = mm.app(); + await app.ready(); + await app.runSchedule('update_cache'); + assert(app.cache); +}); +``` + +When the application starts up, manually perform the scheduled tasks for system initialization, waiting for the initialization finished and then starting the application. See chapter [Application Startup Configuration](./app-start.md), we can implement initialization logic in `app.js`. + +```js +module.exports = (app) => { + app.beforeStart(async () => { + // ensure the data is ready before the application starts listening port + // follow-up data updates automatically by the scheduled task + await app.runSchedule('update_cache'); + }); +}; +``` + +## Extend Scheduled Task Type + +The framework scheduled tasks only support single worker execution and all worker execution, in some cases, our services are not deployed on a single machine, one of the worker processes in the cluster may execute a scheduled task. + +The framework does not provide this functionality directly, but developers can extend the new type of scheduled tasks themselves in the upper framework. + +Inherit `agent.ScheduleStrategy` in `agent.js` and register it with `agent.schedule.use()`: + +```js +module.exports = (agent) => { + class ClusterStrategy extends agent.ScheduleStrategy { + start() { + // subscribe other distributed scheduling service message, after receiving the message, allow a worker process to execute scheduled tasks + // the user configures the distributed scheduling scenario in the configuration of the scheduled task + agent.mq.subscribe(this.schedule.scene, () => this.sendOne()); + } + } + agent.schedule.use('cluster', ClusterStrategy); +}; +``` + +`ScheduleStrategy` base class provides: + +- `this.schedule` - Properties of schedule tasks, `disable` is supported by default, other configurations can be parsed by developers. +- `this.sendOne(...args)` - Notice worker to execute the task randomly, `args` will pass to `subscribe(...args)` or `task(ctx, ...args)`. +- `this.sendAll(...args)` - Notice all worker to execute the task. diff --git a/site/docs/basics/schedule.zh-CN.md b/site/docs/basics/schedule.zh-CN.md new file mode 100644 index 0000000000..40f9946bd1 --- /dev/null +++ b/site/docs/basics/schedule.zh-CN.md @@ -0,0 +1,221 @@ +--- +title: 定时任务 +order: 10 +--- + +虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务,例如: + +1. 定时上报应用状态。 +2. 定时从远程接口更新本地缓存。 +3. 定时进行文件切割、临时文件删除。 + +框架提供了一套机制来让定时任务的编写和维护更加优雅。 +## 编写定时任务 + +所有的定时任务都统一存放在 `app/schedule` 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。 + +一个简单的例子,我们定义一个更新远程数据到内存缓存的定时任务,就可以在 `app/schedule` 目录下创建一个 `update_cache.js` 文件。 + +```js +const Subscription = require('egg').Subscription; + +class UpdateCache extends Subscription { + // 通过 schedule 属性来设置定时任务的执行间隔等配置 + static get schedule() { + return { + interval: '1m', // 1 分钟间隔 + type: 'all', // 指定所有的 worker 都需要执行 + }; + } + + // subscribe 是真正定时任务执行时被运行的函数 + async subscribe() { + const res = await this.ctx.curl('http://www.api.com/cache', { + dataType: 'json', + }); + this.ctx.app.cache = res.data; + } +} + +module.exports = UpdateCache; +``` + +还可以简写为: + +```js +module.exports = { + schedule: { + interval: '1m', // 1 分钟间隔 + type: 'all', // 指定所有的 worker 都需要执行 + }, + async task(ctx) { + const res = await ctx.curl('http://www.api.com/cache', { + dataType: 'json', + }); + ctx.app.cache = res.data; + }, +}; +``` + +这个定时任务会在每一个 Worker 进程上每 1 分钟执行一次,将远程数据请求回来挂载到 `app.cache` 上。 + +### 任务 + +- `task` 或 `subscribe` 同时支持 `generator function` 和 `async function`。 +- `task` 的入参为 `ctx`,匿名的 Context 实例,可以通过它调用 `service` 等。 + +### 定时方式 + +定时任务可以指定 interval 或者 cron 两种不同的定时方式。 + +#### interval + +通过 `schedule.interval` 参数来配置定时任务的执行时机,定时任务将会每间隔指定的时间执行一次。interval 可以配置成: + +- 数字类型,单位为毫秒数,例如 `5000`。 +- 字符类型,会通过 [ms](https://github.com/zeit/ms) 转换成毫秒数,例如 `5s`。 + +```js +module.exports = { + schedule: { + // 每 10 秒执行一次 + interval: '10s', + }, +}; +``` + +#### cron + +通过 `schedule.cron` 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。cron 表达式通过 [cron-parser](https://github.com/harrisiirak/cron-parser) 进行解析。 + +**注意:cron-parser 支持可选的秒(linux crontab 不支持)。** + +```bash +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ │ +│ │ │ │ │ └─ 周日(0 - 7)(0 或 7 是周日) +│ │ │ │ └─── 月份(1 - 12) +│ │ │ └───── 日期(1 - 31) +│ │ └─────── 小时(0 - 23) +│ └───────── 分钟(0 - 59) +└─────────── 秒(0 - 59,可选) +``` + +```js +module.exports = { + schedule: { + // 每三小时准点执行一次 + cron: '0 0 */3 * * *', + }, +}; +``` + +### 类型 + +框架提供的定时任务默认支持两种类型,worker 和 all。worker 和 all 都支持上面的两种定时方式,只是当到执行时机时,会执行定时任务的 worker 不同: + +- `worker` 类型:每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。 +- `all` 类型:每台机器上的每个 worker 都会执行这个定时任务。 + +### 其他参数 + +除了上述介绍的几个参数,定时任务还支持以下参数: + +- `cronOptions`:配置 cron 的时区等,参见 [cron-parser](https://github.com/harrisiirak/cron-parser#options) 文档。 +- `immediate`:配置该参数为 true 时,这个定时任务会在应用启动并 ready 后立即执行一次这个定时任务。 +- `disable`:配置该参数为 true 时,这个定时任务不会被启动。 +- `env`:数组,仅在指定的环境下才启动该定时任务。 + +### 执行日志 + +执行日志会输出到 `${appInfo.root}/logs/{app_name}/egg-schedule.log`,默认不会输出到控制台,可以通过 `config.customLogger.scheduleLogger` 来自定义。 + +```js +// config/config.default.js +config.customLogger = { + scheduleLogger: { + // consoleLevel: 'NONE', + // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), + }, +}; +``` +### 动态配置定时任务 + +有时候,我们需要配置定时任务的参数。定时任务还可以支持另一种写法: + +```js +module.exports = (app) => { + return { + schedule: { + interval: app.config.cacheTick, + type: 'all', + }, + async task(ctx) { + const res = await ctx.curl('http://www.api.com/cache', { + contentType: 'json', + }); + ctx.app.cache = res.data; + }, + }; +}; +``` + +## 手动执行定时任务 + +我们可以通过 `app.runSchedule(schedulePath)` 来运行一个定时任务。`app.runSchedule` 接受一个定时任务文件路径(位于 `app/schedule` 目录下的相对路径或者完整的绝对路径),执行对应的定时任务,并返回一个 Promise 对象。 + +在以下场景中,我们可能需要手动执行定时任务: + +- 手动执行定时任务可以更优雅地编写定时任务的单元测试。 + +```js +const mm = require('egg-mock'); +const assert = require('assert'); + +it('should schedule work fine', async () => { + const app = mm.app(); + await app.ready(); + await app.runSchedule('update_cache'); + assert(app.cache); +}); +``` + +- 应用启动时,可以手动执行定时任务进行系统初始化。在初始化完毕后,再启动应用。具体可以参见[应用启动自定义](./app-start.md)章节。我们可以在 `app.js` 中编写初始化逻辑。 + +```js +module.exports = (app) => { + app.beforeStart(async () => { + // 保证应用启动监听端口前,数据已经准备好 + // 后续数据的更新由定时任务自动触发 + await app.runSchedule('update_cache'); + }); +}; +``` + +## 扩展定时任务类型 + +虽然默认的框架提供的定时任务只支持单个进程执行和全部进程执行,但是在某些情况下,比如服务非单机部署时,我们可能需要集群中的某一个进程执行定时任务。 + +虽然框架没有直接提供此功能,开发者可在上层框架中自行扩展新的定时任务类型。 + +在 `agent.js` 中,继承 `agent.ScheduleStrategy`,然后通过 `agent.schedule.use()` 方法注册即可: + +```js +module.exports = (agent) => { + class ClusterStrategy extends agent.ScheduleStrategy { + start() { + // 订阅其他分布式调度服务发送的消息,收到消息后让一个进程执行定时任务 + // 用户可以在定时任务的 schedule 属性中配置分布式调度的场景(scene) + agent.mq.subscribe(this.schedule.scene, () => this.sendOne()); + } + } + agent.schedule.use('cluster', ClusterStrategy); +}; +``` + +`ScheduleStrategy` 基类提供了以下方法: + +- `this.schedule` - 定时任务的属性,所有任务默认支持的 `disable` 属性,以及其他自定义配置的解析。 +- `this.sendOne(...args)` - 随机通知某个 worker 执行 task,`args` 会传递给 `subscribe(...args)` 或 `task(ctx, ...args)` 方法。 +- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 \ No newline at end of file diff --git a/site/docs/basics/service.md b/site/docs/basics/service.md new file mode 100644 index 0000000000..3a8e3af6f2 --- /dev/null +++ b/site/docs/basics/service.md @@ -0,0 +1,127 @@ +--- +title: Service +order: 8 +--- + +Simply speaking, Service is an abstract layer which is used to encapsulate business logics in complex business circumstances, and this abstraction offers advantages as below: + +- keep logics in Controller cleaner. +- keep business logics independent, since the abstracted Service can be called by many Controllers repeatedly. +- separate logics and representations, and make it easier to write test cases. Write test cases in detail referring to [here](../core/unittest.md). + +## Usage Scenario + +- Processing complex data, e.g. information to be shown need to be got from databases, and should be processed in specific rules before it can be sent and seen by the user. Or when the process is done, the database should be updated. +- Calling third party services, e.g. getting Github information etc. + +## Defining Service + +```js +// app/service/user.js +const Service = require('egg').Service; + +class UserService extends Service { + async find(uid) { + const user = await this.ctx.db.query( + 'select * from user where uid = ?', + uid, + ); + return user; + } +} + +module.exports = UserService; +``` + +### Properties + +Framework will initialize a new Service instance for every request accessing the server, and, for the example above, several attributes are attached to `this` since the Service class inherits `egg.Service`. + +- `this.ctx`: the instance of [Context](./extend.md#context) for current request, through which we can access many attributes and methods, encapsulated by the framework, of current request conveniently. +- `this.app`: the instance of [Application](./extend.md#application) for current request, through which we can access global objects and methods provided by the framework. +- `this.service`: [Service](./service.md) defined by the application, through which we can access the abstract business layer, equivalent to `this.ctx.service`. +- `this.config`: the application's run-time [config](./config.md). +- `this.logger`:logger with `debug`,`info`,`warn`,`error`, use to print different level logs, almost the same as [context logger](../core/logger.md#context-logger), but it will append Service file path for quickly track. + +### Service `ctx` in Detail + +To get the path chain of user request, the request context is injected by us during the Service initialization, so you are able to get the related information of context directly by `this.ctx` in methods. For detailed information about context, please refer to [Context](./extend.md#context). +With `ctx`, we can get various convenient attributes and methods encapsulated by the framework. For example we can use: + +- `this.ctx.curl` to make network calls. +- `this.ctx.service.otherService` to call other Services. +- `this.ctx.db` to make database calls etc, where db may be a module mounted by other plugins in advance. + +### Notes + +- Service files must be put under the `app/service` directory, and multi-level directory is supported, which can be accessed by cascading directory names. + +```js +app/service/biz/user.js => ctx.service.biz.user +app/service/sync_user.js => ctx.service.syncUser +app/service/HackerNews.js => ctx.service.hackerNews +``` + +- one Service file can only define one Class, which should be returned by `module.exports`. +- Service should be defined in the Class way, and the parent class must be `egg.Service`. +- Service is not a singleton but a **request level** object, the framework lazy-initializes it when the request `ctx.service.xx` for the first time, so the context of current request can be got from this.ctx in Service. + +## Using Service + +We begin to see how to use Service from a complete example below. + +```js +// app/router.js +module.exports = (app) => { + app.router.get('/user/:id', app.controller.user.info); +}; + +// app/controller/user.js +const Controller = require('egg').Controller; +class UserController extends Controller { + async info() { + const { ctx } = this; + const userId = ctx.params.id; + const userInfo = await ctx.service.user.find(userId); + ctx.body = userInfo; + } +} +module.exports = UserController; + +// app/service/user.js +const Service = require('egg').Service; +class UserService extends Service { + // the constructor is not a must by default + // constructor(ctx) { + // super(ctx); if some processes should be made in the constructor, this statement is a must in order to use `this.ctx` later + // // get ctx through this.ctx directly + // // get app through this.app directly too + // } + async find(uid) { + // suppose we've got user's id and are going to get detailed user information from databases + const user = await this.ctx.db.query( + 'select * from user where uid = ?', + uid, + ); + + // suppose some complex processes should be made here, and demanded informations are returned then. + const picture = await this.getPicture(uid); + + return { + name: user.user_name, + age: user.age, + picture, + }; + } + + async getPicture(uid) { + const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { + dataType: 'json', + }); + return result.data; + } +} +module.exports = UserService; + +// curl http://127.0.0.1:7001/user/1234 +``` diff --git a/site/docs/basics/service.zh-CN.md b/site/docs/basics/service.zh-CN.md new file mode 100644 index 0000000000..330852ddc7 --- /dev/null +++ b/site/docs/basics/service.zh-CN.md @@ -0,0 +1,122 @@ +--- +title: 服务(Service) +order: 8 +--- + +简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,它的提供具有以下几个优点: + +- 保持 Controller 中的逻辑更简洁。 +- 保持业务逻辑的独立性,抽象出的 Service 可以被多个 Controller 重复使用。 +- 分离逻辑和展示,这样更便于编写测试用例。具体的测试用例编写方法,可以参见[这里](../core/unittest.md)。 +## 使用场景 + +- 数据处理:当需要展示的信息须从数据库获取,并经规则计算后才能显示给用户,或计算后需更新数据库时。 +- 第三方服务调用:例如获取 GitHub 信息等。 +## 定义 Service + +```js +// app/service/user.js +const Service = require('egg').Service; + +class UserService extends Service { + async find(uid) { + const user = await this.ctx.db.query( + 'select * from user where uid = ?', + uid + ); + return user; + } +} + +module.exports = UserService; +``` + +### 属性 + +每一次用户请求,框架都会实例化对应的 Service 实例。因为它继承自 `egg.Service`,所以我们拥有以下属性便于开发: + +- `this.ctx`:当前请求的上下文 [Context](./extend.md#context) 对象实例。通过它,我们可以获取框架封装的处理当前请求的各种便捷属性和方法。 +- `this.app`:当前应用 [Application](./extend.md#application) 对象实例。通过它,我们可以访问框架提供的全局对象和方法。 +- `this.service`:应用定义的 [Service](./service.md)。通过它,我们可以访问到其他业务层,等同于 `this.ctx.service`。 +- `this.config`:应用运行时的 [配置项](./config.md)。 +- `this.logger`:logger 对象。它有四个方法(`debug`,`info`,`warn`,`error`),分别代表不同级别的日志。使用方法和效果与 [context logger](../core/logger.md#context-logger) 所述一致。但通过这个 logger 记录的日志,在日志前会加上文件路径,方便定位日志位置。 + +### Service ctx 详解 + +为了能获取用户请求的链路,在 Service 初始化时,注入了请求上下文。用户可以通过 `this.ctx` 直接获取上下文相关信息。关于上下文的更多详细解释,请参考 [Context](./extend.md#context)。有了 `ctx`,我们可以: + +- 使用 `this.ctx.curl` 发起网络调用。 +- 通过 `this.ctx.service.otherService` 调用其他 Service。 +- 调用 `this.ctx.db` 发起数据库操作,`db` 可能是插件预挂载到 app 上的模块。 + +### 注意事项 + +- Service 文件必须放在 `app/service` 目录下,支持多级目录。可以通过目录名级联访问。 + + ```js + // app/service/biz/user.js 对应到 ctx.service.biz.user + app/service/biz/user.js => ctx.service.biz.user + // app/service/sync_user.js 对应到 ctx.service.syncUser + app/service/sync_user.js => ctx.service.syncUser + // app/service/HackerNews.js 对应到 ctx.service.hackerNews + app/service/HackerNews.js => ctx.service.hackerNews + ``` + +- 一个 Service 文件仅包含一个类,该类需通过 `module.exports` 导出。 +- Service 应通过 Class 形式定义,且继承自 `egg.Service`。 +- Service 不是单例,它是请求级别的对象。框架在每次请求中初次访问 `ctx.service.xx` 时才进行实例化。因此,Service 中可以通过 `this.ctx` 获取当前请求的上下文。 +```js +// app/router.js +module.exports = app => { + app.router.get('/user/:id', app.controller.user.info); +}; + +// app/controller/user.js +const Controller = require('egg').Controller; +class UserController extends Controller { + async info() { + const { ctx } = this; + const userId = ctx.params.id; + const userInfo = await ctx.service.user.find(userId); + ctx.body = userInfo; + } +} +module.exports = UserController; + +// app/service/user.js +const Service = require('egg').Service; +class UserService extends Service { + // 默认不需要提供构造函数。 + /* constructor(ctx) { + super(ctx); // 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx` 的使用。 + // 就可以直接通过 this.ctx 获取 ctx 了 + // 还可以直接通过 this.app 获取 app 了 + } */ + async find(uid) { + // 假如我们拿到用户 id,从数据库获取用户详细信息 + const user = await this.ctx.db.query( + 'select * from user where uid = ?', + uid + ); + + // 假定这里还有一些复杂的计算,然后返回需要的信息 + const picture = await this.getPicture(uid); + + return { + name: user.user_name, + age: user.age, + picture + }; + } + + async getPicture(uid) { + const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { + dataType: 'json' + }); + return result.data; + } +} +module.exports = UserService; + +// curl http://127.0.0.1:7001/user/1234 +``` diff --git a/site/docs/basics/structure.md b/site/docs/basics/structure.md new file mode 100644 index 0000000000..49dc991ead --- /dev/null +++ b/site/docs/basics/structure.md @@ -0,0 +1,69 @@ +--- +title: Structure +order: 1 +--- + +In the [Quick Start](../intro/quickstart.md), we should have a preliminary impression on the framework, next let us simply understand the directory convention specification. + +```bash +egg-project +├── package.json +├── app.js (optional) +├── agent.js (optional) +├── app +| ├── router.js +│ ├── controller +│ | └── home.js +│ ├── service (optional) +│ | └── user.js +│ ├── middleware (optional) +│ | └── response_time.js +│ ├── schedule (optional) +│ | └── my_task.js +│ ├── public (optional) +│ | └── reset.css +│ ├── view (optional) +│ | └── home.tpl +│ └── extend (optional) +│ ├── helper.js (optional) +│ ├── request.js (optional) +│ ├── response.js (optional) +│ ├── context.js (optional) +│ ├── application.js (optional) +│ └── agent.js (optional) +├── config +| ├── plugin.js +| ├── config.default.js +│ ├── config.prod.js +| ├── config.test.js (optional) +| ├── config.local.js (optional) +| └── config.unittest.js (optional) +└── test + ├── middleware + | └── response_time.test.js + └── controller + └── home.test.js +``` + +As above, directories by conventions of framework: + +- `app/router.js` used to configure URL routing rules, see [Router](./router.md) for details. +- `app/controller/**` used to parse the input from user, return the corresponding results after processing, see [Controller](./controller.md) for details. +- `app/service/**` used for business logic layer, optional, recommend to use,see [Service](./service.md) for details. +- `app/middleware/**` uesd for middleware, optional, see [Middleware](./middleware.md) for details. +- `app/public/**` used to place static resources, optional, see built-in plugin [egg-static](https://github.com/eggjs/egg-static) for details. +- `app/extend/**` used for extensions of the framework, optional, see [Extend EGG](./extend.md) for details. +- `config/config.{env}.js` used to write configuration files, see [Configuration](./config.md) for details. +- `config/plugin.js` used to configure the plugins that need to be loaded, see [Plugin](./plugin.md) for details. +- `test/**` used for unit test, see [Unit Test](../core/unittest.md) for details. +- `app.js` and `agent.js` are used to customize the initialization works at startup, see [Application Startup Configuration](./app-start.md) for details. For the role of `agent.js` see [Agent Mechanism](../core/cluster-and-ipc.md#agent-mechanism). + +Directories by conventions of built-in plugins: + +- `app/public/**` used to place static resources, optional, see built-in plugin [egg-static](https://github.com/eggjs/egg-static) for details. +- `app/schedule/**` used for scheduled tasks, optional, see [Scheduled Task](./schedule.md) for details. + +**To customize your own directory specification, see [Loader API](../advanced/loader.md)** + +- `app/view/**` used to place view files, optional, by view plugins conventions, see [View Rendering](../core/view.md) for details. +- `app/model/**` used to place the domain model, optional, by the domain related plugins conventions, such as [egg-sequelize](https://github.com/eggjs/egg-sequelize). diff --git a/site/docs/basics/structure.zh-CN.md b/site/docs/basics/structure.zh-CN.md new file mode 100644 index 0000000000..884426b154 --- /dev/null +++ b/site/docs/basics/structure.zh-CN.md @@ -0,0 +1,70 @@ +--- +title: 目录结构 +order: 1 +--- + +在[快速入门](../intro/quickstart.md)中,大家对框架应该有了初步的印象,接下来我们简单了解下目录约定规范。 + +```bash +egg-project +├── package.json +├── app.js(可选) +├── agent.js(可选) +├── app +| ├── router.js +│ ├── controller +│ │ └── home.js +│ ├── service(可选) +│ │ └── user.js +│ ├── middleware(可选) +│ │ └── response_time.js +│ ├── schedule(可选) +│ │ └── my_task.js +│ ├── public(可选) +│ │ └── reset.css +│ ├── view(可选) +│ │ └── home.tpl +│ └── extend(可选) +│ ├── helper.js(可选) +│ ├── request.js(可选) +│ ├── response.js(可选) +│ ├── context.js(可选) +│ ├── application.js(可选) +│ └── agent.js(可选) +├── config +| ├── plugin.js +| ├── config.default.js +│ ├── config.prod.js +| ├── config.test.js(可选) +| ├── config.local.js(可选) +| └── config.unittest.js(可选) +└── test + ├── middleware + | └── response_time.test.js + └── controller + └── home.test.js +``` + +如上,由框架约定的目录: + +- `app/router.js` 用于配置 URL 路由规则,具体参见 [Router](./router.md)。 +- `app/controller/**` 用于解析用户的输入,处理后返回相应的结果,具体参见 [Controller](./controller.md)。 +- `app/service/**` 用于编写业务逻辑层,建议使用,具体参见 [Service](./service.md)。 +- `app/middleware/**` 用于编写中间件,具体参见 [Middleware](./middleware.md)。 +- `app/public/**` 用于放置静态资源,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 +- `app/extend/**` 用于框架的扩展,具体参见 [框架扩展](./extend.md)。 +- `config/config.{env}.js` 用于编写配置文件,具体参见 [配置](./config.md)。 +- `config/plugin.js` 用于配置需要加载的插件,具体参见 [插件](./plugin.md)。 +- `test/**` 用于单元测试,具体参见 [单元测试](../core/unittest.md)。 +- `app.js` 和 `agent.js` 用于自定义启动时的初始化工作,具体参见 [启动自定义](./app-start.md)。关于 `agent.js` 的作用,参见 [Agent 机制](../core/cluster-and-ipc.md#agent-机制)。 + + +由内置插件约定的目录: + +- `app/public/**` 用于放置静态资源,具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 +- `app/schedule/**` 用于定时任务,具体参见 [定时任务](./schedule.md)。 + +**若需自定义自己的目录规范,参见 [Loader API](https://eggjs.org/zh-cn/advanced/loader.html)** + +- `app/view/**` 用于放置模板文件,具体参见 [模板渲染](../core/view.md)。 +- `app/model/**` 用于放置领域模型,如 [`egg-sequelize`](https://github.com/eggjs/egg-sequelize) 等领域类相关插件。 diff --git a/site/docs/community/CONTRIBUTING.md b/site/docs/community/CONTRIBUTING.md new file mode 100644 index 0000000000..fed558a2ff --- /dev/null +++ b/site/docs/community/CONTRIBUTING.md @@ -0,0 +1,203 @@ +--- +title: Contribution Guide +--- + +If you have any comment or advice, please report your [issue](https://github.com/eggjs/egg/issues), +or make any change as you wish and submit a [PR](https://github.com/eggjs/egg/pulls). + +## Reporting New Issues + +- Please specify what kind of issue it is. +- Before you report an issue, please search for related issues. Make sure you are not going to open a duplicate issue. +- Explain your purpose clearly in tags(see **Useful Tags**), title, or content. + +Egg group members will confirm the purpose of the issue, replace more accurate tags for it, identify related milestone, and assign developers working on it. +Tags can be divided into two groups, `type` and `scope`. + +- type: What kind of issue, e.g. `feature`, `bug`, `documentation`, `performance`, `support` ... +- scope: What did you modified. Which files are modified, e.g. `core: xx`, `plugin: xx`, `deps: xx` + +### Useful Tags + +- `support`: the issue asks helps from developers of our group. If you need helps to locate and handle problems or have any idea to improve Egg, mark it as `support`. +- `bug`: if you find a problem which possiblly could be a bug, please tag it as `bug`. Then our group members will review that issue. If it is confirmed as a bug by our group member, this issue will be tagged as `confirmed`. + - A confirmed bug will be resolved prior. + - If the bug has negative impact on running online application, it will be tagged as `critical`, which refers to top priority, and will be fixed ASAP! + - A bug will be fixed from lowest necessary version, e.g. A bug needs to be fixed from 0.9.x, then this issue will be tagged as `0.9`, `0.10`, `1.0`, `1.1`, referring that the bug is required to be fixed in those versions. +- `core: xx`: the issue is related to core, e.g. `core: loader` refers that the issue is related with `loader` config. +- `plugin: xx`: the issue is related to plugins. e.g. `plugin: session` refers that the issue is related to `session` plugin. +- `deps: xx`: the issue is related to `dependencies`, e.g. `deps:egg-cors` refers that the issue is related to `egg-cors` +- `chore: documentation`: the issue is about documentation. Need to modify documentation. + +## Documentation + +All features must be submitted along with documentations. The documentations should satify several requirements. + +- Documentations must clarify one or more aspects of the feature, depending on the nature of feature: what it is, why it happens and how it works. +- It's better to include a series of procedues to explain how to fix the problem. You are also encourgaed to provide **simple, but self-explanatory** demo. + All demos should be compiled at [eggjs/examples](https://github.com/eggjs/examples) repository. +- Please provide essential urls, such as application process, terminology explainations and references. + +## Submitting Code + +### Pull Request Guide + +If you are developer of egg repo and you are willing to contribute, feel free to create a new branch, finish your modification and submit a PR. Egg group will review your work and merge it to master branch. + +```bash +# Create a new branch for development. The name of branch should be semantic, avoiding words like 'update' or 'tmp'. We suggest to use feature/xxx, if the modification is about to implement a new feature. +$ git checkout -b branch-name + +# Run the test after you finish your modification. Add new test cases or change old ones if you feel necessary +$ npm test + +# If your modification pass the tests, congradulations it's time to push your work back to us. Notice that the commit message should be wirtten in the following format. +$ git add . # git add -u to delete files +$ git commit -m "fix(role): role.use must xxx" +$ git push origin branch-name +``` + +Then you can create a Pull Request at [egg](https://github.com/eggjs/egg/pulls) + +No one can garantee how much will be remembered about certain PR after some time. To make sure we can easily recap what happened previously, please provide the following information in your PR. + +1. Need: What function you want to achieve (Generally, please point out which issue is related). +2. Updating Reason: Different with issue. Briefly describe your reason and logic about why you need to make such modification. +3. Related Testing: Briefly descirbe what part of testing is relevant to your modification. +4. User Tips: Notice for Egg users. You can skip this part, if the PR is not about update in API or potential compatibility problem. + +### Style Guide + +Eslint can help to identify styling issues that may exist in your code. Your code is required to pass the test from eslint. Run the test locally by `$ npm run lint`. + +### Commit Message Format + +You are encouraged to use [angular commit-message-format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) to write commit message. In this way, we could have a more trackable history and an automatically generated changelog. + +```xml +(): + + + +