20210226-20210227:《Egg.js框架入门与实战》 |
课程地址:https://www.imooc.com/learn/1185
一、Egg.js框架介绍
1、Egg.js是基于Node.js和Koa的一个企业级应用开发框架,可以帮助团队降低开发成本和维护成本。
二、express,koa.js
上手比较简单,但是没有严格的开发规范
三、Egg.js特性
1、提供基于Egg的定制上层框架的能力
2、高度可扩展的插件机制
3、内置多进程管理
4、基于Koa开发性能优异
5、框架稳定,测试覆盖率高
6、渐进式开发
四、涉及技术点
vant ui框架
vue-cli3
moment.js
Egg.js基础
mysql基础
前后端联调
一、官网
1、https://eggjs.org/zh-cn/intro/quickstart.html
二、建议使用node的LTS版本
LTS:长期稳定版本
current:含目前nodejs最新的特性,相对而言没那么稳定
三、
1、脚手架生成项目
mkdir egg-demo && cd egg-demo
npm init egg --type=simple
2、安装相关依赖包
npm install
3、命令启动
npm run dev:开发中
npm run start:实际生产项目中使用
四、目录
1、app:项目核心目录。业务逻辑、数据库方面的操作
2、config:针对egg.js的插件进行配置
(1)config.default.js:
(2)plugin.js:
3、test:单元测试的时候使用的
4、autod.conf.js:autod的配置文件
5、.eslintrc、.eslintrc:eslint配置文件
一、Router主要用来描述请求URL和具体承担执行动作的Controller的对应关系,框架约定了app/router.js文件用于同一所有路由规则。
二、通过同一的配置,我们可以避免路由规则逻辑散落在多个地方,从而出现未知的冲突,集中在一起我们可以更方便的来查看全局的路由规则。
三、const { ctx } = this
每次用户请求的时候,框架就会实例化一个context上下文
context主要用来存放用户请求的一些信息
四、Controller:负责解析用户的输入,处理后返回相应的结果
一、两种传参方式
1、键值对的模式:?id=123&acb=456
路由:http://127.0.0.1:7000/product/detail?id=123
const { ctx } = this
console.log(ctx.query)
ctx.body = 'id == ctx.query.id'
2、基于/:/123/456
路由:http://127.0.0.1:7000/product/detail2/100
ctx.params.id获取到的id,也就是这个100,是字符串类型。
const { ctx } = this
console.log(ctx.params)
ctx.body = `id == ctx.params.id`
一、请求方法
router.head - HEAD
router.options - OPTIONS
router.get - GET
router.put - PUT
router.post - POST
router.patch - PATCH
router.delete - DELETE
router.del - delete是保留字,所以提供了一个delete方法的别名
router.redirect - 可以对URL进行重定向处理
二、POST
const { ctx } = this
console.log(ctx.request.body)
ctx.body = {
id: 123
}
这时无法在浏览器请求了,这时候用一个工具:postman
1、postman发送请求,报错。终端提示了报错信息
2、解决方案
config/config.default.js中关闭csrf防御方案。
config.security = {
csrf: {
enable: false,
}
}
三、常见的安全漏洞
1、XSS攻击:对Web页面注入脚本,使用JavaScript窃取用户信息,诱导用户操作。
2、CSRF攻击:伪造用户请求向网站发起恶意请求。
3、钓鱼攻击:利用网站的跳转链接或者图片制造钓鱼陷阱。
4、HTTP参数污染:利用对参数格式验证的不完善,对服务器进行参数注入攻击。
5、远程代码执行:用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。
四、egg框架针对web常见的安全风险内置了丰富的解决方案
1、利用extend机制扩展了Helper API,提供了各种模板过滤函数,防止钓鱼或XSS攻击。
2、常见的Web安全头的支持。
3、CSRF的防御方案。
4、灵活的安全配置,可以匹配不同的请求url
5、可定制的白名单,用于安全跳转和url过滤。
6、各种模板相关的工作函数做预处理。
在框架中内置了安全差价egg-security提供了默认的安全实践。
五、PUT,更新数据
路由:http://127.0.0.1:7000/product/update/100
const { ctx } = this
console.log(ctx.params)
ctx.body = `id == ctx.params.id`
六、DELETE
路由:http://127.0.0.1:7000/product/delete/100
const { ctx } = this
console.log(ctx.params)
ctx.body = `id == ctx.params.id`
一、服务(Service)
简单来说,Service就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:
1、保持Controller中的逻辑更加简洁
2、保持业务逻辑的独立性,抽irc象出来的Service可以被多个Controller重复调用。
3、将逻辑和展现分离,更容易编写测试用例。
二、使用场景
1、复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回一用户显示。或者计算完成后,更新到数据库。
2、第三方服务的调用,比如GitHub信息获取等。
三、Service服务一般放在app/service文件夹下
一、View模板渲染
绝大多数情况,我们都需要读取数据后渲染模板,然后呈现给用户。故我们需要引入对应的模板引擎。
框架内置egg-view-ejs作为模板解决方案,并支持多模板渲染,每个模板引擎都以插件的方式引入,但保持渲染的API一致。
二、默认情况下,模板引擎文件会放在app/view文件夹下
三、plugin.js
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
}
.config.default.js
const path = require('path'); // Nodejs内置模块,可解析文件路径
config.view = {
mapping: {
'.html': 'ejs'
},
root: path.join(appInfo.baseDir, 'app/html'); // 修改index.html等html文件存放的文件夹目录,这是存放在app/html文件夹下
root: [ // 多目录配置
path.join(appInfo.baseDir, 'app/view'),
path.join(appInfo.baseDir, 'app/html')
]
}
config.ejs = {
delimiter: "$", // 全局修改分隔符, index.html中用<$= id $>代替<%= id %>
}
四、
1、刷新页面,会报错404 not found
因为egg.js中每个方法几乎都是使用同步模式,返回的是promise,所以需要使用await
const { ctx } = this;
ctx.render('index.html')
修改成:
const { ctx } = this;
await ctx.render('index.html')
五、
controller/home.js
await ctx.render('index.html', {
res,
lists: ['a', 'b', 'c']
}, {
delimiter: '$', // 支持对单个文件进行分隔符修改
})
view/index.html
Document
这是测试
测试样式
<%=res.id%>
<%# 这是注释 %>
<%for(var i=0; i
- <%=lists[i]%>
<%}%>
六、为什么要学后端的模板引擎
1、后端渲染由来以及,渲染性能得到业界认可
2、利于SEO优化,对纯展示类网站体验较好(如公司官网、某些运营页面)
3、对前后端分离开发模式的补充(单点登录的登录页面)
七、Ejs模板引擎中静态资源的使用和配置
view/user.html
<% include user-header.html %>
八、egg项目中,静态资源默认是放在public的
九、egg项目会默认引入egg-static,egg-static是引用了koajs的static-cache这个插件,然后进行二次封装。
config.default.js
config.static = {
perfix: "/assets/", // 将静态资源存放在assets目录下,默认是public文件夹
dir: path.join(appInfo.baseDir, "app/assets"),
}
index.html
一、默认情况下,静态资源会放在app/public文件夹下
1、public/img,存放图片资源
2、public/css,存放css资源
3、public/css,存放css资源
一、vue-cli
npm install -g @vue/cli
vue --version
vue --help
vue create client
mkdir client & cd client
npm run serve
二、h5移动端组件库vant
1、vant:轻量、可靠的移动端Vue组件库
npm i --save vant
2、引入组件的方式
自动按需引入组件(推荐):用到babel-plugin-import这个babel插件
手动按需引入组件
导入所有组件
通过CDN方式
三、.babelrc中
{
"plugins": [
[
"import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}
]
]
}
一、npm i vue-router
二、App.vue
1、src/router/index.js,引入vue-router,配置routes
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'hash',
routes: []
})
2、main.js
import router from './router'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
一、this.$router.push()进行路由的跳转
一、数据库
终端操作
mysql -uroot -p;
show databases;
create database egg_article;
use egg_article;
create table article(
id int(10) not null auto_increment,
img text default null comment '缩略图',
title varchar(80) default null comment '文章标题',
summary varchar(300) default null comment '文章简介',
content text default null comment '文章内容',
createTime timestamp default null comment '发布时间',
primary key(id)
)engine=InnoDB AUTO_INCREMENT=1 comment '文章表';
insert into article(img, title, summary, content, createTime) values('编程必备', 'https://img2.mukewang.com/szimg/5d1032ab08719e0906000338.jpg', '介绍编程必备基础知识', '快速、系统补足您所需要的知识内容', '2019-08-10 10:20:20');
use egg_article;
show tables
desc article;
select * from article;
一、egg-mysql
框架提供了egg-mysql插件来访问MySQL数据库。这个插件既可以访问普通的MySQL数据库,也可以访问基于MySQL协议的在线数据库服务。
二、
npm i egg-mysql
一、
npm i moment
一、插入数据
const result = await app.mysql.insert('article', params);
二、查询文章列表
const result = await app.mysql.select('article');
三、查询文章详情
1、查询单个表,使用get方法
const result = await app.mysql.get('article', { id })
一、proxy的changeOrigin:开启虚拟服务器,让虚拟服务器去请求代理服务器,这样就相当于是两台服务器之间进行数据的交互,就不用担心跨域的问题。
20210228~20210707:《用 React+React Hook+Egg 造轮子 全栈开发旅游电商应用》 |
课程地址:https://coding.imooc.com/class/452.html
(aSuncat-20210707: 前后端涉及的内容比较多,可以多听几遍)
一、课程涉及框架
1、react
umiJS:react工具集锦,路由管理、数据mock,其他相关插件
egg.js:是nodejs的一个框架,有比较严格的项目管理规范,支持路由配置,controller配置器,service服务,模板渲染,默认支持很多实用的插件
2、
自定义hook:提高研发效率
数据流工具think-react-store: react数据流解决方案
Project-libs:常用函数集锦
IntersectionObserver:滚动加载,图片懒加载
二、后端核心技术
egg.js:主框架
jwt:用户验证
Mysql:数据存储
Sequelize:ORM框架,操作mysql
扩展egg框架:提交研发效率
自定义中间件和插件:拦截请求等处理
三、技术架构图
四、前端干货
1、自定义hook
useTitleHook:动态修改浏览器title
useHttpHook: 发送http请求,对某些状态值进行监听
useObserverHook: 监听dom元素是否进入展示区域
useImgHook: 实现图片懒加载
2、自定义组件
createPortal:createPortal在根节点之外创建新的节点
Modal:
ErrorBoundary:捕获错误,展示友好信息
MenuBar:底部Menu
LazyLoad: 懒加载
ShowLoading:底部加载
五、后端干货(手把手开发)
1、中间件
httpLog: 日志中间件
userExist:判断用户是否存在
2、插件
egg-auth:验证用户
egg-info:获取系统信息
3、框架扩展
application:egg.js的应用实例
helper:帮助函数,将要使用的函数全都挂载到egg.js中,这样使用时无需引入,直接用helper即可
requst:对请求来进行扩展
context:对上下文扩展
response: 对response扩展
六、课程安排
react基础进阶 -> 开发组件 -> 开发自定义hook -> egg.js基础 -> egg.js高级 -> 前端界面开发 -> 后端接口开发 -> 系统安全处理 -> 项目发布
七、学习基础
熟悉react.js基础语法
了解node.js基础语法
对全栈开发感兴趣
一、组件经历的不同阶段
二、组件之间的通信
1、父子组件通信
2、兄弟组件通信
3、组件跨页面通信
三、Lazy和Suspence实现懒加载
三、组件报错了怎么办:ErrorBoundary
可将错误信息上传,便于错误的排查
四、CreatePortal创建自定义弹窗
五、章节介绍
UmiJs入门介绍
React组件生命周期
React组件通信方式
Dva数据处理和mock
Context实现数据流管理
组件懒加载
ErrorBoundary错误边界
createPortal自定义弹窗
Ref api操作dom和组件
一、vscode设置
command + shift + p:选择将”code“命令添加到PATH
这样就在终端输入code . 就可以在终端打开vscode编辑器了。
二、依赖包管理工具:npm、yarn
一、父组件向子组件传值
二、子组件向父组件传值
三、兄弟组件之间的传值
1、父组件作为中间层
2、如果嵌组件套得非常多,【父组件作为中间层】的方式就不可行了。
(1)dva
(2)context api
一、models下的namespace是可选的,如果有,就取namespce,否则取文件名。
二、reducers:同步的方法
effects:异步的方法
call:调用异步函数
put: 事件派发
// 同步
reducers: {
getLists(state, action) {
return {
...state,
lists: Array(10).fill(action.payload)
}
},
},
// 异步
effects: {
*getListsAsync({ payload }, { call, put }) {
yield put({
type: 'getLists',
payload,
})
}
}
三、umi的mock功能是对express的封装。
一、子组件(消费者组件),订阅Provider的属性,Provider组件值改变,消费者组件会被重新渲染。
1、contextType
2、consumer
一、.umirc.js 中dynamicImport: true, // 按需加载
1、页面级别:每个页面内所有组件打包成1个js
二、希望对每个页面的每个组件实现按需加载。
const Demo = lazy(() => import('./demo'))
loading...
三、components/LazyLoad/index.js
_renderLay = () => {
let lazy;
const { component, delay, ...other } = this.props;
if (!component || component.constructor.name != 'Promise') {
Lazy = import('./error')
}
Lazy = lazy(() => {
return new Promise(resolve) => {
setTimeout(() => {
resolve(component)
}, delay || 300)
}
})
return
}
一、错误处理:react提供的2个构造函数
getDerivedStateFromError()
componentDidCatch()
二、src/components/ErrorBoundary/index.js
import React, { Component } from 'react';
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
flag: false,
}
}
static getDerivedStateFromError(error) {
return {
flag: true,
}
}
/* error: 抛出的错误
* info: 带有componentStack key的对象,其中包含有关组件引发错误的栈信息
*/
copmponentDidCatch(error, info) {
}
render() {
return (
{this.state.flag ? 发生错误,请稍后再试!
: this.props.children}
)
}
}
1、layouts/index.js中引入ErrorBoundary组件
错误边界是在父组件监测子组件的错误,并不能监测本身发生的错误。
三、可选技术
house?.info?.activity
一、ErrorBoundary,无法处理组件点击事件内部的错误,setTimeout内部的错误。
二、src/CreatePortal/index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class CreatePortal extends Component {
constructor(props) {
super(props);
this.body = document.querySelector('body');
this.el = document.createElement('div');
}
componentDidMount() {
this.el.setAttribute('id', 'protal-root');
this.body.appendChild(this.el);
}
componentWullUnmount() {
this.body.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(this.props.children, this.el)
}
}
createPortal(react node节点,希望插入的dom节点)
一、数据流之外,父组件操作子组件,或操作内部组件dom元素,则可以用ref
一、hook api主要用于function 类型的组件
二、useEffect方法虽然可以执行异步函数,但是不支持async, await
// 错误写法
useEffect(async() => {
})
1、async方法写在useEffect方法里面
useEffect(() => {
async function demo() {
console.log('use')
}
demo
}, [count])
2、async写在useEffect外面
async function demo() {
console.log('demo')
}
useEffect(() => {
console.log('use')
demo()
}, [count])
三、useLayoutEffect是在所有dom渲染完后,才同步执行useLayoutEffect。
一般在这个方法内做dom操作。
一、useTitleHook.js
import { useLayoutHook, useState } from 'react';
export default function useTitleHook(title) {
const [state, setState] = useState();
useLayoutEffect(() => {
document.title = title;
setState(title)
}, [title])
return state;
}
二、jsconfig.json
1、配置vscode相关的配置
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/hooks": ["hooks/index"]
}
}
}
2、这样import { useTitleHook } from ‘@/hooks’;,鼠标放到@/hooks上,就能看到是从哪里引入的
一、think-react-store:基于react hooks和context实现的数据流工具
二、
store/stores/user.js
export default {
state: {
id: undefined,
username: undefined,
}
reducers: {
getUser(state, payload) {
return {
...state,
...payload,
}
}
},
effects: {
async getUserAsync(dispatch, rootState, payload) {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000)
})
dispatch({
type: 'getUser',
payload,
})
}
}
}
store/stores/index.js
import { default as User } from './user';
store/index.js
import React, { useState, useEffect } from 'react';
import { StoreProvider } from 'think-react-store';
import * as store from './stores';
import log from 'think-react-store/middleware/log';
import User from './user';
export default function(props) {
const [state, setState] = useState();
useEffect(() => {
}, [])
return (
)
}
store/user.js
import React, { useState, useEffect } from 'react';
import { useStoreHook, useStateHook, useDispatchHook } from 'think-react-store';
export default function(props) {
const [state, setState] = useState();
const { user: { id, usename, getUserAsync } } = useStoreHook();
const states = useStateHook(); // const states = useStateHook('user'); // 仅返回用户相关的全局state
console.log(states);
const dispatch = useDispatchHook(); //const dispatch = useDispatchHook('user'); // 这样dispatch()里就不用写key
const handleClick = () => {
getUserAsync({
id: 20,
usename: 'admin2',
})
}
const handleClick2 = () => {
dispatch({
key: 'user',
type: 'getUserAsync',
payload: {
id: 20,
username: 'admin2'
}
})
}
useEffect(() => {
getUserAsync({
id: 10,
usename: 'admin',
})
})
return (
user-id: {id}
username: {username}
)
}
一、为什么需要Fiber架构
react16之前的渲染流程
Fiber架构:渲染阶段分成调度阶段、提交阶段
二、Fiber的执行流程
三、Fiber对React生命周期API的影响
调度阶段是可以执行多次的。所以比如发送请求等不适合放在调度阶段。
一、企业级应用的特点
功能完善
规范性高
便于扩展(插件方面的扩展,帮助函数方面的扩展)、升级
二、Egg.js的特点
提供基于Egg定制上层框架的能力
高度可扩展的插件机制(有别于中间件模式)
内置多进程管理
基于koa开发,性能优异
框架稳定,测试覆盖率高
渐进式开发
三、egg.js与koa/express对比
特性对比 | Egg.js | Express/Koa |
---|---|---|
代码的规范性(是否提供明确的MVC架构开发模式) | 三层架构:Controller/Service/View,具有明确的开发和命名规范 | 可以灵活编写代码,没有明确规范 |
学习成本 | 中 | 易 |
插件机制/框架扩展机制 | 有 | 无(有中间件机制) |
多进程管理 | 有 | 无(实现:引用第三方插件,或自己手动封装) |
HttpClient集成 | 有 | 无(实现:引用第三方插件资源) |
一、nodejs单线程,单进程
二、有时需要子进程中执行某些shell命令
三、进程
1、child_process模块
2、cluster模块
nodejs只能用cpu中的某一个内核,这样会造成极大的浪费
3、master进程与cluster进程的通信
四、const { exec, spawn } = require('child_process');
exec、spawn都是用来创建子进程的。
exec:创建子进程,并且将进程执行的结果缓存起来,之后将缓存的结果返回给回调函数。
spawn:返回的是一个stream流
五、child_process.js
const { exec, spawn } = require('child_process');
exec('cat a.js', (error, stdout, stderr) => {
});
const ls = spawn('ls', ['-a'], { encoding: 'utf8' });
ls.stdout.on('data', (data) => {
});
ls.stderr.on('data', (data) => {
});
ls.on('close', (code) => {
})
六、cluster.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const cpus = os.cpus().length;
console.log(cpus)
if (cluster.isMaster) {
console.log('主进程 ${process.pid} 正在运行');
// 衍生工作进程
for (let index = 0; index < cpus; index++) {
cluster.fork();
}
} else {
// 工作进程可以共享任何tcp连接
// 这里我们共享的是一个http服务器
http.createServer((req, res) => {
res.writeHead(200, {'Content-type': 'text/html; charset=utf-8'});
res.write('你好');
res.end()
}).listen(8000)
console.log(`工作进程 ${process.id} 已经启动`)
}
一、Controller
Controller
中的方法可以是同步的,也可以是异步的。但是egg.js规定Controller
里的方法是异步的。
二、测试文件是以.test.js为后缀的。
三、user.test.js
'use strict';
const { app } = require('egg-mock/bootstrap');
describe('user test', () => { // arguments:测试用例的名称,回调函数
it('user index', () => {
return app.httpRequest()
.get('/user')
.expect(200)
.expect('user index')
});
it('user lists', async () => {
await app.httpRequest()
.get('/user/lists')
.expect(200)
.expect('[{"id":123}]')
})
});
终端执行yarn test
一、参数校验:egg-validator
plugin.js
exports.validate = {
enable: true,
package: 'egg-validate'
}
app/user.js
const rule = {
name: { type: 'string' },
age: { type: 'number' }
}
ctx.validate(rule)
test/app/service/user.test.js
'use strict';
const { app, assert } = require(''egg-mock/bootstrap');
deacribe('service user test', () => {
it.only('test detail', async () => {
const ctx = app.mockContext();
const user = await ctx.service.user.detail(10);
assert(user);
assert(user.id === 10);
})
})
一、Cookie
HTTP请求都是无状态的,但是我们的Web应用通常需要知道发起请求的人是谁。为了解决这个问题,HTTP协议涉及了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合Cookie指定规则的网站时带上对应的Cookie来保证安全性)
1、Cookie是运行在浏览器上的
二、通过ctx.cookies,可以在controller中便捷、安全地设置和读取Cookie
三、egg.js中默认对Cookie进行了集成,封装在ctx上下文。
ctx.cookies
const cookies = ctx.cookies.get('user');
ctx.cookies.set('user', JSON.stringify(body));
ctx.cookies.set('user', JSON.stringify(body), {
maxAge: 1000 * 60 * 10, // 过期时间
httpOnly: true, // 只允许服务端操作cookie, document.cookie获取不到cookie的值
})
四、egg.js无法直接设置中文cookie。解决方法
1、加密
ctx.cookie.set('zh', '测试', {
encrypt: true,
});
const zh = ctx.cookies.get('zh', {
encrypt: true,
});
2、base64
app/controller/user.js
encode(str = '') {
return new Buffer(str).toString('base64');
}
decode(str = '') {
return new Buffer(str, 'base64').toString();
}
ctx.cookies.set('base64', this.encode('中文base64'));
const base64 = this.decode(ctx.cookies.get('base64'));
一、session
Session的实现是基于Cookie的,默认配置下,用户Session的内容加密后直接存储在Cookie中的一个字段中,用户每次请求我们网站的时候都会带上这个Cookie,我们在服务端解密后使用。
二、Session、Cookie的区别
异同 | Cookie | Session |
---|---|---|
存储数据 | 是,存储在浏览器 | 是,存储在浏览器、内存或redis等其他数据源中 |
操作环境 | 客户端和服务端均可操作 | 服务端操作session |
大小限制 | 有大小限制并且不同浏览器存储cookie的个数也有不同 | 没有大小限制单核服务器的内存大小有关;但如果session保存到cookie里会受到浏览器限制 |
是否唯一 | 是,不同用户cookie存放在各自的客户端 | 是,服务端会给每个用户创建唯一的session对象 |
安全问题 | 有安全隐患,通过拦截本地文件找到cookie后可以进行攻击 | 安全性高,浏览器可以获取session但难以解析 |
常用场景 | 判断用户是否登录 | 保存用户登录信息 |
四、
async index() {
const session = ctx.session.user;
console.log(session);
}
// 保存session
ctx.session.user = body;
五、session是可以直接支持中文的
六、config.default.js
config.session = {
key: 'IMOOC',
httpOnly: true, // 实际项目中都是用true,来提高安全性
maxAge: 1000 * 5,
renew: true,
}
2、对session进行扩展
app.js
module.exports = app => {
const store = {}
app.sessionStore = {
async get(key) {
console.log('--store--', store);
return store[key]
}
async set(key, value, maxAge) {
store[key] = value
}
async destroy(key) {
store[key] = null;
}
}
}
一、User server, Article server, Order server, Other server
二、
contoller/home.js
class HomeController extends Controller {
async index() {
const { ctx } = this;
const res = await ctx.service.user.detail(20);
console.log(res);
ctx.body = 'hi, egg';
}
}
contoller/curl.js
class CurlController extends Controller {
async curlGet() {
const { ctx, app } = this
const res = await ctx.curl('http://localhost:7001/', {
dataType: 'json',
})
console.log(res);
ctx.body = {
status: 200,
data: res.data,
}
}
}
一、中间件是按照顺序,由外而内,一层层地执行,并且每个中间件都会执行2次。
实际项目中,我们一般使用中间件对请求进行拦截。
二、app/middleware
m1.js
module.exports = options => {
return async(ctx, next) => {
console.log('m1 start');
await next();
console.log('m1 end');
}
}
m2.js
module.exports = options => {
return async(ctx, next) => {
console.log('m2 start');
await next();
console.log('m2 end');
}
}
httpLog.js
const dayjs = require('dayjs');
const fs = require('fs'); // 引入文件处理模块
module.exports = options => {
console.log(options) // config.default.js中的 config.httpLog对象就是它的参数
return async (ctx, next) {
const sTime = Date.now();
const startTime = dayjs( Date.now()).format('YYYY-MM-DD HH:mm:ss');
await next(); // 如果没有这行,浏览器会报错:404 not found
const log = {
method: req.method,
url: req.url,
data: req.body,
startTime,
endTime: dayjs( Date.now()).format('YYYY-MM-DD HH:mm:ss'),
timeLength: Date.now() - sTime
}
// console.log(log)
const data = dayjs( Date.now()).format('YYYY-MM-DD HH:mm:ss') + ' [httpLog] ' + JSON.stringify(log) + '\r\n';
fs.appendFileSync(ctx.app.baseDir + 'httpLog.log', data)
}
}
三、config.default.js
// config.middleware = ['m1', 'm2'];
config.middleware = ['httpLog'];
config.httpLog = { // 中间件的参数
type: 'all',
}
一、扩展方式
扩展点 | 说明 | this指向 | 使用方式 |
---|---|---|---|
application | 全局应用对象 | app对象 | this.app |
context | 请求上下文 | ctx对象 | this.ctx |
request | 请求级别的对象,提供了请求相关的属性和方法 | ctx.request对象 | this.ctx.request |
response | 请求级别的对象,提供了响应相关的属性和方法 | ctx.response对象 | this.ctx.response |
helper | 帮助函数 | ctx.helper对象 | this.ctx.helper |
二、扩展一般放在app/extend文件夹下
三、对application的扩展有2个,1是方法层面的扩展,2是属性的扩展
四、app/extend/application.js
const path = require('path');
module.exports = {
// 方法扩展
package(key) {
const pack = getPack();
return key ? pack[key] : pack;
}
// 属性扩展
get allPackage() {
return getPack();
}
}
function getPack() {
const filePath = path.join(process.cwd(), 'package.json');
const pack = require(filePath);
return pack;
}
app/controller/home.js
class HomeController extends Controller {
async newApplication() {
const { ctx, app } = this;
const packageInfo = app.package('scripts');
const allPack = app.allPackage;
ctx.body = 'newApplication';
}
// 对context上下文进行扩展
async newContext() {
const { ctx } = this;
const params = ctx.params();
ctx.body = 'newContext';
}
async newRequest() {
const { ctx } = this;
const token = ctx.request.token
ctx.body = token;
}
async newResponse() {
const { ctx } = this;
ctx.response.token = 'abc123';
const base64Parse = ctx.helper.base64Encode('newResponse');
ctx.body = base64Parse;
}
}
app/extend/context.js
获取get/post的参数是使用不同的方式,现在我们希望用的是同一种方式
context.js
module.exports = {
params(key) {
const method = this.request.method;
if (method === 'GET') {
return key ? this.query[key] : this.query;
} else {
return key ? this.request.body[key] : this.request.body;
}
}
}
app/extend/request.js
一般情况下,对request, response的扩展一般都是对属性的扩展
module.exports = {
// 获取相关的token
get token() {
console.log('header', this.header);
return this.get('token');
}
}
app/extend/response.js
module.exports = {
// 希望能设置相关的token
set token(token) {
this.set('token', token);
}
}
app/extend/helper.js
module.exports = {
base64Encode(str = '') {
return new Buffer(str).toString('base64');
}
}
一、插件
1、中间件更适合处理请求,插件不仅可以包含中间件所有功能,还可以处理业务逻辑。
2、Egg.js中的插件相当于一个微型应用。
3、插件不包括router.js和controller控制器(可能会与主项目中的路由产生冲突)
二、项目目录下新建lib文件夹,插件都会放在lib文件夹下的plugin文件夹下,plugin里的文件一般以egg-开头
lib/plugin/egg-auth/app/package.json
{
"name": "egg-auth",
"eggPlugin": {
"name": "auth"
}
}
lib/plugin/egg-auth/app/middleware/auth.js
module.exports = options => {
return async (ctx, next) => {
const url = ctx.request.url;
const user = ctx.session.user;
if (!user && !options.exclude.includes(ctx.request.url.split('?')[0])) {
ctx.body = {
status: 1001,
errMsg: '用户未登录',
}
} else {
await next();
}
}
}
config/plugin.js
这里用的是本地的插件,不能用pacakge属性,package属性一般是指线上安装的依赖包
path与package属性是互斥的。
exports.auth = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-auth')
}
app.js
module.exports = app => {
app.config.corMiddleware.push('auth');
}
config/config.default.js
config.auth = {
exclude: ['/home', '/user', '/login', '/logout']
}
一、定时任务
1、定时上报应用状态,便于系统监控
2、定时从远程接口更新数据
3、定时处理文件(清除过期日志文件)
二、
lib/plugin/egg-info/app/extend
三、app/schedule文件夹下存放的都是定时任务
app/schedule/get_info.js
require('egg').Subscription;
class getInfo extends Subscription {
static get schedule() {
return {
interval: 3000,
cron: '*/3 * * * *', // 每隔3秒钟
type: 'worker' // 类型:'all' 、 ’worker‘, all:每个worker进程都会执行这个定时任务, worker:master进程会指定一个进程,来单独执行这个任务
}
}
async subscribe() {
const info = this.ctx.info;
console.log(Date.now(), info)
}
}
module.exports = getInfo;
一、
show database;
create database egg;
二、可视化数据图工具
mysql workbench
一、show databases;
二、demo.sql
-- 删除数据库
drop database egg;
-- 创建数据库
create database egg;
-- 创建表
use egg;
create table user(
id int(10) not null auto_increment,
name varchar(20) not null default 'admin' comment '用户名',
pwd varchar(50) not null comment '密码',
primary key(id)
)engine=InnoDB charset=utf8;
-- 查看表
show tables;
-- 查看表结构
desc user;
-- 删除表
drop table user;
-- 插入表数据
insert into user values(1, 'user1', '123');
insert into user(name, pwd) values('user2', '123');
-- 查询表数据
select * from user;
select id, name from user;
select id, name from user where id = 1;
-- 修改表数据
update user set pwd = '123456' where id = 1;
-- 删除表数据
delete from user where id = 2;
一、
yarn add egg-mysql
yarn dev
config/plugin.js
exports.mysql = {
enable: true,
package: 'egg-mysql'
}
config/config.default.js
config.mysql = {
app: true, // 是否将mysql挂载到app下
agent: false,
client: {
host: '127.0.0.1',
port: '3306',
user: 'root',
password: 'abc123456',
database: 'egg',
}
}
二、egg-mysql的使用方式比较简单,比较适合中小型项目
一、sequelize是一个crm框架
二、
yarn add egg-sequelize mysql2
yarn dev
exports.sequelize = {
enable: true,
package: 'egg-sequelize'
}
config.sequelize = {
dialect: 'mysql', // 数据源
host: '127.0.0.1',
port: '3306',
user: 'root',
password: 'abc123456',
database: 'egg',
define: {
timestamps: false, // 在使用sequelize时,不需要sequelize这个框架为我们自动添加时间相关的字段
freezeTabelName: true, // 冻结表名称,使用sequelize的时候,使用原始的表名称,而不需要sequelize框架额外地处理表名称
}
}
三、app/model,里面是我们的模型文件
app/model/user.js
module.exports = app => {
const { STRING, INTEGER } = app.Sequelize;
const User = app.model.define('user', { // 模型名称,一般是表名称, 'user'
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(20),
pwd: STRING(50)
});
}
app/controller/user.js
const res = await ctx.model.User.findAll({
// where: {
// id: 2,
// }
limit: 1,
offset: 1
})
const res = await ctx.model.User.findByPk(ctx.query.id);
// 更新之前判断数据是否存在
const user = await ctx.model.User.findByPk(ctx.request.body.id);
if (!user) {
ctx.body = {
status: 404,
errMsg: 'id不存在',
}
return;
}
const res = user.update(ctx.request.body);
ctx.body = {
status: 200,
data: res,
}
一、章节目标
1、完成前端界面开发
2、实现列表滚动加载、图片懒加载效果
3、使用mock数据模拟接口(umijs)
二、系统模块
三、技术要点
1、IntersectionObserver,元素是否进入到可视区域
四、学习收获
1、可以学习到前端系统的开发流程
2、了解并实现滚动加载和图片懒加载的思路
3、前端项目的优化思路(公共组件、缓存、骨架屏)
一、vscode用func命令生成新的组件
二、react-icons,是针对react项目封装的icon
yarn add react-icons
// bootstrap的icons
import { BsHouseDoorFill, BsHouseDoor} from 'react-icons/bs'
一、如果父组件内的子组件没有数据交互,数据请求就放在父组件中
一、IntersectionObserver提供了一种异步观察目标元素与其祖先元素及顶级文档视窗(viewport)交叉状态的方法。
二、
三、使用这个特性会比较消耗性能,一般我们会在页面初始化的时候观察这个dom节点,离开页面时取消观察
let observer;
useEffect(() => {
console.log('进入页面');
observer = new IntersectionObserver(entries => {
console.log(entries); // 重点关注intersectionRadio, isIntersecting属性
});
observer.observe(document.querySelector('#loading'));
return () => {
console.log('离开页面')
if(observer) {
// 解绑元素
observer.unobserve(document.querySelector('#loading'));
// 停止监听
observer.disconnect();
}
}
}, [])
一、
/**
* 1、监听loading是否展示出来(loading:请求的节点)
* 2、修改分页数据
* 3、监听分页数据的修改,发送接口,请求下一页的数据
* 4、监听loading变化,拼装数据(loading:数据是否变化的状态)
*/
useObserverHook('#loading'), (entries) => {
}, null)
一、获取url参数,umi
import { useLocation } from 'umi'
const { query } = useLocation()
body = {
code: query?.code
}
一、useImageHook.js
/**
* 1、监听图片是否进入可视区域
* 2、将src属性的值替换为真是的图片地址,data-src
* 3、停止监听当前的节点
*/
const useImgHook(ele, callback, watch = []) => {
}
const dataSrc = item.target.getAttribute('data-src');
item.target.setAttribute('src', dataSrc);
observer.unobserve(item.target);
一、id,loading在search页面是唯一的
二、antd-mobile的日历组件的svg标签中的元素有id是loading,所以search页面的loading这个id得改一下。
三、enums/common.js,可导出一些常量
export const LOADING_ID = 'mk-loading';
enums/index.js
import * as CommonEnum from './common';
export {
CommonEnum
}
使用
import { CommonEnum } from '@/enums'
四、工具函数库
project-libs(文档:https://cpagejs.github.io/project-libs/)
一、banner滑动
react-awesome-swiper
一、不同组件之间的交互,useHttpHook就不适用了。
二、数据流:think-react-store
effects:异步方法,可以在其中发送请求
三、滚动加载
search页面:useHttpHook方式
民宿详情页:数据流方式
/**
* 1、监听loading是否展示出来
* 2、触发reload, 修改分页
* 3、监听reload变化,重新请求接口
* 4、拼装数据
*/
一、
import { history } from 'umi';
history.push({
pathname: '',
query: {
id: '',
}
})
import { useLocation } from 'umi';
const { query } = useLocation();
一、不用进行数据监听,
/**
* 1、页面初始化时候请求接口,useEffect
* 2、监听loading组件是否展示出来,useObserverHook
* 3、修改page, pageNum+1,再次重新请求接口
* 4、拼装数据,然后page
*/
一、cookie
1、右上角登录/注册,如果已经登录了,就显示用户名。
2、点击“我的”,如果未登录,则跳转到登录页面
umi运行时配置
src/appp.js:可以修改路由,复写render
import { cookie } from 'project-libs';
import { history } from 'umi';
// 初始加载,路由切换的时候进行响应的逻辑
export function onRouteChange(route) {
const nowPath = routes.routes[0].routes.filter(item => item.path === route.location.pathname);
const isLogin = cookie.get('user');
if(nowPath.length === 1 && nowPath[0].auth && !isLogin) {
history.push({
pathname: '/login',
query: {
from: route.location.pathname
}
})
}
}
一、
header组件多次渲染,用Memo
import { memo } from 'react';
function areEqual(prevProps, nextProps) {
if (prevProps.citys === nextProps.citys && prevProps.cityLoading) {
return true; // 允许组件重新渲染
} else {
return false;
}
}
export default memo(Search, areEqual);
一、思路
1、通过伪元素实现骨架样式
(1)用伪元素是因为骨架屏只展示区块,区块不包含文字、图片
2、制作布局组件,添加骨架样式
3、替换默认Loading效果
二、这章是针对单独的页面写单独的骨架屏的
三、、global.css
.skeletons {
position: relative;
display: block;
overflow: hidden;
width: 100%;
min-height: 20px;
background-color: #ededed;
}
.skeletons:empty::after {
display: block;
content: ' ';
position: absolute;
width: 100%;
height: 100%;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(216, 216, 216, 0.6), transparent);
animation: loading 1.5s infinite;
}
@keyframes loading { /* 骨架屏动画*/
from {
left: -100%;
}
to {
left: 120%;
}
}
四、src/skeletons
src/skeletons/OrderSkeletons
一、后端
二、章节目标
完成用户模块的接口开发
使用JWT技术验证用户:用户信息加密,生成字符串,之后对字符串解密
提取公共逻辑,优化系统
三、技术要点
redis主要保存核心数据
mysql主要保存业务数据
四、学习收获
1、学习如何开发登录、注册接口以及注意事项
2、学习到如何使用JWT技术进行用户验证
3、如何根据项目需求进行优化(框架扩展、中间件、公共类)
一、创建数据库
app.sql
create database egg_house;
use egg_house;
-- 用户表
create table `user`(
`id` int not null auto_increment,
`username` varchar(20) default null comment '密码',
`createTime` timestamp default null comment '创建时间',
primary key(`id`)
)engine=InnoDB auto_increment=1 default charset=utf8 comment=‘用户表’;
二、
app/model/user.js
module.exports = app => {
const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;
const User = app.modeldefine('user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
username; STRING(20),
createTime: DATE,
});
return User;
}
一、配置项
config/config.default.js
const userConfig = {
salt: 'muke'
}
app/controller/user.js
const { app } = this;
const salt = app.config.salt
一、存在的问题
1、返回给前端的数据,有些是不需要展示的(如密码)
2、dayjs多处使用,推荐将dayjs写成eggjs中的帮助函数
二、app/extend/helper.js
time() {
return dayjs().format('YYYY-MM-DD HH:mm:ss')
}
ctx.helper.time();
三、返回的数据都有dataValue
ctx.session.userId = user.id;
ctx.body = {
status: 200,
data: {
...ctx.helper.unPick(user.dataValues, ['password']),
createTime: ctx.helper.timestamp(user.createTime);
}
}
一、JWT全称JSON Web Tokens,是一种规范化的token。它里面包含用户信息,具有验证用户身份、方式CSRF攻击等优点。
二、jwt官网:https://jwt.io/introduction
三、JWT结构
头部(header) | 载荷(Payload) | 签名(Signature) |
---|---|---|
头部包含了两部分,采用的签名算法和token类型 | 载荷用来存放信息,data(用户数据),exp(过期时间),iat(签发时间) | header和payload编码后的字符串拼接后以及secret(密钥)进行加密,得到签名 |
{ “alg”: “HS256”, “typ”: “JWT” } | { “data”: “xxx”, “iat”: 1441593502, “exp”: 1441594722, } | HMAC-SHA256(encodeBase64Url(header) + “.” + encodeBase64Url(payload), secret) |
一、
yarn add egg-jwt
cofig/中对插件进行配置
config.jwt = {}
一、lib/plugin/egg-auth/app/middleware/auth.js
实际项目中,一般不会将缓存放在session中,因为如果服务重启,session会丢失,如果有多台服务,会导致session不一致。
一、redis可以将缓存与业务解耦
二、Redis是一个基于内存的高性能key-value数据库。具有存储速度快、支持丰富的数据类型、过期后自动删除等特点。被广泛地应用于企业级项目。
二、安装
brew install redis
启动,开机的时候同时进行启动
brew services start redis
进入redis终端
redis-cli
设置值
set id 1
查看
get id
设置过期时间
expire id 3 // id的过期时间为3秒
三、/usr/local/etc里有配置文件redis.conf
搜素requirepass,requirepass可以用来设置redis密码
四、egg.js连接redis
yarn add egg-redis
五、app/controller/user.js
await app.redis.set(username, 1, 'EX', 5);
六、插件、中间件如果想获取app的实例,用ctx.app就可以了
lib/plugin/egg-auth/app/middleware/auth.js
const user = await ctx.app.redis.get(ctx.username);
一、缓存中是否有用户名,而用户名是从前端传过来的token中解析出来的。重新登录,后端会重新生成token,
二、egg.js提供了中间件的多种使用方式
三、app/router.js
const userExist = app.middleware.userExist(); // userExist中间件可以不在config.default.js中配置,即不应用到整个系统中,而是直接应用于某些接口
router.post('/api/user/detail', userExist, contoller.user.detail);
四、优化
1、用户注册登录后,将token保存在redis中
2、改造了登录验证插件,之前登录验证是没有对新旧token进行比较的
3、移除密码、时间处理,封装成共用的方法
4、使用了之前公用的方法promise
5、创建了公用的BaseController, BaseService
一、server/app.sql
-- 民宿表
create table `house`(
`id` int not null auto_increment,
`name` varchar(50) default null comment '房屋名称'
) engine=InnoDB auto_increment=1 default charset=utf8 comment='房屋表';
--图片表
create table `imgs`(
`id` int not null auto_increment,
`url` varchar(500) default null comment '图片地址',
`houseId` int not null comment '房屋id',
`createTime` timestamp default null comment '创建事件',
primary key(`id`)
) engine=InnoDB auto_increment=1 default charset=utf8 comment='图片表';
1、通过设置houseId,将民宿表与图片表关联(外链?)
一、
推荐在需要的页面使用,而不是直接写在layouts/index.js中
二、插件,lib/plugin
1、egg-auth,判断用户是否存在
egg-notFound,判断接口是否存在
2、egg-notFound需要放在egg-auth前面
app.config.coreMiddleware.push('notFound');
一、城市接口,采用第三方接口
const result = await app.httpClient.request()
二、service/house.js
await ctx.model.House.findAll({
limit: 4,
order: [ // 排序
['showCount', 'DESC'],
],
attributes: {
exclude: ['startTime', 'endTime', 'publishTime'], // 去掉数据库中某些字段,不在接口中返回给前端
}
})
三、多表关联
新的特性:associate
include属性
一、model中,通过设置get方法修改获取到的值,如转换成时间戳
publishTime: {
type: DATE,
get() {
return new Date(this.getDataValue('publishTime')).getTime()
}
}
createTime: ctx.helper.time()
一、web应用中存在的安全风险
1、篡改网页内容
2、窃取网站内部数据
3、网页中植入恶意代码,使用户利益得到侵害
一、开发新的插件,对所有接口进行拦截
一、思路
/**
**3秒内最多允许3个接口请求
* 1、设置计数器,每次请求加1,保存起始时间
* 2、超过3秒,计数器大于3,则提示请求频繁;计数器清零,起始时间修改为当前时间
* 3、超过3秒,计数器小于3,计数器清零,起始时间修改为当前时间
*/
一、缓存接口思路
/** 缓存接口
* 1、接口地址作为redis中的key
* 2、查询redis, 有缓存、返回返回接口
* 3、没有缓存,将接口返回结果保存到redis中
*/
二、缓存的接口:比如用户详情接口
一、为什么需要Docker
1、开发环境不一致:开发系统不一致、本地开发环境和线上环境不同
线上环境一般是linux系统
2、软件安装麻烦:安装不同软件的复杂程度不同,不仅耗时久还容易出错。
3、运维成本过高:软件维护和升级都比较费时费力,如果新增机器,所有软件都需要重新安装
二、docker官网:https://docs.docker.com/get-docker/
一、Docker操作
1、镜像操作:拉取、查看、删除等
2、容器操作:运行、查看、进入、删除等
二、Docker engine
1、docker引擎的镜像:https://register.docker-cn.com,默认是国外的,可以设置成这个国内的
https://hub.daocloud.io,国内的镜像
2、docker pull daocloud.io/library/node:12.18
docker images
docker tag 28faf336034d node
docker tag 28faf336034d node1:v1.0
3、导出镜像
mkdir docker
cd docker
ls
docker save -o node.image 28faf336034d
4、删除镜像
ls
docker rmi 28faf336034d -f
5、导入镜像
docker load -i node.image
三、本课程mysql版本:8.0.20
1、启动并运行一个镜像:run
2、docker run -d -p 3307:3306 --name mysql -e MYSQL_ROOT_PASSWORD=abc123456 be0dbf01a0f3(3306:当前启动的容器的端口)
3、docker ps
4、docker ps -a // 查看所有当前运行的/停止的镜像
5、docker stop a24c8a967dd6
6、docker rm a24c8a967dd6
7、docker exec -it ecf7f372b176 sh // 进入容器内部
mysql -uroot -p
show database
exit // 退出mysql
exit // 退出mysql容器
8、docker restart ecf7f372b176
一、云服务器ecs
公网ip:暴露给第三方
私有ip:当前实例中使用的,外部人员是访问不到的
二、ssh [email protected] // 跟着的是公网ip,进入阿里云后台
三、pwd,当前路径
一、阿里云环境下安装docker
centos
二、用yum安装docker
1、yum install yum-utils device-mapper-persistent-data lvm2
2、y
3、设置阿里的镜像源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
4、y
5、y
6、启动docker
start docker
7、开机启动
enable docker
8、docker -v
9、修改镜像源
vi /etc/docker/daemon.json
{
"registry-mirrors": ["https://register.docker-cn.com/"]
}
systemct1 daemon-reload
systemct1 restart docker
三、安装镜像
1、mysql:docker pull daocloud.io/library/mysql:8.0.20
2、redis
四、本地nginx里的内容复制到阿里云,scp
scp -rp nginx [email protected]:/root
五、server/Dockerfile
# 使用node镜像
FROM daocloud.io/library/node:12.18
# 在容器中新建目录文件夹 egg
RUN mkdir -p /egg
# 将/egg设置为默认工作目录
WORKDIR /egg
# 将package.json 复制默认工作目录
COPY package.json /egg/package.json
# 安装依赖
RUN yarn config set register https://registry.npm.taobao.org
RUN yarn --production
# 再copy代码至容器
COPY ./ /egg
# 7001端口
EXPOSE 7001
# 等容器启动之后执行脚本
CMD yarn start
六、daemon可以让服务在后台运行
七、解压 unzip
unzip -u -d server egg.zip
八、docker中mysql的权限配置
1、docker exec -it fb2520649292 sh
2、mysql -uroot -p
3、远程连接授权
GRANT ALL PRIVILEGES ON . TO ‘root’@’%’ WITH GRANT OPTION
4、刷新权限
FLUSH PRIVILEGES;
5、更改加密规则
ALTER USER ‘root’@‘localhost’ IDENTIFIED BY ‘password’ PASSWORD EXPIRE NEVER;
6、更新root用户密码
ALTER USER ‘root’@’%’ IDENTIFIED WITH mysql_native_password BY ‘abc123456’;
7、刷新权限
FLUSH PRIVILEGES;
九、docker build -t egg:v1.0 ./server
docker run -d -p 7001:7001 --name server cf0aef86ed0e
docker logs -f abbfa1822b05 // abbfa1822b05是port
一、课程主线
前端:React.js Umijs think-react-store
后端:Egg.js Mysql Redis Docker
二、分页,多表联查 egg-sequelize