├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── controller │ ├── antd.ts │ ├── blog.ts │ └── demo.ts ├── extend │ ├── application.ts │ └── context.ts ├── lib │ ├── condition.ts │ └── db │ │ ├── base.ts │ │ ├── blog.json │ │ ├── collection.ts │ │ ├── factory.ts │ │ ├── file.ts │ │ ├── mongo.ts │ │ └── mysql.ts ├── middleware │ ├── access.ts │ └── locals.ts ├── mocks │ └── article │ │ └── list.ts ├── model │ └── article.ts ├── router.ts ├── service │ └── article.ts ├── typings │ ├── global.d.ts │ └── module.d.ts ├── view │ ├── .gitkeep │ └── README.md └── web │ ├── asset │ ├── css │ │ └── blog.css │ └── images │ │ ├── favicon.ico │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── menu.png │ │ └── react.png │ ├── component │ ├── header │ │ ├── index.css │ │ └── index.tsx │ └── layout │ │ └── index.tsx │ ├── framework │ ├── app.tsx │ └── request.ts │ ├── page │ ├── antd │ │ ├── component │ │ │ ├── mobx │ │ │ │ ├── index.tsx │ │ │ │ └── store.tsx │ │ │ └── tab │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── store │ │ │ └── config.ts │ ├── blog │ │ ├── index.tsx │ │ ├── router │ │ │ ├── about.tsx │ │ │ ├── async.tsx │ │ │ ├── detail.tsx │ │ │ ├── example.tsx │ │ │ ├── home.css │ │ │ ├── home.tsx │ │ │ ├── index.tsx │ │ │ └── route.tsx │ │ ├── store │ │ │ ├── actions.ts │ │ │ ├── constant.ts │ │ │ ├── index.ts │ │ │ └── reducers.ts │ │ ├── view │ │ │ ├── main.css │ │ │ └── main.tsx │ │ └── widget │ │ │ ├── async-image.tsx │ │ │ └── loading.tsx │ └── demo │ │ ├── async.tsx │ │ └── node.tsx │ ├── tsconfig.json │ ├── typings │ ├── global.d.ts │ └── type.ts │ └── view │ └── layout.html ├── babel.config.js ├── config ├── config.default.ts ├── config.local.ts ├── config.prod.ts ├── config.test.ts ├── plugin.local.ts ├── plugin.ts └── tsconfig.json ├── docs ├── images │ ├── egg-webpack.png │ └── webpack.png ├── issue_template.md └── perform.md ├── package.json ├── postcss.config.js ├── tsconfig.json ├── tslint.json ├── typings ├── app │ ├── controller │ │ └── index.d.ts │ ├── extend │ │ ├── application.d.ts │ │ └── context.d.ts │ ├── index.d.ts │ ├── middleware │ │ └── index.d.ts │ ├── model │ │ └── index.d.ts │ └── service │ │ └── index.d.ts └── config │ └── plugin.d.ts ├── webpack.config.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=javascript 2 | *.css linguist-language=javascript 3 | *.html linguist-language=javascript -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .happypack/ 3 | node_modules/ 4 | npm-debug.log 5 | .idea/ 6 | dist 7 | static 8 | public/ 9 | private 10 | run 11 | *.iml 12 | artifacts.json 13 | tmp/ 14 | *tmp 15 | _site 16 | logs 17 | app/controller/**/*.js 18 | app/middleware/**/*.js 19 | app/mocks/**/*.js 20 | app/router.js 21 | config/**/*.js 22 | index.js 23 | config/buildConfig.json 24 | config/manifest.json 25 | app/view/* 26 | !app/web/**/*.js 27 | !app/view/layout.html 28 | !app/view/README.md 29 | !app/view/.gitkeep 30 | package-lock.json 31 | typings -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Egg React", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "npm", 10 | "windows": { "runtimeExecutable": "npm.cmd" }, 11 | "runtimeArgs": [ "run", "debug" ], 12 | "console": "integratedTerminal", 13 | "protocol": "auto", 14 | "restart": true, 15 | "port": 9229, 16 | "autoAttachChildProcesses": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "USE_GITIGNORE": true, 4 | "**/*.js": { 5 | "when": "$(basename).ts" 6 | } 7 | }, 8 | "path-intellisense.mappings": { 9 | "lib": "${workspaceRoot}/app/web/lib", 10 | "asset": "${workspaceRoot}/app/web/asset", 11 | "component": "${workspaceRoot}/app/web/component", 12 | "page": "${workspaceRoot}/app/web/page", 13 | "store": "${workspaceRoot}/app/web/store", 14 | }, 15 | "typescript.tsdk": "node_modules/typescript/lib" 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [5.1.0](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/4.1.5...5.1.0) (2023-04-01) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * doc link error ([ff22416](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/ff22416)) 8 | 9 | 10 | 11 | 12 | ## [4.1.5](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/3.1.0...4.1.5) (2020-05-23) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * easy init db file exist ([dded629](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/dded629)) 18 | * hot reload ([ce39d76](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/ce39d76)) 19 | * spa refresh state sync render ([f81a2d4](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/f81a2d4)) 20 | * ts props error ([919ded6](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/919ded6)) 21 | * typescript new version not support 'get' and 'set' accessors cannot declare this parameters ([7bed34e](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/7bed34e)) 22 | 23 | 24 | ### Features 25 | 26 | * add node and async data render demo ([e0cf200](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/e0cf200)) 27 | * implment full blog example ([cab9d0c](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/cab9d0c)) 28 | * mobx provider ([4377e96](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/4377e96)) 29 | * support @ import file ([949f151](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/949f151)) 30 | * upgrade babel 6 to babel 7 ([1505cc5](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/1505cc5)) 31 | * upgrade easy deps ([84d388b](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/84d388b)) 32 | * version ([20664d6](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/20664d6)) 33 | 34 | 35 | 36 | 37 | # [3.1.0](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/3.0.2...3.1.0) (2019-08-02) 38 | 39 | 40 | ### Features 41 | 42 | * mobx support ([c35cd2c](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/c35cd2c)) 43 | 44 | 45 | 46 | 47 | ## [3.0.2](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/2.4.0...3.0.2) (2019-07-25) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * autoprefixer browselist to overrideBrowserslist ([27a94da](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/27a94da)) 53 | * egg view no state componet ([cb869a4](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/cb869a4)) 54 | 55 | 56 | 57 | 58 | # [2.4.0](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/2.3.0...2.4.0) (2018-11-08) 59 | 60 | 61 | ### Features 62 | 63 | * antd theme ([2205d42](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/2205d42)) 64 | * react typescript ([65302a5](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/65302a5)) 65 | 66 | 67 | 68 | 69 | # [2.3.0](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/2.0.1...2.3.0) (2018-11-07) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * npm run dev for windows ([656fa2b](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/656fa2b)) 75 | 76 | 77 | ### Features 78 | 79 | * remove test code ([e014206](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/e014206)) 80 | * use ts-node ([d08f9ff](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/d08f9ff)) 81 | 82 | 83 | 84 | 85 | ## [2.0.1](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/2.0.0...2.0.1) (2018-03-21) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * autoprefixer ([5064f5f](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/5064f5f)) 91 | 92 | 93 | 94 | 95 | # [2.0.0](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/1.0.1...2.0.0) (2018-03-12) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * cross-env for windows ([ad63161](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/ad63161)) 101 | 102 | 103 | ### Features 104 | 105 | * webpack4 ([b0ed8c7](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/b0ed8c7)) 106 | 107 | 108 | 109 | 110 | ## [1.0.1](https://github.com/easy-team/egg-react-typescript-boilerplate/compare/482a14f...1.0.1) (2018-01-18) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * import for typescript ([482a14f](https://github.com/easy-team/egg-react-typescript-boilerplate/commit/482a14f)) 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 sky. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-react-typescript-boilerplate 2 | 3 | 基于 Egg + React + TypeScript + Webpack 服务端渲染工程骨架 4 | 5 | ## 版本 6 | 7 | - Egg 版本: ^2.x.x 8 | - Node 版本: Node ^8.x.x+, 9 | - Webpack 版本: ^4.x.x 10 | - React 版本: ^16.0.0 11 | - TypeScript: ^3.0.0 12 | 13 | ## 文档 14 | 15 | - https://easyjs.cn/egg-react 16 | - https://www.yuque.com/easy-team/egg-react 17 | 18 | 19 | ## 特性 20 | 21 | - 支持 Egg Node 端代码和前端代码 TypeScript 编写和构建 22 | 23 | - 支持 Node 和 asyncData 方式获取数据进行渲染 24 | 25 | - 支持多页面(MPA) 和 单页面(SPA) 服务端渲染(SSR)和前端渲染(CSR) 26 | 27 | - 支持 AntD 按需加载和主题配置功能以及 MobX 应用 28 | 29 | - 支持 Webpack 时时编译和热更新, `npm run dev` 一键启动应用 30 | 31 | - 支持开发环境, 测试环境,正式环境 Webpack 编译 32 | 33 | 34 | ## 依赖 35 | 36 | - [easywebpack-react](https://github.com/hubcarl/easywebpack) 37 | - [egg-view-react-ssr](https://github.com/hubcarl/egg-view-react-ssr) 38 | - [egg-webpack](https://github.com/hubcarl/egg-webpack) 39 | - [egg-webpack-react](https://github.com/hubcarl/egg-webpack-react) 40 | 41 | ## 运行 42 | 43 | #### 安装依赖 44 | 45 | ```bash 46 | npm install 47 | ``` 48 | 49 | #### 本地启动应用 50 | 51 | ```bash 52 | npm run dev 53 | ``` 54 | 55 | 应用访问: http://127.0.0.1:7001 56 | 57 | #### 构建文件 58 | 59 | - TypeScript Egg 构建 60 | 61 | ```bash 62 | npm run tsc 63 | ``` 64 | 65 | - TypeScript 前端工程构建 66 | 67 | ```bash 68 | npm run build 69 | ``` 70 | 71 | #### 打包部署 72 | 73 | 1. 先运行 `npm run tsc` 和 `npm run build` 构建 TypeScript Egg 代码和 TypeScript 前端代码 74 | 2. 项目代码和构建代码一起打包代码 75 | 3. 应用部署后,通过 `npm start` 启动应用 76 | 77 | 78 | ## 开发 79 | 80 | #### 编写前端代码 81 | 82 | >添加 `${root}/app/web/page/demo.tsx` 前端代码 83 | 84 | ```js 85 | 'use strict'; 86 | import React, { Component } from 'react'; 87 | class Demo extends Component { 88 | render() { 89 | const { title, article } = this.props; 90 | return
91 |

{title}

92 |

{article.title}

93 |
{article.content}
94 |
; 95 | } 96 | } 97 | export default Demo; 98 | ``` 99 | 100 | #### 编写 Node 代码 101 | 102 | >添加 `${root}/app/controller/demo.ts` Node 代码 103 | 104 | ```js 105 | import { Controller, Context } from 'egg'; 106 | 107 | export default class DemoController extends Controller { 108 | public async index(ctx: Context) { 109 | const title = 'Node 直接获取渲染数据'; 110 | const article = await ctx.service.article.query({ id: Number(id) }); 111 | await ctx.render('demo.js', { title, article }); 112 | } 113 | } 114 | ``` 115 | 116 | #### Egg 路由配置 117 | 118 | >添加 `${root}/app/router.ts` Egg 路由配置 119 | 120 | ```js 121 | import { Application } from 'egg'; 122 | export default (app: Application) => { 123 | const { router, controller } = app; 124 | router.get('/demo', controller.demo.index); 125 | }; 126 | ``` 127 | 128 | #### Webpack 构建配置 129 | 130 | >添加 `${root}/webpack.config.js` 新增页面 entry 配置 131 | 132 | ```js 133 | module.exports = { 134 | entry: { 135 | demo: 'app/web/page/demo.tsx', 136 | } 137 | } 138 | ``` 139 | 140 | 141 | ## License 142 | 143 | [MIT](LICENSE) 144 | -------------------------------------------------------------------------------- /app/controller/antd.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from 'egg'; 2 | export default class AntDController extends Controller { 3 | public async index() { 4 | const { ctx } = this; 5 | await ctx.render('antd.js', { 6 | title: '--Ant Design Tab--', 7 | keywords: 'react, server side render, ant design', 8 | message: { text: 'Ant Design Tab Theme and Code Spliting' } 9 | }); 10 | } 11 | } -------------------------------------------------------------------------------- /app/controller/blog.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Context } from 'egg'; 2 | import { deserialize } from '@hubcarl/json-typescript-mapper'; 3 | import Condition from '../lib/condition'; 4 | 5 | export default class BlogController extends Controller { 6 | 7 | public async home(ctx: Context) { 8 | await ctx.render('blog.js', { url: ctx.url }); 9 | } 10 | 11 | public async list(ctx: Context) { 12 | const condition = deserialize(Condition, ctx.request.body); 13 | ctx.body = await ctx.service.article.getArtilceList(condition); 14 | } 15 | 16 | public async detail(ctx: Context) { 17 | const { id } = ctx.params; 18 | const article = await ctx.service.article.query({ id: Number(id) }); 19 | ctx.body = { article }; 20 | } 21 | } -------------------------------------------------------------------------------- /app/controller/demo.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Context } from 'egg'; 2 | 3 | export default class DemoController extends Controller { 4 | 5 | public async nodeData(ctx: Context) { 6 | const { id = 1 } = ctx.params; 7 | const title = 'Node 直接获取渲染数据'; 8 | const article = await ctx.service.article.query({ id: Number(id) }); 9 | await ctx.render('demo/node.js', { title, article }); 10 | } 11 | 12 | public async asyncData(ctx: Context) { 13 | const title = 'Frontend asyncData 获取渲染数据'; 14 | await ctx.render('demo/async.js', { title }); 15 | } 16 | 17 | public async article(ctx: Context) { 18 | const article = await ctx.service.article.query({ id: 1 }); 19 | ctx.body = { article }; 20 | } 21 | } -------------------------------------------------------------------------------- /app/extend/application.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg'; 2 | import DB from '../lib/db/base'; 3 | import DBFactory from '../lib/db/factory'; 4 | const DBSymbol = Symbol('Application#db'); 5 | export default { 6 | get db(): DB { 7 | // if (!this[DBSymbol]) { 8 | // this[DBSymbol] = DBFactory(); 9 | // } 10 | // return this[DBSymbol]; 11 | return DBFactory(); 12 | } 13 | }; -------------------------------------------------------------------------------- /app/extend/context.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { Context } from 'egg'; 3 | import DB from '../lib/db/base'; 4 | export default { 5 | get db(): DB { 6 | return (this as Context).app.db; 7 | } 8 | }; -------------------------------------------------------------------------------- /app/lib/condition.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { JsonProperty } from '@hubcarl/json-typescript-mapper'; 3 | export default class Condition { 4 | @JsonProperty('title') 5 | public title?: string; 6 | @JsonProperty('categoryId') 7 | public categoryId?: number; 8 | @JsonProperty('status') 9 | public status?: number; 10 | @JsonProperty('tag') 11 | public tag?: string; 12 | @JsonProperty('pageIndex') 13 | public pageIndex: number; 14 | @JsonProperty('pageSize') 15 | public pageSize: number; 16 | public where: any = {}; 17 | public like: any = {}; 18 | public orderByField: string = 'createTime'; 19 | public orderBy: string = 'desc'; 20 | 21 | constructor() { 22 | this.title = undefined; 23 | this.categoryId = undefined; 24 | this.status = undefined; 25 | this.tag = undefined; 26 | this.pageIndex = 1; 27 | this.pageSize = 10; 28 | this.where = {}; 29 | this.like = {}; 30 | } 31 | } -------------------------------------------------------------------------------- /app/lib/db/base.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import Condition from '../condition'; 3 | import * as path from 'path'; 4 | import * as shortid from 'shortid'; 5 | import { PlainObject } from 'egg'; 6 | 7 | const dbpath = path.resolve(__dirname, 'blog.json'); 8 | 9 | export default class DB { 10 | public instance: any; 11 | public name: string; 12 | constructor(name: string = dbpath) { 13 | this.name = name; 14 | } 15 | 16 | public getUniqueId() { 17 | return shortid.generate(); 18 | } 19 | 20 | public get(collectionName: string) { 21 | return null; 22 | } 23 | 24 | public query(collectionName: string, json: PlainObject) { 25 | return null; 26 | } 27 | 28 | public add(collectionName: string, json: PlainObject) { 29 | return null; 30 | } 31 | 32 | public update(collectionName: string, where: PlainObject, json: PlainObject) { 33 | return null; 34 | } 35 | 36 | public delete(collectionName: string, field: any) { 37 | return null; 38 | } 39 | 40 | public getPager(collectionName: string, condition: Condition): any { 41 | return null; 42 | } 43 | } -------------------------------------------------------------------------------- /app/lib/db/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "article": [ 3 | { 4 | "id": 1, 5 | "space": "egg-react", 6 | "slug": "init", 7 | "url": "https://yuque.com/easy-team/egg-react/init", 8 | "title": "Egg React 服务端渲染(SSR)之快速开始", 9 | "summary": "基于 Egg + React + Webpack 服务端渲染开发指南1. 项目初始化1.1 通过 easywebpack-cli 脚手架初始化安装脚手架 npm install easywebpack-cli -g 命令行,然后就可以使用 easy 命令命令行运行 easy init选择 e...", 10 | "coverImage": null, 11 | "createTime": "2019-10-03T14:25:46.000Z", 12 | "updateTime": "2019-10-09T04:44:10.000Z", 13 | "wordCount": 1977 14 | }, 15 | { 16 | "id": 2, 17 | "space": "egg-react", 18 | "slug": "start", 19 | "url": "https://yuque.com/easy-team/egg-react/start", 20 | "title": "Egg React 服务端渲染(SSR)之从零开始", 21 | "summary": "从零开始搭建 Egg + React + Webpack 服务端渲染项目1. 初始化环境安装 Node LST (8.x.x) 环境: https://nodejs.org/zh-cn2. 初始化 egg 项目https://github.com/eggjs/egg-init/blob/mas...", 22 | "coverImage": null, 23 | "createTime": "2019-10-03T13:44:01.000Z", 24 | "updateTime": "2019-10-08T11:44:49.000Z", 25 | "wordCount": 1356 26 | }, 27 | { 28 | "id": 3, 29 | "space": "egg-react", 30 | "slug": "node", 31 | "url": "https://yuque.com/easy-team/egg-react/node", 32 | "title": "Egg React 服务端渲染 SSR(Server Side Render)", 33 | "summary": "目前 egg-view-react-ssr 支持 服务端渲染模式 和 前端渲染模式 两种渲染模式Egg + React 服务端 SSR 渲染模式这里服务端渲染指的是编写的 React 组件在 Node 服务端直接编译成完整的HTML, 然后直接输出给浏览器。MVVM 服务端渲染相比前端渲染,支...", 34 | "coverImage": null, 35 | "createTime": "2019-10-03T14:12:56.000Z", 36 | "updateTime": "2019-10-03T14:12:56.000Z", 37 | "wordCount": 524 38 | }, 39 | { 40 | "id": 4, 41 | "space": "egg-react", 42 | "slug": "web", 43 | "url": "https://yuque.com/easy-team/egg-react/web", 44 | "title": "Egg React 前端渲染 CSR(Client Side Render)", 45 | "summary": "Egg + React 客户端浏览器渲染模式调用 egg-view-react-ssr 的 renderClient 方法实现客户端浏览器渲染renderClient 表示 Node 服务端端只渲染一个包含 HTML,header,body 的一个简单 HTML 页面骨架, 具体页面内容由 R...", 46 | "coverImage": null, 47 | "createTime": "2019-10-15T12:03:53.000Z", 48 | "updateTime": "2019-10-15T12:03:53.000Z", 49 | "wordCount": 524 50 | }, 51 | { 52 | "id": 5, 53 | "space": "egg-react", 54 | "slug": "Egg React Nunjucks 静态页面渲染", 55 | "url": "https://yuque.com/easy-team/egg-react/asset", 56 | "title": "Egg React Nunjucks 前端渲染", 57 | "summary": "背景在 前端渲染模式 章节讲到了基于 React 的一体化的前端渲染模式,好处是不需要借助第三方模板引擎且无效关注静态资源注入问题,但有两个小的功能限制:layout 模板数据绑定能力较弱资源注入不能自己定义,比如 async, crossorigin 等配置针对上面问题 egg-view-r...", 58 | "coverImage": null, 59 | "createTime": "2018-12-17T06:37:54.000Z", 60 | "updateTime": "2019-06-03T07:09:18.000Z", 61 | "wordCount": 779 62 | }, 63 | { 64 | "id": 6, 65 | "space": "egg-react", 66 | "slug": "html", 67 | "url": "https://yuque.com/easy-team/egg-react/html", 68 | "title": "Egg React HTML 静态页面渲前端渲染", 69 | "summary": "背景在 前端渲染模式 和 asset 渲染模式 章节讲到了基于 React 的前端渲染模式,但都依赖 egg-view-react-ssr 插件,那如何基于已有 egg 模板引擎 (egg-view-nunjucks 或 egg-view-ejs) + Webpack 完全自定义前端方案呢?...", 70 | "coverImage": null, 71 | "createTime": "2019-10-08T07:11:39.000Z", 72 | "updateTime": "2019-10-08T07:11:39.000Z", 73 | "wordCount": 652 74 | }, 75 | { 76 | "id": 7, 77 | "space": "egg-react", 78 | "slug": "config", 79 | "url": "https://yuque.com/easy-team/egg-react/config", 80 | "title": "Egg React 服务端渲染(SSR) Webpack 配置说明", 81 | "summary": "Webpack 标准配置// ${root}/webpack.config.js module.exports = { entry: { app: 'app/web/page/app/index.js', // js 文件需要自己实现 react.render 逻辑 he...", 82 | "coverImage": null, 83 | "createTime": "2019-10-03T15:05:12.000Z", 84 | "updateTime": "2019-10-03T15:05:12.000Z", 85 | "wordCount": 346 86 | }, 87 | { 88 | "id": 8, 89 | "space": "egg-react", 90 | "slug": "build", 91 | "url": "https://yuque.com/easy-team/egg-react/build", 92 | "title": "Egg React 服务端渲染(SSR) Webpack 构建流程", 93 | "summary": "基于Egg+React+Webpack构建流程1. 本地Egg项目启动首先执行node index.js 或者 npm run dev 启动 Egg应用在 Egg Agent 里面启动koa服务, 同时在koa服务里面启动Webpack编译服务挂载Webpack内存文件读取方法覆盖本地文件读取...", 94 | "coverImage": null, 95 | "createTime": "2018-12-14T09:56:06.000Z", 96 | "updateTime": "2019-06-05T07:33:41.000Z", 97 | "wordCount": 1120 98 | }, 99 | { 100 | "id": 9, 101 | "space": "egg-react", 102 | "slug": "online", 103 | "url": "https://yuque.com/easy-team/egg-react/online", 104 | "title": "Egg React 项目部署流程", 105 | "summary": "开发部署新项目开发在 egg-react-webpack-boilerplate 骨架项目中, 提供了一些demo, 如果要进行新项目开发,可以删除部分文件:app/web/page 是页面目录。下面的每个目录都是一个单独的页面,其中 spa 目录是一个单页面服务端渲染例子,其他是简单的 Re...", 106 | "coverImage": null, 107 | "createTime": "2018-11-26T09:46:50.000Z", 108 | "updateTime": "2019-05-23T07:04:10.000Z", 109 | "wordCount": 1179 110 | }, 111 | { 112 | "id": 10, 113 | "space": "egg-react", 114 | "slug": "seo", 115 | "url": "https://yuque.com/easy-team/egg-react/seo", 116 | "title": "Egg React 服务端渲染(SSR) SEO 实现", 117 | "summary": "Egg + React SSR SEO 实现MVVM 服务端渲染相比前端渲染,支持 SEO,更快的首屏渲染,相比传统的模板引擎,更好的组件化,前后端模板共用。在 Egg + React 的方案里面, HTML head 里面 meta 信息也作为 React 服务端渲染的一部分, 和普通的数据...", 118 | "coverImage": null, 119 | "createTime": "2019-10-03T15:02:58.000Z", 120 | "updateTime": "2019-10-09T11:50:23.000Z", 121 | "wordCount": 579 122 | }, 123 | { 124 | "id": 11, 125 | "space": "egg-react", 126 | "slug": "antd", 127 | "url": "https://yuque.com/easy-team/egg-react/antd", 128 | "title": "Egg React AntD 服务端渲染(SSR) 配置", 129 | "summary": "项目示例: https://github.com/easy-team/egg-react-webpack-boilerplate/tree/antd-theme按需加载依赖配置// ${root}/package.json { "devDependencies": { &...", 130 | "coverImage": null, 131 | "createTime": "2018-11-06T06:00:08.000Z", 132 | "updateTime": "2019-05-27T06:11:57.000Z", 133 | "wordCount": 368 134 | }, 135 | { 136 | "id": 12, 137 | "space": "egg-react", 138 | "slug": "babel", 139 | "url": "https://yuque.com/easy-team/egg-react/babel", 140 | "title": "Egg React 构建 Babel 配置", 141 | "summary": "在进行 Egg + React 进行 SSR 模式开发时,运行 npm run dev 后你会看到如下界面, 启动了两个 Webpack 构建实例:Node 模式 和 Web 模式。SSR 运行需要 Webapck 单独构建 target: node 和 target: web 主要的...", 142 | "coverImage": null, 143 | "createTime": "2019-01-11T09:43:18.000Z", 144 | "updateTime": "2019-05-27T06:34:59.000Z", 145 | "wordCount": 659 146 | }, 147 | { 148 | "id": 13, 149 | "space": "egg-react", 150 | "slug": "typescript", 151 | "url": "https://yuque.com/easy-team/egg-react/typescript", 152 | "title": "Egg React TypeScript 服务端渲染实现", 153 | "summary": "骨架项目https://github.com/easy-team/egg-react-typescript-boilerplate多种渲染https://github.com/easy-team/egg-react-typescript-boilerplate/tree/awesome-ren...", 154 | "coverImage": null, 155 | "createTime": "2019-10-08T11:48:26.000Z", 156 | "updateTime": "2019-10-08T11:48:27.000Z", 157 | "wordCount": 38 158 | }, 159 | { 160 | "id": 14, 161 | "space": "egg-react", 162 | "slug": "async", 163 | "url": "https://yuque.com/easy-team/egg-react/async", 164 | "title": "Egg React 服务端渲染组件异步加载", 165 | "summary": "issue: react-loadable 怎么加入这个骨架中 #23安装依赖npm install react-loadable --save实现异步组件// async-image.jsx import React, { Component } from 'react'; import L...", 166 | "coverImage": null, 167 | "createTime": "2019-06-28T15:58:59.000Z", 168 | "updateTime": "2019-06-28T15:58:59.000Z", 169 | "wordCount": 266 170 | }, 171 | { 172 | "id": 15, 173 | "space": "egg-react", 174 | "slug": "update", 175 | "url": "https://yuque.com/easy-team/egg-react/update", 176 | "title": "Egg React Webpack 项目插件升级更新", 177 | "summary": "发布历史https://github.com/easy-team/egg-react-webpack-boilerplate/blob/master/CHANGELOG.md版本特性easywebpack体系通过 @easy-team 模式内置 Babel 7 方案 骨架分支: master,...", 178 | "coverImage": null, 179 | "createTime": "2019-10-08T11:49:14.000Z", 180 | "updateTime": "2019-10-08T11:49:14.000Z", 181 | "wordCount": 160 182 | }, 183 | { 184 | "id": 16, 185 | "space": "egg-react", 186 | "slug": "about", 187 | "url": "https://yuque.com/easy-team/egg-react/about", 188 | "title": "Egg React 服务端/前端渲染项目常见问题", 189 | "summary": "常见问题汇总AntD 按需加载与主题定制 以及 issue如果实现 Egg + React + Webpack  热更新?服务端渲染如何使用 react-loadabel 实现异步加载React 文件热更新入口配置模板import React from 'react'; import Reac...", 190 | "coverImage": null, 191 | "createTime": "2019-07-27T06:14:30.000Z", 192 | "updateTime": "2019-07-27T06:14:30.000Z", 193 | "wordCount": 216 194 | }, 195 | { 196 | "id": 17, 197 | "space": "egg-react", 198 | "slug": "demo", 199 | "url": "https://yuque.com/easy-team/egg-react/demo", 200 | "title": "Egg React 社区作品", 201 | "summary": "easy-teamhttps://github.com/easy-team/egg-react-webpack-boilerplatehttps://github.com/easy-team/egg-react-typescript-boilerplateReact + Mobx 例子http...", 202 | "coverImage": null, 203 | "createTime": "2019-07-27T06:25:49.000Z", 204 | "updateTime": "2019-10-15T03:14:47.000Z", 205 | "wordCount": 30 206 | } 207 | ], 208 | "user": {} 209 | } -------------------------------------------------------------------------------- /app/lib/db/collection.ts: -------------------------------------------------------------------------------- 1 | import { PlainObject } from 'egg'; 2 | import DB from './base'; 3 | import Condition from '../condition'; 4 | export default class Collection { 5 | private db: DB; 6 | private name: string; 7 | constructor(db: DB, name: string) { 8 | this.db = db; 9 | this.name = name; 10 | } 11 | 12 | public get(): any { 13 | return this.db.get(this.name); 14 | } 15 | 16 | public query(json: any) { 17 | return this.db.query(this.name, json); 18 | } 19 | 20 | public add(json: PlainObject) { 21 | return this.db.add(this.name, json); 22 | } 23 | 24 | public update(where: PlainObject, json: PlainObject) { 25 | return this.db.update(this.name, where, json); 26 | } 27 | 28 | public delete(field: PlainObject) { 29 | return this.db.delete(this.name, field); 30 | } 31 | 32 | public getPager(condition: Condition) { 33 | return this.db.getPager(this.name, condition); 34 | } 35 | 36 | public getOrderAscByField(field: string) { 37 | return this.get().orderBy(field, 'asc').value(); 38 | } 39 | 40 | public getOrderDescByField(field: string) { 41 | return this.get().orderBy(field, 'desc').value(); 42 | } 43 | } -------------------------------------------------------------------------------- /app/lib/db/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import FileDB from './file'; 3 | import MongoDB from './mongo'; 4 | import MySQLDB from './mysql'; 5 | export default function(type?: string) { 6 | switch (type) { 7 | case 'mysql': 8 | return new MySQLDB(); 9 | case 'mongo': 10 | return new MySQLDB(); 11 | default: 12 | return new FileDB(); 13 | } 14 | } -------------------------------------------------------------------------------- /app/lib/db/file.ts: -------------------------------------------------------------------------------- 1 | import * as lowdb from 'lowdb'; 2 | import * as lodashid from 'lodash-id'; 3 | import * as FileSync from 'lowdb/adapters/FileSync'; 4 | import { PlainObject } from 'egg'; 5 | import BaseDB from './base'; 6 | import Condition from '../condition'; 7 | export default class FileDB extends BaseDB { 8 | public instance: any; 9 | constructor(name?: string) { 10 | super(name); 11 | const file = new FileSync(this.name); 12 | this.instance = lowdb(file); 13 | this.instance._.mixin(lodashid); 14 | this.create(); 15 | } 16 | 17 | public create() { 18 | this.instance.defaults({ article: [], user: {} }).write(); 19 | } 20 | 21 | public get(collectionName: string) { 22 | return this.instance.get(collectionName); 23 | } 24 | 25 | public query(collectionName: string, json: PlainObject) { 26 | return this.get(collectionName) 27 | .find(json) 28 | .write(); 29 | } 30 | 31 | public add(collectionName: string, json: PlainObject) { 32 | return this.get(collectionName) 33 | .push(json) 34 | .write(); 35 | } 36 | 37 | public update(collectionName: string, where: PlainObject, json: PlainObject) { 38 | return this.get(collectionName).find(where).assign(json).write(); 39 | } 40 | 41 | public delete(collectionName: string, json: PlainObject) { 42 | return this.get(collectionName).remove(json).write(); 43 | } 44 | 45 | public getPager(collectionName: string, condition: Condition) { 46 | const { 47 | where, 48 | like, 49 | pageIndex, 50 | pageSize, 51 | orderByField, 52 | orderBy 53 | } = condition; 54 | const start = (pageIndex - 1) * pageSize; 55 | const end = pageIndex * pageSize; 56 | const result = this.get(collectionName) 57 | .filter(where) 58 | .filter(item => { 59 | return Object.keys(like).reduce((isLike, key) => { 60 | return isLike && item[key] && item[key].indexOf(like[key]) > -1; 61 | }, true); 62 | }) 63 | .orderBy(orderByField, orderBy); 64 | const total = result.size().value(); 65 | const list = result.slice(start, end).value(); 66 | return { total, list }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/lib/db/mongo.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import BaseDB from './base'; 3 | export default class MongoDB extends BaseDB { 4 | constructor(name: string) { 5 | super(name); 6 | } 7 | } -------------------------------------------------------------------------------- /app/lib/db/mysql.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import BaseDB from './base'; 3 | export default class MySQLDB extends BaseDB { 4 | constructor(name?: string) { 5 | super(name); 6 | } 7 | } -------------------------------------------------------------------------------- /app/middleware/access.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as util from 'util'; 3 | import { Context } from 'egg'; 4 | 5 | export default function() { 6 | const skipExt = [ '.png', '.jpeg', '.jpg', '.ico', '.gif' ]; 7 | return async (ctx: Context, next: () => Promise) => { 8 | const start = new Date().getTime(); 9 | await next; 10 | 11 | const rs = Math.ceil(new Date().getTime() - start); 12 | 13 | ctx.set('X-Response-Time', String(rs)); 14 | 15 | const ext = path.extname(ctx.url).toLocaleLowerCase(); 16 | const isSkip = skipExt.indexOf(ext) !== -1 && ctx.status < 400; 17 | 18 | if (!isSkip) { 19 | const ip = ctx.get('X-Real-IP') || ctx.ip; 20 | const port = ctx.get('X-Real-Port'); 21 | const protocol = ctx.protocol.toUpperCase(); 22 | const method = ctx.method; 23 | const url = ctx.url; 24 | const status = ctx.status; 25 | const length = ctx.length || '-'; 26 | const referrer = ctx.get('referrer') || '-'; 27 | const ua = ctx.get('user-agent') || '-'; 28 | const serverTime = ctx.response.get('X-Server-Response-Time') || '-'; 29 | const message = util.format('[access] %s:%s - %s %s %s/%s %s %s %s %s %s', 30 | ...[ip, port, method, url, protocol, status, length, referrer, rs, serverTime, ua]); 31 | ctx.logger.info(message); 32 | } 33 | }; 34 | } -------------------------------------------------------------------------------- /app/middleware/locals.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'egg'; 3 | 4 | export default function locals(): any { 5 | return async (ctx: Context, next: () => Promise) => { 6 | ctx.locals.locale = ctx.query.locale || 'cn'; 7 | ctx.locals.origin = ctx.request.origin; 8 | await next(); 9 | }; 10 | } -------------------------------------------------------------------------------- /app/mocks/article/list.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const data = { 3 | list: [{ 4 | id: 0, 5 | title: `Egg + React 服务端渲染骨架`, 6 | summary: '基于Egg + React + Webpack3/Webpack2 多页面和单页面服务器渲染同构工程骨架项目', 7 | hits: 550, 8 | url: 'https://github.com/hubcarl/egg-react-webpack-boilerplate' 9 | }, { 10 | id: 1, 11 | title: '前端工程化解决方案easywebpack', 12 | summary: 'programming instead of configuration, webpack is so easy', 13 | hits: 550, 14 | url: 'https://github.com/hubcarl/easywebpack' 15 | }, { 16 | id: 2, 17 | title: '前端工程化解决方案脚手架easywebpack-cli', 18 | summary: 'easywebpack command tool, support init Vue/Reac/Weex boilerplate', 19 | hits: 278, 20 | url: 'https://github.com/hubcarl/easywebpack-cli' 21 | }, { 22 | id: 3, 23 | title: 'react-渐进式JavaScript 框架', 24 | summary: '简单小巧的核心,渐进式技术栈,足以应付任何规模的应用', 25 | hits: 200, 26 | url: 'https://cn.reactjs.org' 27 | }, { 28 | id: 4, 29 | title: 'webpack配置官方文档', 30 | summary: 'webpack is a module bundler for modern JavaScript applications.', 31 | hits: 550, 32 | url: 'https://webpack.js.org/configuration/' 33 | }, { 34 | id: 5, 35 | title: 'egg-为企业级框架和应用而生', 36 | summary: 'Born to buildbetter enterprise frameworks and apps with Node.js & Koa', 37 | hits: 278, 38 | url: 'https://eggjs.org/' 39 | }, { 40 | id: 5, 41 | title: 'axios-基于 Promise 的 HTTP 请求客户端', 42 | summary: '基于 Promise 的 HTTP 请求客户端,可同时在浏览器和 node.js 中使用', 43 | hits: 998, 44 | url: 'https://www.awesomes.cn/repo/mzabriskie/axios' 45 | }, { 46 | id: 5, 47 | title: 'Centralized State Management for react.js', 48 | summary: 'reactx 是一个专为react.js 应用程序开发的状态管理模式', 49 | hits: 232, 50 | url: 'https://github.com/reactjs/reactx' 51 | }, { 52 | id: 5, 53 | title: 'react服务器渲染', 54 | summary: '服务器渲染可以加快首屏速度,利于SEO', 55 | hits: 565, 56 | url: 'http://csbun.github.io/blog/2016/08/react-2-0-server-side-rendering/' 57 | }, { 58 | id: 5, 59 | title: 'webpack服务器构建', 60 | summary: 'Webpack is an amazing tool.', 61 | hits: 988, 62 | url: 'http://jlongster.com/Backend-Apps-with-Webpack--Part-I' 63 | }, { 64 | id: 5, 65 | title: 'react component loader for Webpack', 66 | summary: 'Webpack loader for react.js components', 67 | hits: 322, 68 | url: 'https://github.com/reactjs/react-loader' 69 | }, { 70 | id: 5, 71 | title: 'react-router--The official router for react.js', 72 | summary: 'It deeply integrates with react.js core to make building Single Page Applications with react.js a breeze', 73 | hits: 566, 74 | url: 'https://github.com/reactjs/react-router' 75 | }, { 76 | id: 5, 77 | title: 'react生命周期', 78 | summary: 'react.js 生命周期和route的生命周期讲解', 79 | hits: 434, 80 | url: 'http://www.jianshu.com/p/e9f884b6ba6c' 81 | }, { 82 | id: 5, 83 | title: 'babel到底将代码转换成什么鸟样', 84 | summary: '将babel捧作前端一个划时代的工具一定也不为过,它的出现让许多程序员幸福地用上了es6新语法', 85 | hits: 432, 86 | url: 'https://github.com/lcxfs1991/blog/issues/9' 87 | }, { 88 | id: 5, 89 | title: 'HTTP2 的真正性能到底如何', 90 | summary: 'HTTP2 的真正性能到底如何', 91 | hits: 565, 92 | url: 'https://segmentfault.com/a/1190000007219256?utm_source=weekly&utm_medium=email&utm_campaign=email_weekly' 93 | }, { 94 | id: 5, 95 | title: 'HTTP,HTTP2.0,SPDY,HTTPS讲解', 96 | summary: '使用SPDY加快你的网站速度', 97 | hits: 787, 98 | url: 'http://www.alloyteam.com/2016/07/httphttp2-0spdyhttps-reading-this-is-enough/' 99 | }, { 100 | id: 5, 101 | title: 'git - 简明指南', 102 | summary: '助你入门 git 的简明指南', 103 | hits: 121, 104 | url: 'http://rogerdudler.github.io/git-guide/index.zh.html' 105 | }, { 106 | id: 5, 107 | title: 'react从1升级到2', 108 | summary: 'Migrating from v1 to v2', 109 | hits: 555, 110 | url: 'https://webpack.js.org/guides/migrating/' 111 | }] 112 | }; 113 | 114 | let initId = 1; 115 | 116 | data.list.forEach(item => { 117 | item.id = initId++; 118 | }); 119 | 120 | const total = data.list.length; 121 | export function getPage(pageIndex = 1, pageSize = 10) { 122 | const start = (pageIndex - 1) * pageSize; 123 | const end = start + Number(pageSize); 124 | return { list: data.list.slice(start, end), total }; 125 | } 126 | export function getDetail(id: number) { 127 | return data.list.filter(item => { 128 | return item.id === id; 129 | }).slice(0, 1); 130 | } -------------------------------------------------------------------------------- /app/model/article.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { JsonProperty } from '@hubcarl/json-typescript-mapper'; 4 | 5 | export default class Article { 6 | @JsonProperty('id') 7 | public id?: string; 8 | @JsonProperty('title') 9 | public title?: string; 10 | @JsonProperty('summary') 11 | public summary?: string; 12 | @JsonProperty('categoryId') 13 | public categoryId?: number; 14 | @JsonProperty('tag') 15 | public tag?: string ; 16 | @JsonProperty('categoryId') 17 | public authorId?: number; 18 | @JsonProperty('createTime') 19 | public createTime?: number; 20 | @JsonProperty('hits') 21 | public hits?: number; 22 | @JsonProperty('url') 23 | public url?: string; 24 | @JsonProperty('status') 25 | public status?: number; 26 | @JsonProperty('content') 27 | public content?: string; 28 | 29 | // constructor must be init everyone JsonProperty 30 | constructor() { 31 | this.id = ''; 32 | this.title = ''; 33 | this.summary = ''; 34 | this.categoryId = -1; 35 | this.tag = ''; 36 | this.authorId = -1; 37 | this.url = ''; 38 | this.status = 0; 39 | this.hits = 0; 40 | this.content = ''; 41 | this.createTime = Date.now(); 42 | } 43 | } -------------------------------------------------------------------------------- /app/router.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from 'egg'; 3 | export default (app: Application) => { 4 | const { router, controller } = app; 5 | router.get('/demo/node', controller.demo.nodeData); 6 | router.get('/demo/async', controller.demo.asyncData); 7 | router.get('/demo/api/article', controller.demo.article); 8 | router.get('/antd', controller.antd.index); 9 | router.get('/api/blog/list', controller.blog.list); 10 | router.get('/api/blog/:id', controller.blog.detail); 11 | router.get('/(.*?)', controller.blog.home); 12 | }; 13 | -------------------------------------------------------------------------------- /app/service/article.ts: -------------------------------------------------------------------------------- 1 | import { Context, Service } from 'egg'; 2 | import { deserialize } from '@hubcarl/json-typescript-mapper'; 3 | import Colllection from '../lib/db/collection'; 4 | import Article from '../model/article'; 5 | import Condition from '../lib/condition'; 6 | 7 | export default class ArticeService extends Service { 8 | private context: Context; 9 | private colllection: Colllection; 10 | constructor(ctx: Context) { 11 | super(ctx); 12 | this.context = ctx; 13 | this.colllection = new Colllection(ctx.db, 'article'); 14 | } 15 | 16 | public async getArtilceList(condition: Condition) { 17 | if (condition.categoryId) { 18 | condition.where.categoryId = condition.categoryId; 19 | } 20 | if (condition.status) { 21 | condition.where.status = condition.status; 22 | } 23 | if (condition.title) { 24 | condition.like.title = condition.title; 25 | } 26 | return this.colllection.getPager(condition); 27 | } 28 | 29 | public async saveArticle(data: object) { 30 | const article: Article = deserialize(Article, data); 31 | if (article.id) { 32 | return this.colllection.update({ id: article.id }, article); 33 | } 34 | article.id = this.context.db.getUniqueId(); 35 | this.colllection.add(article); 36 | return article; 37 | } 38 | 39 | public async query(json: object) { 40 | return this.colllection.query(json); 41 | } 42 | 43 | public async deleteArticle(id: string) { 44 | return this.colllection.delete({ id }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | import DB from '../lib/db/base'; 2 | declare module 'egg' { 3 | interface Application { 4 | db: DB; 5 | } 6 | 7 | interface Context { 8 | db: DB; 9 | } 10 | } -------------------------------------------------------------------------------- /app/typings/module.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lodash-id'; -------------------------------------------------------------------------------- /app/view/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/view/.gitkeep -------------------------------------------------------------------------------- /app/view/README.md: -------------------------------------------------------------------------------- 1 | ## egg规范view目录, 保证view文件夹存在, 否则app.config.view.root为空, 编译服务器文件会存放到该目录. -------------------------------------------------------------------------------- /app/web/asset/css/blog.css: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Bootstrap v3.3.2 (http://getbootstrap.com) 4 | * Copyright 2011-2015 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | 8 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 9 | * { 10 | box-sizing: border-box; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | html { 15 | font-family: sans-serif; 16 | -webkit-text-size-adjust: 100%; 17 | -ms-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | body { 22 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 23 | line-height: 1.7; 24 | font-size: 16px; 25 | color: #404040; 26 | overflow-x: hidden; 27 | background-color: #fff; 28 | } 29 | p { 30 | margin: 30px 0; 31 | } 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 39 | 40 | line-height: 1.1; 41 | font-weight: 800; 42 | } 43 | h1 { 44 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 45 | } 46 | h4 { 47 | font-size: 18px; 48 | margin-top: 24px; 49 | margin-bottom: 24px; 50 | } 51 | a { 52 | color: #404040; 53 | } 54 | a:hover, 55 | a:focus { 56 | color: #0085a1; 57 | } 58 | a img:hover, 59 | a img:focus { 60 | cursor: zoom-in; 61 | } 62 | article { 63 | overflow-x: hidden; 64 | } 65 | blockquote { 66 | color: #808080; 67 | font-style: italic; 68 | font-size: 0.95em; 69 | margin: 20px 0 20px; 70 | } 71 | blockquote p { 72 | margin: 0; 73 | } 74 | 75 | hr.small { 76 | max-width: 100px; 77 | margin: 15px auto; 78 | border-width: 4px; 79 | border-color: white; 80 | } 81 | hr.large { 82 | max-width: 280px; 83 | margin: 15px auto; 84 | border-width: 4px; 85 | border-color: white; 86 | } 87 | .hide { 88 | display: none !important; 89 | } 90 | .show { 91 | display: block !important; 92 | } 93 | .invisible { 94 | visibility: hidden; 95 | } 96 | 97 | *:before, 98 | *:after { 99 | box-sizing: border-box; 100 | } 101 | 102 | ul { 103 | margin: 0; 104 | padding: 0; 105 | } 106 | 107 | article, 108 | aside, 109 | details, 110 | figcaption, 111 | figure, 112 | footer, 113 | header, 114 | hgroup, 115 | main, 116 | menu, 117 | nav, 118 | section, 119 | summary { 120 | display: block; 121 | } 122 | audio, 123 | canvas, 124 | progress, 125 | video { 126 | display: inline-block; 127 | vertical-align: baseline; 128 | } 129 | audio:not([controls]) { 130 | display: none; 131 | height: 0; 132 | } 133 | [hidden], 134 | template { 135 | display: none; 136 | } 137 | a { 138 | background-color: transparent; 139 | } 140 | a:active, 141 | a:hover { 142 | outline: 0; 143 | } 144 | abbr[title] { 145 | border-bottom: 1px dotted; 146 | } 147 | b, 148 | strong { 149 | font-weight: bold; 150 | } 151 | h1 { 152 | margin: .67em 0; 153 | font-size: 2em; 154 | } 155 | small { 156 | font-size: 80%; 157 | } 158 | img { 159 | border: 0; 160 | } 161 | svg:not(:root) { 162 | overflow: hidden; 163 | } 164 | figure { 165 | margin: 1em 40px; 166 | } 167 | hr { 168 | height: 0; 169 | box-sizing: content-box; 170 | } 171 | pre { 172 | overflow: auto; 173 | } 174 | code, 175 | kbd, 176 | pre, 177 | samp { 178 | font-family: monospace, monospace; 179 | font-size: 1em; 180 | } 181 | button, 182 | input, 183 | optgroup, 184 | select, 185 | textarea { 186 | margin: 0; 187 | font: inherit; 188 | color: inherit; 189 | } 190 | button { 191 | overflow: visible; 192 | } 193 | button, 194 | select { 195 | text-transform: none; 196 | } 197 | button, 198 | html input[type="button"], 199 | input[type="reset"], 200 | input[type="submit"] { 201 | -webkit-appearance: button; 202 | cursor: pointer; 203 | } 204 | button[disabled], 205 | html input[disabled] { 206 | cursor: default; 207 | } 208 | button::-moz-focus-inner, 209 | input::-moz-focus-inner { 210 | padding: 0; 211 | border: 0; 212 | } 213 | input { 214 | line-height: normal; 215 | } 216 | input[type="checkbox"], 217 | input[type="radio"] { 218 | box-sizing: border-box; 219 | padding: 0; 220 | } 221 | input[type="number"]::-webkit-inner-spin-button, 222 | input[type="number"]::-webkit-outer-spin-button { 223 | height: auto; 224 | } 225 | input[type="search"] { 226 | box-sizing: content-box; 227 | -webkit-appearance: textfield; 228 | } 229 | input[type="search"]::-webkit-search-cancel-button, 230 | input[type="search"]::-webkit-search-decoration { 231 | -webkit-appearance: none; 232 | } 233 | fieldset { 234 | padding: .35em .625em .75em; 235 | margin: 0 2px; 236 | border: 1px solid #c0c0c0; 237 | } 238 | legend { 239 | padding: 0; 240 | border: 0; 241 | } 242 | textarea { 243 | overflow: auto; 244 | } 245 | optgroup { 246 | font-weight: bold; 247 | } 248 | table { 249 | border-spacing: 0; 250 | border-collapse: collapse; 251 | } 252 | td, 253 | th { 254 | padding: 0; 255 | } 256 | 257 | input, 258 | button, 259 | select, 260 | textarea { 261 | font-family: inherit; 262 | font-size: inherit; 263 | line-height: inherit; 264 | } 265 | a { 266 | color: #337ab7; 267 | text-decoration: none; 268 | } 269 | a:hover, 270 | a:focus { 271 | color: #23527c; 272 | text-decoration: underline; 273 | } 274 | a:focus { 275 | outline: thin dotted; 276 | outline: 5px auto -webkit-focus-ring-color; 277 | outline-offset: -2px; 278 | } 279 | li { 280 | list-style: none; 281 | } 282 | 283 | 284 | .fade { 285 | opacity: 0; 286 | -webkit-transition: opacity .15s linear; 287 | transition: opacity .15s linear; 288 | } 289 | .fade.in { 290 | opacity: 1; 291 | } 292 | .collapse { 293 | display: none; 294 | visibility: hidden; 295 | } 296 | .collapse.in { 297 | display: block; 298 | visibility: visible; 299 | } 300 | 301 | .collapsing { 302 | position: relative; 303 | height: 0; 304 | overflow: hidden; 305 | -webkit-transition-timing-function: ease; 306 | transition-timing-function: ease; 307 | -webkit-transition-duration: .35s; 308 | transition-duration: .35s; 309 | -webkit-transition-property: height, visibility; 310 | transition-property: height, visibility; 311 | } 312 | 313 | 314 | .clearfix:before, 315 | .clearfix:after, 316 | .easy-menu-menu-fluid:before, 317 | .easy-menu-menu-fluid:after, 318 | .nav:before, 319 | .nav:after, 320 | .easy-menu:before, 321 | .easy-menu:after, 322 | .easy-menu-header:before, 323 | .easy-menu-header:after, 324 | .easy-menu-collapse:before, 325 | .easy-menu-collapse:after, 326 | .modal-footer:after { 327 | display: table; 328 | content: " "; 329 | } 330 | .clearfix:after, 331 | .easy-menu-menu-fluid:after, 332 | .row:after, 333 | .nav:after, 334 | .easy-menu:after, 335 | .easy-menu-header:after, 336 | .easy-menu-collapse:after, 337 | .pager:after, 338 | .panel-body:after, 339 | .modal-footer:after { 340 | clear: both; 341 | } 342 | 343 | .easy-menu-toggle { 344 | position: absolute; 345 | padding: 0; 346 | margin: -1px; 347 | overflow: hidden; 348 | clip: rect(0, 0, 0, 0); 349 | border: 0; 350 | width: 36px; 351 | height: 36px; 352 | background-image: url('../images/menu.png'); 353 | background-size: 36px 36px; 354 | } 355 | .easy-menu-toggle-focusable:active, 356 | .easy-menu-toggle-focusable:focus { 357 | position: static; 358 | width: auto; 359 | height: auto; 360 | margin: 0; 361 | overflow: visible; 362 | clip: auto; 363 | } 364 | 365 | .easy-top-intro-container { 366 | padding-right: 15px; 367 | padding-left: 15px; 368 | margin-right: auto; 369 | margin-left: auto; 370 | } 371 | 372 | .easy-menu-menu-fluid { 373 | padding-right: 15px; 374 | padding-left: 15px; 375 | margin-right: auto; 376 | margin-left: auto; 377 | } 378 | 379 | 380 | .dropup, 381 | .dropdown { 382 | position: relative; 383 | } 384 | .dropdown-toggle:focus { 385 | outline: 0; 386 | } 387 | .dropdown-menu { 388 | position: absolute; 389 | top: 100%; 390 | left: 0; 391 | z-index: 1000; 392 | display: none; 393 | float: left; 394 | min-width: 160px; 395 | padding: 5px 0; 396 | margin: 2px 0 0; 397 | font-size: 14px; 398 | text-align: left; 399 | list-style: none; 400 | background-color: #fff; 401 | background-clip: padding-box; 402 | border: 1px solid #ccc; 403 | border: 1px solid rgba(0, 0, 0, .15); 404 | border-radius: 4px; 405 | box-shadow: 0 6px 12px rgba(0, 0, 0, .175); 406 | } 407 | .dropdown-menu.pull-right { 408 | right: 0; 409 | left: auto; 410 | } 411 | .dropdown-menu .divider { 412 | height: 1px; 413 | margin: 9px 0; 414 | overflow: hidden; 415 | background-color: #e5e5e5; 416 | } 417 | .dropdown-menu > li > a { 418 | display: block; 419 | padding: 3px 20px; 420 | clear: both; 421 | font-weight: normal; 422 | line-height: 1.42857143; 423 | color: #333; 424 | white-space: nowrap; 425 | } 426 | .dropdown-menu > li > a:hover, 427 | .dropdown-menu > li > a:focus { 428 | color: #262626; 429 | text-decoration: none; 430 | background-color: #f5f5f5; 431 | } 432 | .dropdown-menu > .active > a, 433 | .dropdown-menu > .active > a:hover, 434 | .dropdown-menu > .active > a:focus { 435 | color: #fff; 436 | text-decoration: none; 437 | background-color: #337ab7; 438 | outline: 0; 439 | } 440 | .dropdown-menu > .disabled > a, 441 | .dropdown-menu > .disabled > a:hover, 442 | .dropdown-menu > .disabled > a:focus { 443 | color: #777; 444 | } 445 | .dropdown-menu > .disabled > a:hover, 446 | .dropdown-menu > .disabled > a:focus { 447 | text-decoration: none; 448 | cursor: not-allowed; 449 | background-color: transparent; 450 | background-image: none; 451 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 452 | } 453 | .open > .dropdown-menu { 454 | display: block; 455 | } 456 | .open > a { 457 | outline: 0; 458 | } 459 | .dropdown-menu-right { 460 | right: 0; 461 | left: auto; 462 | } 463 | .dropdown-menu-left { 464 | right: auto; 465 | left: 0; 466 | } 467 | .dropdown-header { 468 | display: block; 469 | padding: 3px 20px; 470 | font-size: 12px; 471 | line-height: 1.42857143; 472 | color: #777; 473 | white-space: nowrap; 474 | } 475 | .dropdown-backdrop { 476 | position: fixed; 477 | top: 0; 478 | right: 0; 479 | bottom: 0; 480 | left: 0; 481 | z-index: 990; 482 | } 483 | 484 | .dropup .caret, 485 | .easy-menu-fixed-bottom .dropdown .caret { 486 | content: ""; 487 | border-top: 0; 488 | border-bottom: 4px solid; 489 | } 490 | .dropup .dropdown-menu, 491 | .easy-menu-fixed-bottom .dropdown .dropdown-menu { 492 | top: auto; 493 | bottom: 100%; 494 | margin-bottom: 2px; 495 | } 496 | 497 | .nav { 498 | padding-left: 0; 499 | margin-bottom: 0; 500 | list-style: none; 501 | } 502 | .nav > li { 503 | position: relative; 504 | display: block; 505 | } 506 | .nav > li > a { 507 | position: relative; 508 | display: block; 509 | padding: 10px 15px; 510 | } 511 | .nav > li > a:hover, 512 | .nav > li > a:focus { 513 | text-decoration: none; 514 | background-color: #eee; 515 | } 516 | .nav > li.disabled > a { 517 | color: #777; 518 | } 519 | .nav > li.disabled > a:hover, 520 | .nav > li.disabled > a:focus { 521 | color: #777; 522 | text-decoration: none; 523 | cursor: not-allowed; 524 | background-color: transparent; 525 | } 526 | .nav .open > a, 527 | .nav .open > a:hover, 528 | .nav .open > a:focus { 529 | background-color: #eee; 530 | border-color: #337ab7; 531 | } 532 | .nav .nav-divider { 533 | height: 1px; 534 | margin: 9px 0; 535 | overflow: hidden; 536 | background-color: #e5e5e5; 537 | } 538 | .nav > li > a > img { 539 | max-width: none; 540 | } 541 | .easy-menu { 542 | position: relative; 543 | min-height: 50px; 544 | margin-bottom: 20px; 545 | border: 1px solid transparent; 546 | } 547 | .easy-menu-collapse { 548 | padding-right: 15px; 549 | padding-left: 15px; 550 | overflow-x: visible; 551 | -webkit-overflow-scrolling: touch; 552 | border-top: 1px solid transparent; 553 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); 554 | } 555 | .easy-menu-collapse.in { 556 | overflow-y: auto; 557 | } 558 | .easy-menu-fixed-top { 559 | top: 0; 560 | border-width: 0 0 1px; 561 | } 562 | .easy-menu-fixed-bottom { 563 | bottom: 0; 564 | margin-bottom: 0; 565 | border-width: 1px 0 0; 566 | } 567 | .easy-menu-brand { 568 | float: left; 569 | height: 50px; 570 | padding: 15px 15px; 571 | font-size: 18px; 572 | line-height: 20px; 573 | font-weight: bold; 574 | } 575 | .easy-menu-brand:hover, 576 | .easy-menu-brand:focus { 577 | text-decoration: none; 578 | } 579 | .easy-menu-brand > img { 580 | display: block; 581 | } 582 | 583 | 584 | .easy-menu-header, 585 | .easy-menu-menu-fluid > .easy-menu-header, 586 | .easy-menu-collapse, 587 | .easy-menu-menu-fluid > .easy-menu-collapse { 588 | margin-right: -15px; 589 | margin-left: -15px; 590 | } 591 | .easy-menu-fixed-top, 592 | .easy-menu-fixed-bottom { 593 | position: fixed; 594 | right: 0; 595 | left: 0; 596 | z-index: 1030; 597 | } 598 | 599 | .easy-menu-toggle { 600 | position: relative; 601 | float: right; 602 | padding: 9px 10px; 603 | margin-top: 8px; 604 | margin-right: 15px; 605 | margin-bottom: 8px; 606 | border-radius: 4px; 607 | } 608 | .easy-menu-toggle:focus { 609 | outline: 0; 610 | } 611 | .easy-menu-toggle .icon-bar { 612 | display: block; 613 | width: 22px; 614 | height: 2px; 615 | border-radius: 1px; 616 | } 617 | .easy-menu-toggle .icon-bar + .icon-bar { 618 | margin-top: 4px; 619 | } 620 | 621 | .easy-menu-form { 622 | padding: 10px 15px; 623 | margin-top: 8px; 624 | margin-right: -15px; 625 | margin-bottom: 8px; 626 | margin-left: -15px; 627 | border-top: 1px solid transparent; 628 | border-bottom: 1px solid transparent; 629 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); 630 | } 631 | 632 | .easy-menu-nav { 633 | margin: 7.5px -15px; 634 | } 635 | .easy-menu-nav > li > a { 636 | padding-top: 10px; 637 | padding-bottom: 10px; 638 | line-height: 20px; 639 | } 640 | 641 | 642 | .easy-menu-nav > li > .dropdown-menu { 643 | margin-top: 0; 644 | border-top-left-radius: 0; 645 | border-top-right-radius: 0; 646 | } 647 | .easy-menu-fixed-bottom .easy-menu-nav > li > .dropdown-menu { 648 | margin-bottom: 0; 649 | border-top-left-radius: 4px; 650 | border-top-right-radius: 4px; 651 | border-bottom-right-radius: 0; 652 | border-bottom-left-radius: 0; 653 | } 654 | .easy-menu-btn { 655 | margin-top: 8px; 656 | margin-bottom: 8px; 657 | } 658 | .easy-menu-btn.btn-sm { 659 | margin-top: 10px; 660 | margin-bottom: 10px; 661 | } 662 | .easy-menu-btn.btn-xs { 663 | margin-top: 14px; 664 | margin-bottom: 14px; 665 | } 666 | .easy-menu-text { 667 | margin-top: 15px; 668 | margin-bottom: 15px; 669 | } 670 | .easy-menu-default { 671 | background-color: #f8f8f8; 672 | border-color: #e7e7e7; 673 | } 674 | .easy-menu-default .easy-menu-brand { 675 | color: #777; 676 | } 677 | .easy-menu-default .easy-menu-brand:hover, 678 | .easy-menu-default .easy-menu-brand:focus { 679 | color: #5e5e5e; 680 | background-color: transparent; 681 | } 682 | .easy-menu-default .easy-menu-text { 683 | color: #777; 684 | } 685 | .easy-menu-default .easy-menu-nav > li > a { 686 | color: #777; 687 | } 688 | .easy-menu-default .easy-menu-nav > li > a:hover, 689 | .easy-menu-default .easy-menu-nav > li > a:focus { 690 | color: #333; 691 | background-color: transparent; 692 | } 693 | .easy-menu-default .easy-menu-nav > .active > a, 694 | .easy-menu-default .easy-menu-nav > .active > a:hover, 695 | .easy-menu-default .easy-menu-nav > .active > a:focus { 696 | color: #555; 697 | background-color: #e7e7e7; 698 | } 699 | .easy-menu-default .easy-menu-nav > .disabled > a, 700 | .easy-menu-default .easy-menu-nav > .disabled > a:hover, 701 | .easy-menu-default .easy-menu-nav > .disabled > a:focus { 702 | color: #ccc; 703 | background-color: transparent; 704 | } 705 | .easy-menu-default .easy-menu-toggle { 706 | border-color: #ddd; 707 | } 708 | 709 | .easy-menu-default .easy-menu-toggle .icon-bar { 710 | background-color: #888; 711 | } 712 | .easy-menu-default .easy-menu-collapse, 713 | .easy-menu-default .easy-menu-form { 714 | border-color: #e7e7e7; 715 | } 716 | .easy-menu-default .easy-menu-nav > .open > a, 717 | .easy-menu-default .easy-menu-nav > .open > a:hover, 718 | .easy-menu-default .easy-menu-nav > .open > a:focus { 719 | color: #555; 720 | background-color: #e7e7e7; 721 | } 722 | 723 | .easy-menu-default .easy-menu-link { 724 | color: #777; 725 | } 726 | .easy-menu-default .easy-menu-link:hover { 727 | color: #333; 728 | } 729 | .easy-menu-default .btn-link { 730 | color: #777; 731 | } 732 | .easy-menu-default .btn-link:hover, 733 | .easy-menu-default .btn-link:focus { 734 | color: #333; 735 | } 736 | .easy-menu-default .btn-link[disabled]:hover, 737 | fieldset[disabled] .easy-menu-default .btn-link:hover, 738 | .easy-menu-default .btn-link[disabled]:focus, 739 | fieldset[disabled] .easy-menu-default .btn-link:focus { 740 | color: #ccc; 741 | } 742 | .easy-menu-inverse { 743 | background-color: #222; 744 | border-color: #080808; 745 | } 746 | .easy-menu-inverse .easy-menu-brand { 747 | color: #9d9d9d; 748 | } 749 | .easy-menu-inverse .easy-menu-brand:hover, 750 | .easy-menu-inverse .easy-menu-brand:focus { 751 | color: #fff; 752 | background-color: transparent; 753 | } 754 | .easy-menu-inverse .easy-menu-text { 755 | color: #9d9d9d; 756 | } 757 | .easy-menu-inverse .easy-menu-nav > li > a { 758 | color: #9d9d9d; 759 | } 760 | .easy-menu-inverse .easy-menu-nav > li > a:hover, 761 | .easy-menu-inverse .easy-menu-nav > li > a:focus { 762 | color: #fff; 763 | background-color: transparent; 764 | } 765 | .easy-menu-inverse .easy-menu-nav > .active > a, 766 | .easy-menu-inverse .easy-menu-nav > .active > a:hover, 767 | .easy-menu-inverse .easy-menu-nav > .active > a:focus { 768 | color: #fff; 769 | background-color: #080808; 770 | } 771 | .easy-menu-inverse .easy-menu-nav > .disabled > a, 772 | .easy-menu-inverse .easy-menu-nav > .disabled > a:hover, 773 | .easy-menu-inverse .easy-menu-nav > .disabled > a:focus { 774 | color: #444; 775 | background-color: transparent; 776 | } 777 | .easy-menu-inverse .easy-menu-toggle { 778 | border-color: #333; 779 | } 780 | .easy-menu-inverse .easy-menu-toggle:hover, 781 | .easy-menu-inverse .easy-menu-toggle:focus { 782 | background-color: #333; 783 | } 784 | .easy-menu-inverse .easy-menu-toggle .icon-bar { 785 | background-color: #fff; 786 | } 787 | .easy-menu-inverse .easy-menu-collapse, 788 | .easy-menu-inverse .easy-menu-form { 789 | border-color: #101010; 790 | } 791 | .easy-menu-inverse .easy-menu-nav > .open > a, 792 | .easy-menu-inverse .easy-menu-nav > .open > a:hover, 793 | .easy-menu-inverse .easy-menu-nav > .open > a:focus { 794 | color: #fff; 795 | background-color: #080808; 796 | } 797 | 798 | .easy-menu-inverse .easy-menu-link { 799 | color: #9d9d9d; 800 | } 801 | .easy-menu-inverse .easy-menu-link:hover { 802 | color: #fff; 803 | } 804 | .easy-menu-inverse .btn-link { 805 | color: #9d9d9d; 806 | } 807 | .easy-menu-inverse .btn-link:hover, 808 | .easy-menu-inverse .btn-link:focus { 809 | color: #fff; 810 | } 811 | .easy-menu-inverse .btn-link[disabled]:hover, 812 | fieldset[disabled] .easy-menu-inverse .btn-link:hover, 813 | .easy-menu-inverse .btn-link[disabled]:focus, 814 | fieldset[disabled] .easy-menu-inverse .btn-link:focus { 815 | color: #444; 816 | } 817 | 818 | .easy-menu-custom { 819 | background: white; 820 | position: absolute; 821 | top: 0; 822 | left: 0; 823 | width: 100%; 824 | z-index: 3; 825 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 826 | line-height: 1.7; 827 | } 828 | 829 | .easy-menu-custom .nav li a { 830 | text-transform: uppercase; 831 | font-size: 16px; 832 | letter-spacing: 1px; 833 | } 834 | 835 | .easy-intro-header { 836 | background: no-repeat center center; 837 | background-color: #808080; 838 | background-attachment: scroll; 839 | background-size: cover; 840 | -o-background-size: cover; 841 | margin-bottom: 0px; 842 | /* 0 on mobile, changed the setting*/ 843 | } 844 | 845 | .easy-intro-header .easy-top-intro-info { 846 | padding: 100px 0 50px; 847 | color: white; 848 | } 849 | 850 | .easy-intro-header .easy-top-intro-info { 851 | text-align: center; 852 | } 853 | .easy-intro-header .easy-top-intro-info h1{ 854 | margin-top: 0; 855 | font-size: 36px; 856 | } 857 | .easy-intro-header .easy-top-intro-info .subheading, 858 | .easy-intro-header .page-heading .subheading { 859 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 860 | line-height: 1.7; 861 | font-size: 20px; 862 | line-height: 1.1; 863 | display: block; 864 | font-weight: 300; 865 | margin: 10px 0 0; 866 | } 867 | 868 | .easy-intro-header .post-heading h1 { 869 | font-size: 27px; 870 | } 871 | .easy-intro-header .post-heading .subheading, 872 | .easy-intro-header .post-heading .meta { 873 | line-height: 1.1; 874 | display: block; 875 | } 876 | .easy-intro-header .post-heading .subheading { 877 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 878 | line-height: 1.7; 879 | font-size: 17px; 880 | font-weight: normal; 881 | margin: 10px 0 30px; 882 | margin-top: -5px; 883 | } 884 | .easy-intro-header .post-heading .meta { 885 | font-family: 'Lora', 'Times New Roman', serif; 886 | font-style: italic; 887 | font-weight: 300; 888 | font-size: 17px; 889 | } 890 | .easy-intro-header .post-heading .meta a { 891 | color: white; 892 | } 893 | 894 | .easy-menu-fixed-top .easy-menu-collapse, 895 | .easy-menu-fixed-bottom .easy-menu-collapse { 896 | max-height: 340px; 897 | } 898 | 899 | 900 | 901 | @media (min-width: 768px) { 902 | .easy-menu { 903 | border-radius: 4px; 904 | } 905 | .easy-menu-header { 906 | float: left; 907 | } 908 | .easy-menu-collapse { 909 | width: auto; 910 | border-top: 0; 911 | box-shadow: none; 912 | } 913 | .easy-menu-collapse.collapse { 914 | display: block !important; 915 | height: auto !important; 916 | padding-bottom: 0; 917 | overflow: visible !important; 918 | visibility: visible !important; 919 | } 920 | .easy-menu-collapse.in { 921 | overflow-y: visible; 922 | } 923 | .easy-menu-fixed-top .easy-menu-collapse, 924 | .easy-menu-static-top .easy-menu-collapse, 925 | .easy-menu-fixed-bottom .easy-menu-collapse { 926 | padding-right: 0; 927 | padding-left: 0; 928 | } 929 | .easy-menu-right .dropdown-menu { 930 | right: 0; 931 | left: auto; 932 | } 933 | .easy-menu-right .dropdown-menu-left { 934 | right: auto; 935 | left: 0; 936 | } 937 | .easy-top-intro-container { 938 | width: 750px; 939 | } 940 | 941 | .easy-menu-header, 942 | .easy-menu-menu-fluid > .easy-menu-header, 943 | .easy-menu-collapse, 944 | .easy-menu-menu-fluid > .easy-menu-collapse { 945 | margin-right: 0; 946 | margin-left: 0; 947 | } 948 | .easy-menu-fixed-top, 949 | .easy-menu-fixed-bottom { 950 | border-radius: 0; 951 | } 952 | .easy-menu > .easy-menu-brand, 953 | .easy-menu > .easy-menu-menu-fluid .easy-menu-brand { 954 | margin-left: -15px; 955 | } 956 | .easy-menu-toggle { 957 | display: none; 958 | } 959 | .easy-menu-nav { 960 | float: left; 961 | margin: 0; 962 | } 963 | .easy-menu-nav > li { 964 | float: left; 965 | } 966 | .easy-menu-nav > li > a { 967 | padding-top: 15px; 968 | padding-bottom: 15px; 969 | } 970 | .easy-menu-form { 971 | width: auto; 972 | padding-top: 0; 973 | padding-bottom: 0; 974 | margin-right: 0; 975 | margin-left: 0; 976 | border: 0; 977 | box-shadow: none; 978 | } 979 | .easy-menu-form .form-group { 980 | display: inline-block; 981 | margin-bottom: 0; 982 | vertical-align: middle; 983 | } 984 | .easy-menu-form .form-control { 985 | display: inline-block; 986 | width: auto; 987 | vertical-align: middle; 988 | } 989 | .easy-menu-form .form-control-static { 990 | display: inline-block; 991 | } 992 | .easy-menu-form .input-group { 993 | display: inline-table; 994 | vertical-align: middle; 995 | } 996 | .easy-menu-form .input-group .input-group-addon, 997 | .easy-menu-form .input-group .input-group-btn, 998 | .easy-menu-form .input-group .form-control { 999 | width: auto; 1000 | } 1001 | .easy-menu-form .input-group > .form-control { 1002 | width: 100%; 1003 | } 1004 | .easy-menu-form .control-label { 1005 | margin-bottom: 0; 1006 | vertical-align: middle; 1007 | } 1008 | .easy-menu-form .radio, 1009 | .easy-menu-form .checkbox { 1010 | display: inline-block; 1011 | margin-top: 0; 1012 | margin-bottom: 0; 1013 | vertical-align: middle; 1014 | } 1015 | .easy-menu-form .radio label, 1016 | .easy-menu-form .checkbox label { 1017 | padding-left: 0; 1018 | } 1019 | .easy-menu-form .radio input[type="radio"], 1020 | .easy-menu-form .checkbox input[type="checkbox"] { 1021 | position: relative; 1022 | margin-left: 0; 1023 | } 1024 | .easy-menu-form .has-feedback .form-control-feedback { 1025 | top: 0; 1026 | } 1027 | .easy-menu-text { 1028 | float: left; 1029 | margin-right: 15px; 1030 | margin-left: 15px; 1031 | } 1032 | .easy-menu-left { 1033 | float: left !important; 1034 | } 1035 | .easy-menu-right { 1036 | float: right !important; 1037 | margin-right: -15px; 1038 | } 1039 | .easy-menu-right ~ .easy-menu-right { 1040 | margin-right: 0; 1041 | } 1042 | } 1043 | @media (min-width: 992px) { 1044 | .easy-top-intro-container { 1045 | width: 970px; 1046 | } 1047 | .easy-article-list { 1048 | width: 85%; 1049 | } 1050 | } 1051 | @media (min-width: 1200px) { 1052 | .easy-top-intro-container { 1053 | width: 1170px; 1054 | } 1055 | .easy-article-list { 1056 | width: 80%; 1057 | margin: 40px auto; 1058 | } 1059 | } 1060 | 1061 | @media only screen and (min-width: 768px) { 1062 | 1063 | .easy-menu-custom { 1064 | background: transparent; 1065 | border-bottom: 1px solid transparent; 1066 | } 1067 | .easy-menu-custom body { 1068 | font-size: 20px; 1069 | } 1070 | .easy-menu-custom .easy-menu-brand { 1071 | color: white; 1072 | padding: 20px; 1073 | } 1074 | .easy-menu-custom .easy-menu-brand:hover, 1075 | .easy-menu-custom .easy-menu-brand:focus { 1076 | color: rgba(255, 255, 255, 0.8); 1077 | } 1078 | .easy-menu-custom .nav li a { 1079 | color: white; 1080 | padding: 20px; 1081 | } 1082 | .easy-menu-custom .nav li a:hover, 1083 | .easy-menu-custom .nav li a:focus { 1084 | color: rgba(255, 255, 255, 0.8); 1085 | } 1086 | .easy-intro-header { 1087 | margin-bottom: 20px; 1088 | /* response on desktop */ 1089 | } 1090 | } 1091 | 1092 | @media (max-device-width: 480px) and (orientation: landscape) { 1093 | .easy-menu-fixed-top .easy-menu-collapse, 1094 | .easy-menu-fixed-bottom .easy-menu-collapse { 1095 | max-height: 200px; 1096 | } 1097 | } -------------------------------------------------------------------------------- /app/web/asset/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/web/asset/images/favicon.ico -------------------------------------------------------------------------------- /app/web/asset/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/web/asset/images/loading.gif -------------------------------------------------------------------------------- /app/web/asset/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/web/asset/images/logo.png -------------------------------------------------------------------------------- /app/web/asset/images/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/web/asset/images/menu.png -------------------------------------------------------------------------------- /app/web/asset/images/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/app/web/asset/images/react.png -------------------------------------------------------------------------------- /app/web/component/header/index.css: -------------------------------------------------------------------------------- 1 | 2 | .header { 3 | height: 80px; 4 | background-color: #20a0ff; 5 | color: #fff; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 80px; 10 | line-height: 80px; 11 | z-index: 100; 12 | position: relative 13 | } 14 | 15 | .header .nav:after, .header .nav:before { 16 | display: table; 17 | content: "" 18 | } 19 | 20 | .header .nav:after { 21 | clear: both 22 | } 23 | 24 | 25 | .header .container { 26 | height: 100%; 27 | box-sizing: border-box; 28 | width: 1140px; 29 | padding: 0 30px; 30 | margin: 0 auto; 31 | } 32 | 33 | .header h1 { 34 | float: left; 35 | font-size: 32px; 36 | font-weight: 400 37 | } 38 | 39 | .header h1 a { 40 | color: #fff; 41 | text-decoration: none; 42 | display: block 43 | } 44 | 45 | .header h1 span { 46 | font-size: 12px; 47 | display: inline-block; 48 | width: 34px; 49 | height: 18px; 50 | border: 1px solid hsla(0, 0%, 100%, .5); 51 | text-align: center; 52 | line-height: 18px; 53 | vertical-align: middle; 54 | margin-left: 10px; 55 | border-radius: 3px 56 | } 57 | 58 | .header .nav { 59 | float: right; 60 | height: 100%; 61 | line-height: 80px; 62 | background: transparent; 63 | padding: 0; 64 | margin: 0 65 | } 66 | 67 | .header .nav-logo-small, .header .nav-logo { 68 | vertical-align: sub 69 | } 70 | 71 | .header .nav-logo-small { 72 | display: none 73 | } 74 | 75 | .header .nav-item { 76 | margin: 0; 77 | float: left; 78 | list-style: none; 79 | position: relative; 80 | cursor: pointer; 81 | margin-left: 20px 82 | } 83 | 84 | .header .nav-item a { 85 | text-decoration: none; 86 | color: #fff; 87 | display: block; 88 | padding: 0 20px; 89 | opacity: .8 90 | } 91 | 92 | .header .nav-item a.active, .header .nav-item a:hover { 93 | opacity: 1 94 | } 95 | 96 | .header .nav-item a.active { 97 | font-weight: 700 98 | } 99 | 100 | .header .nav-item a.active:before { 101 | content: ""; 102 | display: block; 103 | position: absolute; 104 | bottom: 0; 105 | left: 0; 106 | width: 100%; 107 | height: 4px; 108 | background: #99d2fc 109 | } 110 | 111 | .header .nav-item:last-child { 112 | cursor: default; 113 | margin-left: 34px 114 | } 115 | 116 | .header .nav-item:last-child span { 117 | opacity: .8 118 | } 119 | 120 | .header .nav-item:last-child .nav-lang { 121 | cursor: pointer; 122 | display: inline-block; 123 | height: 100% 124 | } 125 | 126 | .header .nav-item:last-child .nav-lang:hover { 127 | opacity: 1 128 | } 129 | 130 | .header .nav-item:last-child .nav-lang.active { 131 | font-weight: 700; 132 | opacity: 1 133 | } 134 | 135 | .header-home { 136 | position: fixed; 137 | top: 0; 138 | background-color: rgba(32, 160, 255, 0) 139 | } 140 | 141 | @media (max-width: 850px) { 142 | .header .nav-logo { 143 | display: none 144 | } 145 | 146 | .header .nav-logo-small { 147 | display: inline-block 148 | } 149 | 150 | .header .nav-item { 151 | margin-left: 6px 152 | } 153 | 154 | .header .nav-item a { 155 | padding: 0 5px 156 | } 157 | 158 | .header .nav-item:last-child { 159 | margin-left: 10px 160 | } 161 | } 162 | 163 | @media (max-width: 700px) { 164 | .header .container { 165 | padding: 0 12px 166 | } 167 | 168 | .header .nav-item a, .header .nav-lang { 169 | font-size: 12px; 170 | vertical-align: top 171 | } 172 | } -------------------------------------------------------------------------------- /app/web/component/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './index.css'; 3 | export default class Header extends Component { 4 | render() { 5 | return
6 |

7 | Egg + React + TypeScript

8 |
9 |
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/web/component/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | export default class Layout extends Component { 3 | render() { 4 | if(EASY_ENV_IS_NODE) { 5 | return 6 | 7 | {this.props.title} 8 | 9 | 10 | 11 | 12 | 13 | 14 |
{this.props.children}
15 | ; 16 | } 17 | return this.props.children; 18 | } 19 | } -------------------------------------------------------------------------------- /app/web/framework/app.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-style */ 2 | 3 | import React, { Component } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { matchRoutes } from 'react-router-config'; 6 | 7 | async function asyncData(locals, router) { 8 | const url = locals.url; 9 | const matchRouteList = matchRoutes(router, url); 10 | const promises = matchRouteList.map(matchRoute=> { 11 | const componentAsyncData = matchRoute.route.component.asyncData; 12 | return componentAsyncData instanceof Function ? componentAsyncData(locals, matchRoute) : null; 13 | }); 14 | const list = await Promise.all(promises); 15 | return list.reduce((item: Object, result : Object) => { 16 | return { ...result, ...item} 17 | }, {}); 18 | } 19 | 20 | function bootstrap(Entry) { 21 | if (EASY_ENV_IS_BROWSER) { 22 | const root = document.getElementById('app'); 23 | const renderMethod = root.childNodes.length > 0 ? 'hydrate' : 'render'; 24 | ReactDOM[renderMethod](, root); 25 | if (EASY_ENV_IS_DEV) { 26 | module.hot.accept(() => { 27 | ReactDOM[renderMethod](, root); 28 | }); 29 | } 30 | return; 31 | } 32 | return Entry; 33 | } 34 | 35 | export { 36 | asyncData, 37 | bootstrap 38 | } 39 | -------------------------------------------------------------------------------- /app/web/framework/request.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import axios from 'axios'; 3 | // axios.defaults.baseURL = 'http://127.0.0.1:7001'; 4 | axios.defaults.timeout = 15000; 5 | axios.defaults.xsrfHeaderName = 'x-csrf-token'; 6 | axios.defaults.xsrfCookieName = 'csrfToken'; 7 | export default { 8 | async post(url, json, locals = {}) { 9 | const headers = {}; 10 | if (EASY_ENV_IS_NODE) { 11 | headers['x-csrf-token'] = locals.csrf; 12 | headers.Cookie = `csrfToken=${locals.csrf}`; 13 | } 14 | const res = await axios.post(`${locals.origin}${url}`, json, { headers }); 15 | return res.data; 16 | }, 17 | async get(url, locals = {}) { 18 | const res = await axios.get(`${locals.origin}${url}`); 19 | return res.data; 20 | } 21 | }; -------------------------------------------------------------------------------- /app/web/page/antd/component/mobx/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, CSSProperties } from 'react'; 2 | import { observable } from 'mobx'; 3 | import { Observer, observer, inject } from 'mobx-react'; 4 | import AppStore from './store'; 5 | import { StoreProps } from '../../../../typings/type'; 6 | 7 | 8 | const info = observable({ text: "Mobx Observer Local Test" }); 9 | 10 | class LocalReactive extends Component { 11 | 12 | style: CSSProperties = { 13 | textAlign: 'center', 14 | fontSize: '30px' 15 | } 16 | 17 | handle() { 18 | info.text = `${info.text } Local Update`; 19 | console.log('click'); 20 | } 21 | 22 | render() { 23 | return
24 |
{info.text}
25 | {() =>
{ this.handle()}}>点我:{info.text}
}
26 |
27 | } 28 | } 29 | 30 | @inject('configStore') 31 | @observer 32 | class MobXApp extends Component { 33 | store: AppStore; 34 | constructor(props) { 35 | super(props); 36 | this.store = new AppStore(); 37 | } 38 | 39 | render() { 40 | const { configStore } = this.props; 41 | // [mobx] Dynamic observable objects cannot be frozen https://github.com/mobxjs/mobx/blob/master/CHANGELOG.md 42 | return
43 |
{ this.store.plus()}}>点我:{this.store.number}
44 | 45 |
46 | } 47 | }; 48 | export default MobXApp; -------------------------------------------------------------------------------- /app/web/page/antd/component/mobx/store.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import { observable, action } from 'mobx'; 3 | class AppStore { 4 | // 定义状态并使其可观察 5 | @observable number = 0; 6 | @observable size = 24; 7 | @observable style: CSSProperties ={ 8 | textAlign: 'center', 9 | marginTop: '40px', 10 | fontSize: `${this.size}px` 11 | } 12 | 13 | plus(){ 14 | this.number++; 15 | this.style = { 16 | ...this.style, 17 | fontSize: `${this.size++}px` 18 | } 19 | } 20 | } 21 | export default AppStore; -------------------------------------------------------------------------------- /app/web/page/antd/component/tab/index.css: -------------------------------------------------------------------------------- 1 | .tab { 2 | 3 | } 4 | .tab h1 { 5 | text-align: center 6 | } -------------------------------------------------------------------------------- /app/web/page/antd/component/tab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Tabs } from 'antd'; 3 | import Header from '../../../../component/header'; 4 | import { TabProps } from '../../../../typings/type'; 5 | import './index.css'; 6 | 7 | import MobXApp from '../mobx'; 8 | 9 | 10 | const TabPane = Tabs.TabPane; 11 | const tabItemClick = (key) => { 12 | console.log('tab click', key); 13 | }; 14 | // https://github.com/gaearon/react-hot-loader/issues/525 15 | // must export, not export default 16 | export class Tab extends Component { 17 | render() { 18 | return
19 |
20 |
21 |

{this.props.message.text}

22 | 23 | 24 | Content of Tab Pane 2 25 | Content of Tab Pane 3 26 | 27 |
28 |
; 29 | } 30 | } -------------------------------------------------------------------------------- /app/web/page/antd/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component, ReactElement } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { AppContainer } from 'react-hot-loader'; 5 | import { Provider, useStaticRendering } from 'mobx-react'; 6 | import ConfigStore from './store/config'; 7 | import Layout from '@component/layout'; 8 | // https://github.com/gaearon/react-hot-loader/issues/525 9 | import { Tab } from './component/tab'; 10 | import { TabProps } from '../../typings/type'; 11 | 12 | class App extends Component { 13 | render() { 14 | const stores = { 15 | configStore: new ConfigStore() 16 | }; 17 | return ; 18 | } 19 | } 20 | 21 | function bootstrap() { 22 | if (EASY_ENV_IS_NODE) { 23 | useStaticRendering(true); 24 | return App; 25 | } 26 | const stores = window.stores = window.stores || { 27 | configStore: new ConfigStore() 28 | }; 29 | const state = window.__INITIAL_STATE__; 30 | const root = document.getElementById('app'); 31 | if (EASY_ENV_IS_DEV) { 32 | ReactDOM.hydrate(, root); 33 | if (module.hot) { 34 | module.hot.accept(); 35 | } 36 | } else{ 37 | ReactDOM.hydrate(, root); 38 | } 39 | } 40 | 41 | export default bootstrap(); -------------------------------------------------------------------------------- /app/web/page/antd/store/config.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import { observable, action } from 'mobx'; 3 | class ConfigStore { 4 | // 定义状态并使其可观察 5 | @observable size = 2; 6 | @observable themeStyle: CSSProperties ={ 7 | backgroundColor: '#red', 8 | fontSize: `${this.size}px` 9 | } 10 | 11 | changeTheme(){ 12 | this.themeStyle = { 13 | ...this.themeStyle, 14 | fontSize: `${this.size++}px` 15 | } 16 | } 17 | } 18 | export default ConfigStore; -------------------------------------------------------------------------------- /app/web/page/blog/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { BrowserRouter, StaticRouter } from 'react-router-dom'; 4 | import { asyncData, bootstrap } from '@framework/app'; 5 | import createStore from './store'; 6 | import createRouter from './router'; 7 | import Main from './view/main'; 8 | import '@asset/css/blog.css'; 9 | 10 | class Entry extends Component { 11 | static async asyncData(context) { 12 | const router = createRouter(); 13 | return asyncData(context, router); 14 | } 15 | 16 | render() { 17 | if (EASY_ENV_IS_BROWSER) { 18 | const store = createStore(window.__INITIAL_STATE__); 19 | const { url } = store.getState(); 20 | return 21 | 22 |
23 |
24 |
; 25 | } 26 | const store = createStore(this.props); 27 | const { url } = store.getState(); 28 | return 29 | 30 |
31 |
32 |
; 33 | } 34 | } 35 | 36 | export default bootstrap(Entry); -------------------------------------------------------------------------------- /app/web/page/blog/router/about.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import React, { Component } from 'react'; 3 | import { hot } from 'react-hot-loader/root' 4 | 5 | class About extends Component { 6 | 7 | render() { 8 | const styleTitle: any = { 9 | marginTop: '40px', marginBottom: '40px', textAlign: 'center' 10 | }; 11 | const styleSub: any = { 12 | marginBottom: '40px', textAlign: 'center', color: '444444' 13 | }; 14 | return ; 20 | } 21 | } 22 | 23 | export default EASY_ENV_IS_DEV ? hot(About) : About 24 | -------------------------------------------------------------------------------- /app/web/page/blog/router/async.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Loadable from 'react-loadable'; 3 | import Loading from '../widget/loading'; 4 | 5 | const AsyncImageLoadableComponent = Loadable({ 6 | loader: () => import('../widget/async-image'), 7 | loading: Loading 8 | }); 9 | 10 | export default class Async extends Component { 11 | 12 | render() { 13 | return
14 |

Egg React Server Side Render

15 |

Egg + React + Redux + React Router SPA Server Side + Webpack Render Example

16 |
17 |
; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/web/page/blog/router/detail.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { hot } from 'react-hot-loader/root' 6 | 7 | import request from '../../../framework/request'; 8 | 9 | class Detail extends Component { 10 | static async asyncData(locals, route) { 11 | const id = route.match.params.id; 12 | return request.get(`/api/blog/${id}`, locals); 13 | } 14 | 15 | render() { 16 | const { article } = this.props; 17 | return article ?
18 |

{article.title}

19 |
20 | 21 |
22 |
: null; 23 | } 24 | } 25 | 26 | 27 | const mapStateToProps = state => { 28 | return { 29 | article: state.article 30 | }; 31 | }; 32 | 33 | export default connect(mapStateToProps)(EASY_ENV_IS_DEV ? hot(Detail) : Detail) 34 | -------------------------------------------------------------------------------- /app/web/page/blog/router/example.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import React, { Component } from 'react'; 3 | import { hot } from 'react-hot-loader/root' 4 | 5 | class Example extends Component { 6 | 7 | render() { 8 | const styleTitle = { 9 | marginTop: '40px', marginBottom: '40px', textAlign: 'center' 10 | }; 11 | const styleSub = { 12 | marginBottom: '40px', textAlign: 'center', color: '444444' 13 | }; 14 | return
15 |

Egg React Server Side Render

16 |

Node 直接获取数据

17 |

前端 asyncData 获取数据

18 |

AntD + Mobx

19 | 20 |
; 21 | } 22 | } 23 | 24 | export default EASY_ENV_IS_DEV ? hot(Example) : Example 25 | 26 | -------------------------------------------------------------------------------- /app/web/page/blog/router/home.css: -------------------------------------------------------------------------------- 1 | .ant-menu-horizontal { 2 | text-align: center; 3 | } 4 | 5 | .ant-menu-item > a:hover { 6 | text-decoration: none; 7 | } 8 | 9 | .easy-article-detail-title { 10 | text-align: center; 11 | margin: 24px auto; 12 | } 13 | 14 | .easy-article-list { 15 | width: 90%; 16 | margin: 40px auto; 17 | } 18 | 19 | .easy-article-list > li { 20 | margin-top: 40px; 21 | color: #404040; 22 | border-bottom: 1px solid #cecece; 23 | } 24 | .easy-article-list > h2 { 25 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "STHeiti", "Microsoft YaHei", "WenQuanYi Micro Hei", SimSun, sans-serif; 26 | line-height: 1.1; 27 | font-size: 30px; 28 | } 29 | 30 | .easy-article-summary { 31 | font-size: 13px; 32 | font-style: normal; 33 | color: #a3a3a3; 34 | } 35 | 36 | .easy-article-info { 37 | font-family: 'Lora', 'Times New Roman', serif; 38 | color: #808080; 39 | font-size: 16px; 40 | font-style: italic; 41 | margin-top: 0; 42 | } 43 | 44 | .easy-article-item { 45 | margin: 0px 0px 40px 0px; 46 | } 47 | 48 | .easy-article-item> a { 49 | color: #404040; 50 | } 51 | .easy-article-item> a:hover, 52 | .easy-article-item> a:focus { 53 | text-decoration: none; 54 | color: #0666C5; 55 | } 56 | .easy-article-item> a > .easy-article-title { 57 | font-size: 21px; 58 | line-height: 1.3; 59 | margin-top: 30px; 60 | margin-bottom: 8px; 61 | } 62 | .easy-article-item> a > .article-subtitle { 63 | font-size: 15px; 64 | margin: 0; 65 | font-weight: 300; 66 | margin-bottom: 10px; 67 | } 68 | .easy-article-item> .easy-article-meta { 69 | font-family: 'Lora', 'Times New Roman', serif; 70 | color: #808080; 71 | font-size: 16px; 72 | font-style: italic; 73 | margin-top: 0; 74 | } 75 | 76 | .easy-article-item> .easy-article-meta > a:hover, 77 | .easy-article-item> .easy-article-meta > a:focus { 78 | color: #2B4180; 79 | text-decoration: underline; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /app/web/page/blog/router/home.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { hot } from 'react-hot-loader/root' 5 | 6 | import request from '../../../framework/request'; 7 | 8 | import './home.css' 9 | class Home extends Component { 10 | static async asyncData(context, route) { 11 | return request.get('/api/blog/list', context); 12 | } 13 | 14 | render() { 15 | const { list = [] } = this.props; 16 | return
17 |
    18 | {list.map(function (item) { 19 | return
  • 20 |

    21 | {item.title} 22 |

    23 |
    {item.summary}
    24 |
    25 | Word Count:{item.wordCount} 26 | Create Time: {item.createTime} 27 |
    28 |
  • ; 29 | })} 30 |
31 |
; 32 | } 33 | } 34 | 35 | 36 | const mapStateToProps = state => { 37 | return { 38 | list: state.list 39 | }; 40 | }; 41 | 42 | 43 | const mapDispatchToProps = dispatch => { 44 | return {}; 45 | }; 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(EASY_ENV_IS_DEV ? hot(Home) : Home); 48 | -------------------------------------------------------------------------------- /app/web/page/blog/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from 'react-router-dom'; 2 | import Home from './home'; 3 | import Async from './async'; 4 | import Example from './example'; 5 | import Detail from './detail'; 6 | import About from './about'; 7 | const NotFound = () => { 8 | return ( 9 | { 10 | if (staticContext) { 11 | staticContext.status = 404; 12 | } 13 | return ( 14 |
15 |

404 : Not Found

16 |
17 | ); 18 | }}/> 19 | ); 20 | }; 21 | 22 | 23 | export default function createRouter() { 24 | return [ 25 | { 26 | path: '/detail/:id', 27 | component: Detail 28 | }, 29 | { 30 | path: '/async', 31 | component: Async 32 | }, 33 | { 34 | path: '/example', 35 | component: Example 36 | }, 37 | { 38 | path: '/about', 39 | component: About 40 | }, 41 | { 42 | path: '/', 43 | component: Home 44 | }, 45 | { 46 | path: '*', 47 | component: Home 48 | } 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /app/web/page/blog/router/route.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import { Route } from 'react-router-dom'; 5 | import { connect } from 'react-redux'; 6 | import { update } from '../store/actions'; 7 | 8 | class WrapperRoute extends Component { 9 | 10 | async componentWillReceiveProps(nextProps) { 11 | const { type, locals, component, computedMatch, updateState } = nextProps; 12 | console.log('>>route', computedMatch.url, this.props.computedMatch.url); 13 | if (computedMatch.url !== this.props.computedMatch.url) { 14 | const { asyncData } = component; 15 | if (asyncData) { 16 | const data = await asyncData(locals, { match: computedMatch }); 17 | console.log('>>>update', type, data); 18 | updateState(type, data); 19 | } 20 | } 21 | } 22 | 23 | render() { 24 | return 25 | } 26 | } 27 | 28 | const mapStateToProps = state => { 29 | return { 30 | locals: state 31 | } 32 | }; 33 | 34 | const mapDispatchToProps = dispatch => { 35 | return { 36 | updateState: (type, data) => dispatch(update(type, data)) 37 | }; 38 | }; 39 | 40 | export default connect(mapStateToProps, mapDispatchToProps)(WrapperRoute); 41 | -------------------------------------------------------------------------------- /app/web/page/blog/store/actions.ts: -------------------------------------------------------------------------------- 1 | export const update = (type, data) => { 2 | return { 3 | type, 4 | data 5 | }; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /app/web/page/blog/store/constant.ts: -------------------------------------------------------------------------------- 1 | export const ARTICLE_LIST = 'ARTICLE_LIST'; 2 | export const ARTICLE_DETAIL = 'ARTICLE_DETAIL'; 3 | export const ADD = 'ADD'; 4 | export const DEL = 'DEL'; 5 | -------------------------------------------------------------------------------- /app/web/page/blog/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import reducers from './reducers'; 3 | 4 | export default function(initalState){ 5 | return createStore(reducers, initalState); 6 | } 7 | -------------------------------------------------------------------------------- /app/web/page/blog/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { ARTICLE_DETAIL, ARTICLE_LIST, ADD, DEL } from './constant'; 2 | 3 | export default function update(state, action) { 4 | const newState = Object.assign({}, state); 5 | if (action.type === ADD) { 6 | const list = Array.isArray(action.data) ? action.data : [action.data]; 7 | newState.list = [...newState.list, ...list]; 8 | } else if (action.type === DEL) { 9 | newState.list = newState.list.filter(data => { 10 | return data.id !== action.id; 11 | }); 12 | } else if (action.type === ARTICLE_LIST) { 13 | newState.list = action.data.list; 14 | newState.total = action.data.total; 15 | } else if (action.type === ARTICLE_DETAIL) { 16 | newState.article = action.data.article; 17 | } 18 | return newState; 19 | } 20 | -------------------------------------------------------------------------------- /app/web/page/blog/view/main.css: -------------------------------------------------------------------------------- 1 | .menu-tab { 2 | margin: 8px auto; 3 | border-bottom: 1px solid #0a52a5; 4 | height: 40px; 5 | } 6 | 7 | .menu-tab li { 8 | height: 40px; 9 | line-height: 40px; 10 | vertical-align: middle; 11 | float: left; 12 | width: 25%; 13 | text-align: center; 14 | } -------------------------------------------------------------------------------- /app/web/page/blog/view/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, Switch } from 'react-router-dom'; 3 | import { hot } from 'react-hot-loader/root' 4 | import { ARTICLE_LIST, ARTICLE_DETAIL } from '../store/constant'; 5 | import Layout from '../../../component/layout' 6 | import Header from '../../../component/header' 7 | import Route from '../router/route'; 8 | import Home from '../router/home'; 9 | import Detail from '../router/detail'; 10 | import Example from '../router/example'; 11 | import Async from '../router/async'; 12 | import About from '../router/about'; 13 | import './main.css'; 14 | 15 | class Main extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { current: props.url }; 20 | } 21 | 22 | tabClick(e) { 23 | console.log('click', e.target); 24 | } 25 | 26 | render() { 27 | return 28 |
29 |
    30 |
  • Home
  • 31 |
  • Async
  • 32 |
  • Example
  • 33 |
  • About
  • 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
; 43 | } 44 | } 45 | 46 | export default EASY_ENV_IS_DEV ? hot(Main) : Main; 47 | -------------------------------------------------------------------------------- /app/web/page/blog/widget/async-image.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Logo from '../../../asset/images/react.png'; 3 | export default class AsyncImage extends Component { 4 | render() { 5 | return
这是异步加载的内容
6 | } 7 | } -------------------------------------------------------------------------------- /app/web/page/blog/widget/loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | export default class Loading extends Component { 3 | render() { 4 | return
Loading.....
5 | } 6 | } -------------------------------------------------------------------------------- /app/web/page/demo/async.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import { hot } from 'react-hot-loader/root' 5 | 6 | import request from '@framework/request'; 7 | 8 | class AsyncMode extends Component { 9 | static async asyncData(locals) { 10 | return request.get(`/demo/api/article`, locals); 11 | } 12 | 13 | render() { 14 | const { title, article } = this.props; 15 | return article ?
16 |

{title}

17 |

{article.title}

18 |
19 | 20 |
21 |
: null; 22 | } 23 | } 24 | 25 | export default EASY_ENV_IS_DEV ? hot(AsyncMode) : AsyncMode; 26 | -------------------------------------------------------------------------------- /app/web/page/demo/node.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import { hot } from 'react-hot-loader/root' 5 | 6 | class NodeMode extends Component { 7 | render() { 8 | const { title, article } = this.props; 9 | return article ?
10 |

{title}

11 |

{article.title}

12 |
13 | 14 |
15 |
: null; 16 | } 17 | } 18 | 19 | export default EASY_ENV_IS_DEV ? hot(NodeMode) : NodeMode; 20 | -------------------------------------------------------------------------------- /app/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "target": "es5", 6 | "module": "esnext", 7 | "sourceMap": true, 8 | "lib": [ 9 | "es6", 10 | "dom", 11 | "es2017", 12 | "esnext" 13 | ], 14 | "baseUrl": "./", 15 | "paths": { 16 | "@asset/*": ["asset/*"], 17 | "@component/*": ["component/*"], 18 | "@framework/*": ["framework/*"] 19 | } 20 | }, 21 | "files": [ 22 | "./typings/global.d.ts" 23 | ], 24 | "include": [ 25 | "./page/**/**.tsx", "page/antd/store/config.ts", 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "**/*.spec.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /app/web/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var EASY_ENV_IS_NODE: boolean; 2 | declare var EASY_ENV_IS_DEV: boolean; 3 | declare var EASY_ENV_IS_BROWSER: boolean; 4 | declare var process: { 5 | env: { 6 | NODE_ENV: string 7 | } 8 | }; 9 | interface Window { 10 | __INITIAL_STATE__: any; 11 | stores: any; 12 | } 13 | interface NodeModule { 14 | hot: { 15 | accept: any; 16 | } 17 | } -------------------------------------------------------------------------------- /app/web/typings/type.ts: -------------------------------------------------------------------------------- 1 | import ConfigStore from '../page/antd/store/config'; 2 | 3 | export interface TabProps { 4 | title: string; 5 | keywords: string; 6 | description: string; 7 | message: { 8 | text: string 9 | }; 10 | } 11 | 12 | export interface StoreProps { 13 | configStore?: ConfigStore 14 | } -------------------------------------------------------------------------------- /app/web/view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Egg + React 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": { 4 | "presets": [ 5 | "@babel/preset-react", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "modules": false, 10 | "targets": { 11 | "node": "current" 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | "@babel/plugin-syntax-dynamic-import", 18 | "@babel/plugin-proposal-object-rest-spread" 19 | ] 20 | }, 21 | "web": { 22 | "presets": [ 23 | "@babel/preset-react", 24 | [ 25 | "@babel/preset-env", 26 | { 27 | "modules": false, 28 | "targets": { 29 | "browsers": [ 30 | "last 2 versions", 31 | "safari >= 8" 32 | ] 33 | } 34 | } 35 | ] 36 | ], 37 | "plugins": [ 38 | "react-hot-loader/babel", 39 | "@babel/plugin-syntax-dynamic-import", 40 | "@babel/plugin-proposal-object-rest-spread", 41 | [ 42 | "import", 43 | { 44 | "libraryName": "antd", 45 | "libraryDirectory": "lib", 46 | "style": true 47 | } 48 | ] 49 | ] 50 | } 51 | }, 52 | "comments": false 53 | } 54 | -------------------------------------------------------------------------------- /config/config.default.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { EggAppConfig } from 'egg'; 4 | export default function(app: EggAppConfig) { 5 | const exports: any = {}; 6 | 7 | exports.siteFile = { 8 | '/favicon.ico': fs.readFileSync(path.join(app.baseDir, 'app/web/asset/images/favicon.ico')) 9 | }; 10 | 11 | exports.logger = { 12 | consoleLevel: 'DEBUG', 13 | dir: path.join(app.baseDir, 'logs') 14 | }; 15 | 16 | exports.static = { 17 | prefix: '/public/', 18 | dir: path.join(app.baseDir, 'public') 19 | }; 20 | 21 | exports.keys = '123456'; 22 | 23 | exports.middleware = [ 24 | 'locals', 25 | // 'access' 26 | ]; 27 | 28 | exports.reactssr = { 29 | layout: path.join(app.baseDir, 'app/web/view/layout.html') 30 | }; 31 | 32 | return exports; 33 | } 34 | -------------------------------------------------------------------------------- /config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig } from 'egg'; 2 | import * as path from 'path'; 3 | import { getWebpackConfig } from 'easywebpack-react' 4 | 5 | export default (app: EggAppConfig) => { 6 | const config: any = {}; 7 | 8 | config.view = { 9 | cache: 0 10 | }; 11 | 12 | config.static = { 13 | maxAge: 0 // maxAge 缓存,默认 1 年 14 | }; 15 | 16 | config.development = { 17 | watchDirs: ['app'], // 指定监视的目录(包括子目录),当目录下的文件变化的时候自动重载应用,路径从项目根目录开始写 18 | ignoreDirs: ['app/web', 'public', 'config/manifest.json'] // 指定过滤的目录(包括子目录) 19 | }; 20 | 21 | config.logview = { 22 | dir: path.join(app.baseDir, 'logs') 23 | }; 24 | 25 | config.webpack = { 26 | browser: true, 27 | webpackConfigList: getWebpackConfig() 28 | }; 29 | 30 | return config; 31 | }; 32 | -------------------------------------------------------------------------------- /config/config.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生产环境配置 3 | * 4 | * 最终生效的配置为 prod + default(前者覆盖后者) 5 | */ 6 | 7 | import * as path from 'path'; 8 | import * as fs from 'fs'; 9 | export default function(app) { 10 | const exports: any = {}; 11 | 12 | return exports; 13 | } 14 | -------------------------------------------------------------------------------- /config/config.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | export default function(app) { 5 | const exports: any = {}; 6 | 7 | return exports; 8 | } 9 | -------------------------------------------------------------------------------- /config/plugin.local.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cors: { 3 | package: 'egg-cors' 4 | }, 5 | webpack: { 6 | package: 'egg-webpack' 7 | }, 8 | webpackreact : { 9 | package: 'egg-webpack-react' 10 | } 11 | }; -------------------------------------------------------------------------------- /config/plugin.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | reactssr: { 3 | package: 'egg-view-react-ssr' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | // "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | // "outDir": "./", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | "strictFunctionTypes": false, /* Enable strict checking of function types. */ 26 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | 29 | /* Additional Checks */ 30 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 31 | "noUnusedParameters": false, /* Report errors on unused parameters. */ 32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 34 | 35 | /* Module Resolution Options */ 36 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 37 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 38 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 39 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 40 | // "typeRoots": [], /* List of folders to include type definitions from. */ 41 | // "types": [], /* Type declaration files to be included in compilation. */ 42 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 43 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 44 | 45 | /* Source Map Options */ 46 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 47 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 48 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 49 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 50 | "experimentalDecorators": true, 51 | "emitDecoratorMetadata": true 52 | } 53 | } -------------------------------------------------------------------------------- /docs/images/egg-webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/docs/images/egg-webpack.png -------------------------------------------------------------------------------- /docs/images/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-team/egg-react-typescript-boilerplate/b3714e07df7f31c293d26f3b6d13293a133ee688/docs/images/webpack.png -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/perform.md: -------------------------------------------------------------------------------- 1 | http://www.jianshu.com/p/1dffe3126686 2 | 3 | http://alexkuz.github.io/webpack-chart/ 4 | http://webpack.github.io/analyse/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-react-typescript-boilerplate", 3 | "version": "5.1.2", 4 | "description": "基于 egg + react + typescript + webpack 服务端渲染工程骨架项目", 5 | "scripts": { 6 | "dev": "egg-bin dev -r egg-ts-helper/register", 7 | "debug": "egg-bin debug", 8 | "build": "npm run tsc && easy build", 9 | "start": "egg-scripts start --port 7001 --workers 4", 10 | "backend": "nohup egg-scripts start --port 7001 --workers 4 &", 11 | "tsc": "ets && tsc -p tsconfig.json", 12 | "clean": "ets clean && easy clean", 13 | "kill": "easy kill", 14 | "lint": "tslint --project . -c tslint.json", 15 | "fix": "tslint --fix --project . -c tslint.json", 16 | "yarn": "yarn --registry https://registry.npmmirror.com", 17 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "lint-staged" 22 | } 23 | }, 24 | "lint-staged": { 25 | "app/**/*.ts,tsx": [ 26 | "tslint .", 27 | "git add" 28 | ] 29 | }, 30 | "dependencies": { 31 | "@hubcarl/json-typescript-mapper": "^2.0.1", 32 | "axios": "^0.21.1", 33 | "conventional-changelog-cli": "^1.3.5", 34 | "cross-env": "^7.0.3", 35 | "egg": "^2.1.0", 36 | "egg-cors": "^2.0.0", 37 | "egg-logger": "^2.5.0", 38 | "egg-scripts": "^2.6.0", 39 | "egg-validate": "^2.0.2", 40 | "egg-view-react-ssr": "^3.0.0", 41 | "lodash": "^4.17.4", 42 | "lodash-id": "^0.14.0", 43 | "lowdb": "^1.0.0", 44 | "mo": "^1.7.3", 45 | "mobx": "^6.1.6", 46 | "mobx-react": "7.1.0", 47 | "moment": "^2.17.1", 48 | "react": "17.0.1", 49 | "react-dom": "17.0.1", 50 | "react-loadable": "^5.5.0", 51 | "react-redux": "7.2.2", 52 | "react-router": "5.2.0", 53 | "react-router-config": "5.1.1", 54 | "react-router-dom": "5.2.0", 55 | "react-router-redux": "^4.0.8", 56 | "redux": "4.0.5", 57 | "shortid": "^2.2.8", 58 | "showdown": "^1.8.6" 59 | }, 60 | "devDependencies": { 61 | "@types/react": "^17.0.2", 62 | "@types/react-dom": "^17.0.1", 63 | "antd": "^4.12.3", 64 | "autod-egg": "^1.0.0", 65 | "easywebpack-cli": "^5.2.0", 66 | "easywebpack-react": "^5.0.1", 67 | "egg-bin": "^4.7.1", 68 | "egg-ts-helper": "^1.9.0", 69 | "egg-webpack": "^5.0.0", 70 | "egg-webpack-react": "^3.0.0", 71 | "eslint": "^7.20.0", 72 | "eslint-config-egg": "^9.0.0", 73 | "eslint-plugin-react": "^7.1.0", 74 | "husky": "^4.2.5", 75 | "ip": "^1.1.5", 76 | "less": "^4.1.1", 77 | "less-loader": "^8.0.0", 78 | "lint-staged": "^10.2.2", 79 | "ts-loader": "^8.0.17", 80 | "tslint": "^6.1.3", 81 | "tslint-loader": "^3.5.3", 82 | "typescript": "^4.1.5" 83 | }, 84 | "egg": { 85 | "typescript": true 86 | }, 87 | "engines": { 88 | "node": ">=10.0.0" 89 | }, 90 | "ci": { 91 | "version": "10, 12, 14" 92 | }, 93 | "repository": { 94 | "type": "git", 95 | "url": "git+https://github.com/easy-team/egg-react-typescript-boilerplate.git" 96 | }, 97 | "publishConfig": { 98 | "registry": "https://registry.npmjs.org", 99 | "access": "public" 100 | }, 101 | "author": "hubcarl@126.com", 102 | "license": "MIT", 103 | "homepage": "https://github.com/easy-team/egg-react-typescript-boilerplate" 104 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | module.exports = { 5 | plugins: [ 6 | require('autoprefixer')({ overrideBrowserslist: ['iOS >= 7', 'Android >= 4.0'] }) 7 | ] 8 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./config/tsconfig.json", 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | "target": "es2017", 6 | "module": "commonjs", 7 | /* Experimental Options */ 8 | "skipLibCheck": true, 9 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 10 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 11 | "strictNullChecks": false 12 | }, 13 | "include": [ 14 | "index.ts", 15 | "app/**/*.ts", 16 | "config/**/*.ts", 17 | "mock/**/*.ts", 18 | "test/**/*.ts" 19 | ], 20 | "exclude": [ 21 | "public", 22 | "app/web", 23 | "app/public", 24 | "app/view", 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "member-access": false, 9 | "ordered-imports": false, 10 | "trailing-comma": false, 11 | "quotemark": [true, "single", "jsx-double"], 12 | "eofline": false, 13 | "object-literal-sort-keys": false, 14 | "interface-name": false, 15 | "arrow-parens": false, 16 | "no-console": false, 17 | "max-line-length": false, 18 | "only-arrow-functions": false, 19 | "max-classes-per-file": [true, 3, "exclude-class-expressions"] 20 | }, 21 | "paths": { 22 | "component": "app/web/component" 23 | }, 24 | "rulesDirectory": ["app"] 25 | } -------------------------------------------------------------------------------- /typings/app/controller/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import ExportAntd from '../../../app/controller/antd'; 7 | import ExportBlog from '../../../app/controller/blog'; 8 | import ExportDemo from '../../../app/controller/demo'; 9 | 10 | declare module 'egg' { 11 | interface IController { 12 | antd: ExportAntd; 13 | blog: ExportBlog; 14 | demo: ExportDemo; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typings/app/extend/application.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import ExtendApplication from '../../../app/extend/application'; 7 | type ExtendApplicationType = typeof ExtendApplication; 8 | declare module 'egg' { 9 | interface Application extends ExtendApplicationType { } 10 | } -------------------------------------------------------------------------------- /typings/app/extend/context.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import ExtendContext from '../../../app/extend/context'; 7 | type ExtendContextType = typeof ExtendContext; 8 | declare module 'egg' { 9 | interface Context extends ExtendContextType { } 10 | } -------------------------------------------------------------------------------- /typings/app/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | export * from 'egg'; 7 | export as namespace Egg; 8 | -------------------------------------------------------------------------------- /typings/app/middleware/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import ExportAccess from '../../../app/middleware/access'; 7 | import ExportLocals from '../../../app/middleware/locals'; 8 | 9 | declare module 'egg' { 10 | interface IMiddleware { 11 | access: typeof ExportAccess; 12 | locals: typeof ExportLocals; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /typings/app/model/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import ExportArticle from '../../../app/model/article'; 7 | 8 | declare module 'egg' { 9 | interface IModel { 10 | Article: ReturnType; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /typings/app/service/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | type AnyClass = new (...args: any[]) => any; 7 | type AnyFunc = (...args: any[]) => T; 8 | type CanExportFunc = AnyFunc> | AnyFunc>; 9 | type AutoInstanceType : T> = U extends AnyClass ? InstanceType : U; 10 | import ExportArticle from '../../../app/service/article'; 11 | 12 | declare module 'egg' { 13 | interface IService { 14 | article: AutoInstanceType; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typings/config/plugin.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper@1.35.1 2 | // Do not modify this file!!!!!!!!! 3 | /* eslint-disable */ 4 | 5 | import 'egg'; 6 | import 'egg-onerror'; 7 | import 'egg-session'; 8 | import 'egg-i18n'; 9 | import 'egg-watcher'; 10 | import 'egg-multipart'; 11 | import 'egg-security'; 12 | import 'egg-development'; 13 | import 'egg-logrotator'; 14 | import 'egg-schedule'; 15 | import 'egg-static'; 16 | import 'egg-jsonp'; 17 | import 'egg-view'; 18 | import 'egg-view-react-ssr'; 19 | import 'egg-cors'; 20 | import 'egg-webpack'; 21 | import 'egg-webpack-react'; 22 | import { EggPluginItem } from 'egg'; 23 | declare module 'egg' { 24 | interface EggPlugin { 25 | onerror?: EggPluginItem; 26 | session?: EggPluginItem; 27 | i18n?: EggPluginItem; 28 | watcher?: EggPluginItem; 29 | multipart?: EggPluginItem; 30 | security?: EggPluginItem; 31 | development?: EggPluginItem; 32 | logrotator?: EggPluginItem; 33 | schedule?: EggPluginItem; 34 | static?: EggPluginItem; 35 | jsonp?: EggPluginItem; 36 | view?: EggPluginItem; 37 | reactssr?: EggPluginItem; 38 | cors?: EggPluginItem; 39 | webpack?: EggPluginItem; 40 | webpackreact?: EggPluginItem; 41 | } 42 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://www.yuque.com/easy-team/egg-react/config 3 | const path = require('path'); 4 | const resolve = (filepath) => path.resolve(__dirname, filepath); 5 | module.exports = { 6 | entry: { 7 | blog: 'app/web/page/blog/index.tsx', 8 | antd: 'app/web/page/antd/index.tsx', 9 | 'demo/node': 'app/web/page/demo/node.tsx', 10 | 'demo/async': 'app/web/page/demo/async.tsx', 11 | }, 12 | resolve: { 13 | alias:{ 14 | '@asset': resolve('app/web/asset'), 15 | '@framework': resolve('app/web/framework'), 16 | '@component': resolve('app/web/component') 17 | } 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | less: { 23 | include: [resolve('app/web'), resolve('node_modules')], 24 | options: { 25 | lessOptions: { 26 | javascriptEnabled: true, 27 | modifyVars: { 28 | 'primary-color': 'red', 29 | 'link-color': '#1DA57A', 30 | 'border-radius-base': '2px' 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | { 37 | typescript: true 38 | } 39 | ], 40 | }, 41 | plugins:[] 42 | }; --------------------------------------------------------------------------------