Egg.js学习之路
官网:eggjs.org/
Egg.j介绍
Egg.js为企业级框架和应用而生的Node.js框架,Egg 选择 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。
Egg.js特性
- 提供基于Egg定制上层框架的能力
- 高度可扩展的插件机制
- 内置多进程管理(Node是单进程,无法使用多核CPU的能力)
- 基于Koa开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发,逐步模块化模式
Egg.js的Hello World
我是用的VSCode来进行学习过程中的代码编写,进入软件(最好是用管理员进行启动,不然可能会在安装包的过程中遇到问题),启动终端
- 创建目录并进入
mkdir egg-example
cd egg-example
- 初始化
npm init egg --type=simple
+安装依赖包
npm i
以上的操作会给我们拉去egg.js框架和生成项目,在生成项目的过程中有些配置会提醒我们配置,比如项目名称、描述、作者,这些都是可以略过的。 完成之后我们就可以启动项目
npm run dev
之后会提醒我们访问地址,点开地址就可以查看项目启动成功,出现hi,egg的字样。
我们可以把 app/controller/home.js 文件中的 ctx.body = 'hi, egg',改为 ctx.body = 'Hello World'
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'Hello World';
}
}
module.exports = HomeController;
这样我们就完成了我们的Hello World。
Egg的目录结构
- app - 项目开发的主目录,工作中的代码几乎都写在这里面
-- controller -- 控制器目录,所有的控制器都写在这个里面
-- router.js -- 项目的路由文件
- config - 项目配置目录,比如插件相关的配置
-- config.default.js -- 系统默认配置文件
-- plugin.js -- 插件配置文件
- logs -- 项目启动后的日志文件夹
- node_modules - 项目的运行/开发依赖包,都会放到这个文件夹下面
- test - 项目测试/单元测试时使用的目录
- run - 项目启动后生成的临时文件,用于保证项目正确运行
- typings - TypeScript配置目录,说明项目可以使用TS开发
- .eslintignore - ESLint配置文件
- .eslintrc - ESLint配置文件,语法规则的详细配置文件
- .gitignore - git相关配置文件,比如那些文件归于Git管理,那些不需要
- jsconfig.js - js配置文件,可以对所在目录下的所有JS代码个性化支持
- package.json - 项目管理文件,包含包管理文件和命令管理文件
- README.MD - 项目描述文件
run dev 和run start的区别
当我们在使用这两句命令的时候,会发现一些问题,这就是接下来需要介绍到的问题。 需要仔细的记住。
npm run dev
这是我们在开发环境下使用命令,更改代码后不需要重新启动服务器,会自动检测修改的内容,当刷新网页的时候,内容就会呈现。
npm run start
这是在生产环境中使用的,使用这个命令是以服务的方式进行运行,修改内容需要重新启动,同时你即使关闭终端,服务还是在继续运行,需要使用 npm run stop 来停止服务。
Egg 入门
通过上面的介绍,我们可以对Egg有一点点的了解,那么我们开始尝试着写点东西
我们在 /app/controller/home.js,可以看到下面的代码。
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'Hello World';
}
}
module.exports = HomeController;
我们在原有的代码中,加入一些代码。在Egg.js中。使用的全部都是异步 async 名字(){....} ,Egg也是使用 Context 对象,并提供了很多的方法。如下:
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'Hello World';
}
async xiaoben(){
const {ctx} = this;
ctx.body="<h1>小笨哥哥,等我一下哦</h1>";
}
}
module.exports = HomeController;
然后我们再进入页面发现并不是我们想要的内容,这是为什么,因为我们在 /app/controller/home.js 写完代码之后,还需要配置路由,同样是在app/router.js 进行如下的配置:
router.get('/ben', controller.home.xiaoben);
这样就完成了路由的配置
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/ben', controller.home.xiaoben);
};
当我们访问 http://127.0.0.1:7001/ben 的时候就会出现我们想要的内容。
Controller 控制器
Controller 是什么
Controller 顾名思义,控制器,简单的说 Controller 负责解析用户的输入,处理后返回相应的结果 ,但是不同的场景,那么具体的作用有一点区别:
- 在 RESTful 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
- 在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
- 在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。
官方文档 推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),然后调用对应的 service 方法处理业务,得到业务结果后封装并返回:
- 获取用户通过 HTTP 传递过来的请求参数。
- 校验、组装参数。
- 调用 Service 进行业务处理,必要时处理转换 Service 的返回结果,让它适应用的需求。
- 通过 HTTP 将结果响应给用户。
-------------------------------------------------------1
编写Controller
所有的Controller 文件都必须放在app/controller 目录下,可以支持多级目录,访问的时候可以通过目录名级联访问。
在 controller 下新建一个han.js 文件,编写下面的代码
'use strict';
const Controller = require('egg').Controller;
class HanController extends Controller{
async index(){
const { ctx } = this;
ctx.body = '<h1>hello, hanhan</h1>';
}
}
module.exports =JspangController;
写完Controller之后,然后需要去配置路由。
在 app.js 目录下的 router.js 文件,在里面添加如下的代码。
router.get('/han',controller.jspang.index);
当我们访问 http://127.0.0.1:7001/han 的时候就可以了。
Egg.js 单元测试
对于各种可能的输入的测试用例,一旦测试覆盖,都能明确它的输出。 代码改动后,可以通过测试结果判断代码的改动是否影响已确定的结果。
在每次写完应用的 Controller、Service、Helper、Extend 等代码之后,都必须有对应的单元测试保证代码质量。 当然,框架和插件的每个功能改动和重构都需要有相应的单元测试,并且要求尽量做到修改的代码能被 100% 覆盖到。越早发现问题,越好解决。
单元测试分为同步测试和异步测试
同步单元测试
Egg框架规定,所有的测试代码,都需要放在/test目录下面。如果是Controller相关的代码就需要放在/test/app/controller文件夹下面。所有的测试文件都需要以.test.js为后缀的。
这里就创建一个与上面han.js 对应的单元测试 han.test.js,然后编写:
'use strict';
const { app } = require('egg-mock/bootstrap');
describe('jspang test', () => {
it('han index', () => {
return app.httpRequest()
.get('/han')
.expect(200)
.expect('<h1>hello, hanhan</h1>');
});
});
这里的describe( )方法有两个参数,第一个是测试的描述(字符串类型),这个描述一般都是用文件的路径。第二个参数是一个回调函数,里边是对这个控制器里边的具体方法的测试用例。
写好之后,运行 npm run test 就可以查看测试用例是否通过。
异步方法
编写异步单元测试,需要一个异步的 Controller 方法,回到app/controller/han.js 文件中,编写一个异步的方法 Baby,这里就直接用setTimeout 来模拟一下。
async Baby(){
const { ctx } = this;
await new Promise(resolve => {
setTimeout(()=>{
resolve(ctx.body="<h1>憨憨,你就是最靓的仔</h1>")
},5000)
})
}
再到/router.js里编写对应的路由。
router.get('/baby', controller.han.Baby);
之后就可以在浏览器中通过http://127.0.0.1:7001/baby访问这个页面了,可以看到页面5秒后才会出现结果,憨憨,你就是最靓的仔。
完成之后 test/app/controller/han.test.js
it('han Baby', async () => {
await app.httpRequest()
.get('/baby')
.expect(200)
.expect('<h1>憨憨,你就是最靓的仔</h1>');
});
注意的是在代码的第一行,加入了一个async关键字,这就是异步代码测试的关键。
写完测试代码,再次输入dev test进行测试,可以看到这次的测试用时变成了5000多毫秒。
Get请求和参数传递
下面开始学习比较常用的请求 get请求 ,比如通过url进行访问地址,get请求的特点
- 优点:使用简单,清晰有条例。适合网站和对外App的使用。
- 缺点:传递参数是有大小限制,安全性较差,不能完成重要数据的传递。
我们需要根据具体的使用情况,来判断使用的请求方式。
自由传参方式(Query String 方式)
这种传参的方式和最初的传参模式相同,传递的参数个数和名称都没有具体的明确的定义,设置不限制你是否传递参数,使用起来非常的灵活。
假设我们上文中的 han.js 中的憨憨是一个服务人员,那么上面的憨憨是被我们固定写死了的,这明显不是想要的结果,我们还是希望有不同的服务滴,所以呢,这个时候的我们就需要开始翻牌子了,就需要告诉老板,我们需要的是多少号的憨憨。所以:
打开/app/controller/han.js文件,重新编写一个hanTwo( )的方法。
async hanTwo(){
const {ctx} = this;
ctx.body = ctx.query
}
这里的 ctx.query 就是获得传递的参数,写完之后我们需要配置路由,
打开/app/router.js文件。
router.get('/hantwo', controller.han.hanTwo);
这样的话,当你使用浏览器进行访问的时候,URL为 http://127.0.0.1:7001/hantwo?name=元宝 ,这时候页面就会显示传你传递的参数。
如果你传递多个参数,页面都会进行完美的展示,比如 http://127.0.0.1:7001/hantwo?name=元宝&age=18
严格传参模式(参数命名方式)
传递的参数个数是固定的,你传递参数顺序是固定的,你传递的参数名称是固定的。
Get的严格传参模式需要配合router.js文件进行设置。例如设置一个hanThree的路径,然后必须传递一个name的参数,就可以这样设置。
router.get('/hanthree/:name', controller.han.hanThree);
设置好路由后,再到/app/controller/han.js。编写一个hanThree的方法就可以。
async hanThree(){
const {ctx} = this;
ctx.body=ctx.params.name
}
ctx.params是获取所有的传递参数,ctx.params.name是获取传递参数的name值。
这时候的访问URL不在是用 ? 来开始传递参数了,而是直接用/ 左斜杠来传递。
http://127.0.0.1:7001/hanthree/小憨
时候如果你的url中不传参或者多传参数,都会报错。
严格传参中的多参数传递
对个参数的传递,其实也很简单。
路由配置页面就可以这样设置。
router.get('/hanthree/:name/:age', controller.han.hanThree);
然后修改hanThree的方法就可以。
async hanThree(){
const {ctx} = this;
ctx.body=ctx.params
}
当你进行访问 http://127.0.0.1:7001/hanthree/小憨/18
就可以看到页面显示的数据。
------------------------------------------1.10
Post请求和参数的接收
编写Post请求的Controller方法
在/app/controller/han.js文件中编写一个 add()方法。
async add(){
const {ctx} = this
ctx.body="add post"
}
然后进行路由配置/app/router.js
router.post('/add', controller.han.add);
此时我们就可以进行POST访问,但是POST的访问不能用浏览器访问页面,我用的是Postman,当然你也可以在VSCode中安装插件REST Client。
安全设置解除
当你第一次请求时,可能会出现403错误,这是因为Egg.js默认开启了CSRF安全策略,原因是egg 框架内置了安全系统,默认开启防止 XSS 攻击 和 CSRF 攻击。
在Security的默认拦截器里,默认会开启CSRF处理,判断请求是否携带了token,如果没有就拒绝访问。并且,在请求为(GET|HEAD|TRACE|OPTIONS)时,则不会开启。
就是要不你关闭了这个安全拦截器,要不你请求带token。
因为是在学习的过程中,所以我采取的是直接关闭拦截器,但是官网并不推荐这种方法,在官网中有比较详细的方式 eggjs.org/zh-cn/core/…
我们直接关闭,在/config/config/default.js文件中配置
config.security = {
csrf :{
enable:false,
}
}
然后就可以发送请求了。
接收POST参数
能发送POST请求后,需要在服务端接收请求。Egg.js已经给我们封装好了,直接使用ctx.request.body来获取。
async add(){
const ctx = this.ctx
ctx.body={
status: 200,
data:ctx.request.body
}
}
发送请求就能拿到数据了。
Service的编写
简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:
- 保持 Controller 中的逻辑更加简洁。
- 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
- 将逻辑和展现分离,更容易编写测试用例,测试用例的编写具体可以查看这里。
应用场景
- 复杂数据的处理,比如要展现的信息需要从数据库获取,还要经过一定的规则计算,才能返回用户显示。或者计算完成后,更新到数据库。
- 第三方服务的调用,比如 GitHub 信息获取等。
就是说只要是和数据库进行交互的操作,都写在Service里(不知道这么说对不对)
创建一个service文件
Egg规定Service文件必须放在/app/service目录,所以我们在/app文件夹下面,新建一个service文件夹。然后在新建一个han.js文件。
'use strict';
const Service = require("egg").Service;
class HanService extends Service{
async getGirl(id){
//因为没有真实连接数据库,所以模拟数据
return {
id:id,
name:'小红',
age:18
}
}
}
module.exports =HanService;
Controller 调用Service数据
写完Service方法后,就可以在Controller中使用Service获取它提供的数据了。现在我们回到/app/controller/han.js文件中的getGirl( )方法。
我们并不需要把service进行引入,因为Egg已经为我们作好了这一切。直接使用ctx上下文就可以进行使用了。
下面为Controller中的getGirl( )方法改写。
async getGirl(){
const {ctx} = this;
const res = await ctx.service.han.getGirl('1818');
ctx.body = res
}
启动服务,发送请求,就可以看到返回的数据。
Service 方法的可调用性
当你写完一个service方法后,你可以在其它的Controller里进行使用。比如我们现在去home.js中进行使用。打开/app/controller/home.js文件下,新建一个testGetGirl( )方法。
async testGetGirl(){
const ctx = this.ctx;
let id = ctx.query.id
const res = await ctx.service.han.getGirl(id)
ctx.body=res;
}
写完上面的代码,到/app/router.js文件里,增加一个新的路由。
router.get('/testGetGirl', controller.home.testGetGirl);
写完后保存文件,然后在浏览器中输入URL(http://127.0.0.1:7001/testGetGirl?id=2000)。可以看到,页面如愿以偿的获得了这些数据。
View中使用EJS模板引擎
Egg.js专注后端开发,但是也提供了View层的渲染.并不是所有的开发都是前后端分离的。还是有很多应用需要服务端渲染的,比如博客就需要服务端渲染。
服务端渲染的好处
还是先来说一下服务端渲染页面的三个优点。
- 对SEO非常友好,单页应用,比如Vue是到客户端才生成的。这种应用对于国内的搜索引擎是没办法爬取的,这样SEO就不会有好的结果。所以如果是官网、新闻网站、博客这些展示类、宣传类的网址,必须要使用服务端渲染技术。
- 后端渲染是老牌开发模式,渲染性能也是得到一致认可的。在PHP时代,这种后端渲染的技术达到了顶峰。
- 对前后端分离开发模式的补充,并不是所有的功能都可以实现前后端分离的。特别现在流行的中台系统,有很多一次登录,处处可用的原则。这时候就需要服务端渲染来帮忙。
EJS在Egg的安装和配置
npm 的安装方法
$ npm i egg-view-ejs --save
yarn 的安装方法
yarn add egg-view-ejs
安装完成后,并不能直接使用,需要再进行一些简单的配置。
配置/config/plugin.js文件
exports.ejs = {
enable :true,
package:"egg-view-ejs"
}
plugin.js文件配置完成后,再到/config /config.default.js文件里进行配置。
config.view = {
mapping : {
".html":"ejs"
}
};
config.ejs={
}
使用EJS模板引擎
当配置项都完成后,就可以使用模板EJS模板引擎了。打开/config/controller/han.js文件。修改index( ),因为render( )方法返回的是Promise的对象,所以要使用关键字await.
async index() {
const { ctx } = this;
await ctx.render('han.html')
}
这时候我们还没有han.html这个页面,所以需要马上建立一个。Egg.js规定view的引擎模板,必须写在/app/view/文件夹下面(如果没有view文件夹,可以自己建立)。在文件夹下面新建一个 文件han.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HANHAN</title>
</head>
<body>
<h1>I am Han!</h1>
</body>
</html>
启动服务,访问 http://127.0.0.1:7001/my ,因为前面的router.js文件中配置了
router.get('/my', controller.han.index);
所以直接访问就可以了。
EJS模板中显示contorller 返回的数据
在Controller中我们有一些动态的数据,需要在EJS模板中进行显示。这时候可以使用render( )第二个参数,第二个参数可以传递JavaScript中的对象,也就是说你可以任意的传递各种形式的参数过去。
我们先到/app/controller/han.js文件下面。在index( )方法里,使用第二个render( )的参数,来传递参数。
async index() {
const { ctx } = this;
await ctx.render(
'han.html',{
id:2021,
name:'小红',
age: 18,
skill:'唱歌'
})
}
有了这些数据后,就可以在/controller/view/han.html文件下使用这些参数了。默认使用的方法是<%= 参数 %> 。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>现在由<%= id %>号服务员,为你服务!</h2>
<h3>你好,我是<%= name %><%= age %>岁,专业技能是<%= skill %></h3>
</body>
</html>
通过这种方式,在EJS模板中就可以使用controller传递过来的变量了。
EJS模板中的循环显示
技师小姐姐不可能只会一项技能,比如我们小红,可能就会三项技能唱歌、跳舞、华尔兹,那这时候我希望他是循环展示出来的。也就是说cotroller传递过来的数据是一个数组,如何利用列表来展示。
先来修改\app\controller\han.js文件中的index( )方法。
async index() {
const { ctx } = this;
await ctx.render(
'han.html',{
id:2021,
name:'小红',
age: 18,
skills:[
'唱歌',
'跳舞',
'华尔兹'
]
})
}
这时候已经传递了一个数组到模板中,重点是如何再模板中用列表的形式进行展示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>现在由<%= id %>号服务员,为你服务!</h2>
<h3>你好,我是<%= name %><%= age %>岁</h3>
<br/>
我的技能如下:
<ul>
<% for(var i=0;i<skills.length;i++){ %>
<li><%= skills[i] %></li>
<%}%>
</ul>
</body>
</html>
这里使用了for循环进行展示,这种写法跟php的写法也非常类似。
修改默认分隔符符号
EJS也为我们提供了修改默认分隔符合,EJS为我们提供了配置型方法,你可以根据你的喜好配置这个配置符。
打开/config/config.default.js文件,然后修改config.ejs选项。
config.ejs={
delimiter: "$"
}
然后再回到han.html文件,用全部地替换的方法,把%换车$,这样就更换成功了。但是不要随意改动。
EJS还提供了单独修改某个修饰符,但这里不建议这样修改,会让项目变的混乱不堪,也就是render的第三个参数。
在ESJ模板引擎中配置和使用静态资源文件
共代码片段的使用
比如网站项目中的头部和底部往往都是一样的,这时候我们把公共的部分抽离出来,单独到一个文件夹中,然后在需要的页面直接引入。
在/app/view/目录建立一个header.html文件。在文件里我们只需要写一些代码片段就可以了,没必须写出全部的html架构。
/app/view/header.html文件
<h1>欢迎来到小屋!</h1>
然后回到/app/view/han.html文件,直接使用<% include header.html %>引入到模板里就可以了。
<body>
<% include header.html %>
<h2>现在由<%= id %>号服务员,为你服务!</h2>
<h3>你好,我是<%= name %><%= age %>岁</h3>
<br/>
我的技能如下:
<ul>
<% for(var i=0;i<skills.length;i++){ %>
<li><%= skills[i] %></li>
<%}%>
</ul>
</body>
在终端中用yarn dev启动服务,然后在浏览器中输入http://127.0.0.1/my。 可以看到公用部分的文件已经引入进来了。
配置静态资源
Egg中默认的静态资源文件在/app/public目录下,比如我们在public文件夹下新建一个css文件夹,然后新建default.css文件。
body{
color:red;
}
然后直接在浏览器中试着查看这个CSS文件。浏览器中输入地址http://127.0.0.1:7001/public/css/default.css 。正常情况css代码内容已经显示在浏览器里了。
疑问:静态资源我们并没有配置路由,为什么就可以方法到那?
不用配置,静态资源就可以方法到,是因为Egg使用了egg-static插件,这个插件是koa-static的升级版。我们可以打开node_modules 文件夹,直接搜索egg-static文件夹,就可以看到。
知道了这个道理,我们也可以猜测到,这个public前缀一定是可以修改的,比如修改为assets.
打开/config/config.default.js文件,然后写入如下配置。
onfig.static = {
prefix:"/assets/"
}
这时候再访问原来的URL就会报404错误,你这时候修改一下路径,把public改成assets就又可以访问到了。
甚至你可以配置静态文件放置的文件夹,但我个人不建议你修改,包括public的前缀也不建议你修改。
使用静态资源文件
会配置静态资源了,我们就可以在EJS模板里使用这些资源了,比如现在引入刚才写的default.css文件。(先把刚才修改的配置删除掉,回归默认配置。)
然后到/app/view/han.html中引入这个CSS样式。引入方法和正常的HTML引入方法一样就可以了。
<link rel="stylesheet" type="text/css" href="public/css/default.css" />
到浏览器进行查看,发现字体已经全部变成了红色。
Cookie的增删改查
Cookie的作用就是在浏览器客户端留下一些数据,比如我们经常使用的登录一个网站,下次再来的时候就不用再次登录了。但是Cookie是可以设置时间限制的,所以经常看到一段时间内不用重复登录,这样的信息。
对Cookie的简单了解
HTTP请求是无状态的,但是在开发时,有些情况是需要知道请求的人是谁的。为了解决这个问题,HTTP协议设计了一个特殊的请求头:
Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保留,并在下一次请求同一个服务的时候带上。
简单来说,就相当于你去上班,给你发了一个工牌,这样你就可以进出办公大楼了。
基本准备
我们先来了解在Egg下,如何实现Cookie的增删改查,这也是最基本的操作。(提示,在实际开发中Cookie的操作应该放在服务端,而不是用客户端的JS操作。)
在上面view那一节的/app/view/han.html模板中,编写四个按钮,分别是增加Cookie、删除Cookie、修改Cookie和查看Cookie。
<div>
<button onclick="add()">增加Cookie</button>
<button onclick="del()">删除Cookie</button>
<button onclick="editor()">修改Cookie</button>
<button onclick="show()">查看Cookie</button>
</div>
写完按钮后,增加对应的方法JavaScript。
可以先写一个方法(function),然后其它的复制后进行修改。就可以快速写出这些代码了。
<script>
function add(){
fetch("/add",{
method:"post",
headers:{
"Content-type":"application/json"
}
});
}
function del(){
fetch("/del",{
method:"post",
headers:{
"Content-type":"application/json"
}
});
}
function editor(){
fetch("/editor",{
method:"post",
headers:{
"Content-type":"application/json"
}
});
}
function show(){
fetch("/show",{
method:"post",
headers:{
"Content-type":"application/json"
}
});
}
</script>
再去/app/controller/han.js文件下,增加这四个对应的方法。
async add(){
const ctx = this.ctx
}
async del(){
const ctx = this.ctx
}
async editor(){
const ctx = this.ctx
}
async show(){
const ctx = this.ctx
}
最后再到/app/router.js中配置对应的路由
router.post('/add', controller.han.add);
router.post('/del', controller.han.del);
router.post('/editor', controller.han.editor);
router.post('/show', controller.han.show);
Cookie的增加操作
先到/app/controller/han.js文件的add( )方法里编写代码。其实egg已经为我们准备好了操作Cookie的方法。直接使用就可以了。
async add(){
const ctx = this.ctx
ctx.cookies.set("user","jspang.com")
ctx.body = {
status:200,
data:'Cookie添加成功'
}
}
这部分代码写完后,就可以可以在终端中开启服务,输入yarn dev开启。
来到页面点击增加Cookie按钮,然后按F12打开调试模式,找到Application,可以看到Cookie值已经被加入进来了。
Cookie的删除操作
再来看一下Cookie的删除操作,再来到/app/controller/han.js文件。修改del( )方法。
async del(){
const ctx = this.ctx
ctx.cookies.set("user",null)
ctx.body = {
status:200,
data:'Cookie删除成功'
}
}
写好后,直接到浏览器中点击删除,就可以删除Cookie了。
Cookie的修改操作
修改操作和删除和增加一样。直接修改值就可以了。
async editor(){
const ctx = this.ctx
ctx.cookies.set("user",'bilibili')
ctx.body = {
status:200,
data:'Cookie修改成功'
}
}
Cookie的显示
显示Cookie要使用ctx.cookies.get( )方法。
async show(){
const ctx = this.ctx
const user=ctx.cookies.get("user")
console.log(user)
ctx.body = {
status:200,
data:'Cookie显示成功'
}
}
点击按钮后,可以在VSCode的终端中显示出结果。你可以试着让它显示在页面上。
Cookie的配置和加密
Cookie时效设置
Cookie是拥有时效性的,那么在Egg.js中,使用ctx.cookie.set()方法,ctx.cookie.set()方法是有三个参数的,第一个第一个参数是key,第二个参数是value,第三个参数就可以进行配置。比如你需要配置Cookie的有效时间,可以使用maxAge属性。(这个时间是毫秒。)
比如现在我们到/app/controller/han.js文件里,修改add( )方法,把maxAge设置为两秒,在添加Cookie两秒后,这个Cookie就会自己失效。
async add(){
const ctx = this.ctx
ctx.cookies.set("user","han.com",{
maxAge:1000*2
})
ctx.body = {
status:200,
data:'Cookie添加成功'
}
}
现在你重新刷新页面,会发现,等两秒之后Cookie自动没有了。
HttpOnly的设置
伪造Cookie来绕过登录是黑客经常使用的一种手段,所以为了安全,Egg.js默认设置只允许服务端来操作Cookie。
比如现在你通过JS的方式document.cookie获取Cookie是不能获取的(需要在浏览器的控制台输入获取)。当我们想通过客户端操作Cookie时,可以通过下面的代码进行设置。
sync add(){
const ctx = this.ctx
ctx.cookies.set("user","han.com",{
maxAge:1000*60*60,
httpOnly:false
})
ctx.body = {
status:200,
data:'Cookie添加成功'
}
}
这样我们就可以看到,能够从客户端获取到Cookie,并且可以看到浏览器控制台的HttpOnly就变成false属性了。
设置中文Cookie的方法
有时候开发需求我们在Cookie里设置中文,如果直接设置&添加,服务端会直接报错的。比如我们在add( )方法里设置中文,然后再操作就会报错500。
ctx.cookies.set("user","小笨")
点击按钮后,会直接报500的错误。那下面就学习一下解决中文Cookie的方法。
设置中文Cookie的时候进行加密
加密只要在第三个参数中,加入encrypt:true,就可以加密成功。
ctx.cookies.set("user","小笨",{
encrypt:true
})
加密成功了,再来获取这个值。如果你直接通过ctx.cookies.get( )方法获取,获取的是undefind,也就是无法获取的。这时候需要再次配置解密才可以使用, 在show( )方法里配置代码如下。
const user=ctx.cookies.get("user",{
encrypt:true
})
这时候到VSCode的终端中就可以获取这个解密后的值了。
Session的相关操作
Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。 Cookie和Session非常类似,Egg中的Session就存储在Cookie中,但是Session比Cookie的安全性更高。所以在开发中经常使用Cookie来保存是否登录,而用Session来保存登录信息和用户信息。
Session的添加
在开发中你理解为不重要的信息,可以公开的信息都可以临时存在Cookie里,但是隐私重要的信息,可以存在Session里,并且只允许在服务端进行操作。
先来看一下如何添加一个Session。打开/app/controller/han.js文件,在add( )方法里编写。
ctx.session.username='hanhan'
Session的获取
添加完成后,我们在index( )方法里获取一下 session 并展示在页面上。
async index() {
const { ctx } = this;
//获取Session
const username= ctx.session.username
await ctx.render(
'han.html',{
id:2021,
name:'小红',
age: 18,
//赋值给模板
username:username,
skills:[
'战神4',
'LOL',
'Controll'
]
})
}
在Session存在的情况下,我们已获得了Session的值,并且把获得的值发送给了模板。再到模板中修改模板,把值显示出来app/view/han.html。
<h3><%=username%></h3>
当我们点击按钮时,刷新页面,我们就可以看到显示的hanhan,说明添加和获取Session成功。
这时候打开浏览器的控制台的Application标签,可以看到多了一个EGG_SESS的Cookie,这个就是我们刚才存储的Session了
Session可以直接支持中文
比如现在我们要把Session中username的值换成娃哈哈爱喝爽歪歪,代码如下。
ctx.session.username='娃哈哈爱喝爽歪歪'
删除所有Cookie,然后再次点击增加Cookie按钮,然后再次刷新按钮,页面显示出了娃哈哈爱喝爽歪歪字样,说明中文的Session值也是完全可以使用的。
Session的删除
删除Session非常简单只需要把值设置为null就可以了。在del( )方法中编写下面的代码。
ctx.session.username=null
这样就可以删除session了。
Session的相关配置
配置Session的一些选项,需要到config.default.js文件中进行配置。
config.session = {
key :"PANG_SESS", // 设置Key的默认值
httpOnly:true, // 设置服务端操作
maxAge:1000*60 , // 设置最大有效时间
renew: true, // 页面有访问动作自动刷新session
}
Egg.js中间件
我们介绍了 Egg 是基于 Koa 实现的,所以 Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。
中间件的编写
Egg.js约定中间件要写在/app/middleware文件夹下面,如果没有middleware可以自己建立。在文件夹下,新建一个counter.js的文件。我们来作一下访问页面的次数这样的计数器。由于我们还没学数据库,所以把这个计数器的值保存在Session当中。
中间件需要 exports 一个普通的 function,接受两个参数:
- options: 中间件的配置项,框架会将
app.config[${middlewareName}]传递进来。 - app: 当前应用 Application 的实例。
module.exports = options =>{
return async (ctx,next)=>{
if(ctx.session.counter){
ctx.session.counter++
}else{
ctx.session.counter=1
}
await next();
}
};
中间件的全局使用
写完这个之后,我们需要手动的挂载中间件。挂载方法是打开/config/config.default.js文件。
config.middleware = ['counter'];
这样配置之后,就是全局的中间件,无论访问那个页面,计数器都会增加。为了看到效果。分别在\app\controller\home.js文件的index( )方法中 和 \app\controller\han.js的index( )方法中,加入控制台输出代码。
console.log(ctx.session.counter)
然后分别访问这两个页面,看到session都会增加。这就说明了中间件现在的作用域是全局的。
router中间件的使用
如果只想让访问/my时,计数器才会增加,这时候就要使用在router(路由)中配置中间件的使用。直接在\app\router.js 中实例化和挂载。
我们先去掉全局的挂载,然后到router.js文件中挂载单个路由。
'use strict';
module.exports = app => {
const counter = app.middleware.counter()
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/my',counter ,controller.han.index);
};
这时候在访问首页,计数器虽然会显示,但是不会增加。只有在访问/my路径的时候才会继续增加。也就是说中间件才会起作用。
这儿只是简单学习了中间件的用法,还有许多的中间件需要在官网中查看。
Egg.js的Extend-application
Egg虽然给我们提供了很多内置的方法,但有时候还是感觉不够用,这时候就需要我们自己对Egg中的方法进行扩展和编写了。
多种对象进行扩展
Egg.js可以对内部的五种对象进行扩展,给出了可扩展的对象、说明、this指向和使用方式。
对application 对象的方法扩展
app 对象指的是 Koa 的全局应用对象,全局只有一个,在应用启动时被创建。
按照Egg的约定,扩展的文件夹和文件的名字必须是固定的。比如我们要对application扩展,要在/app目录下,新建一个/extend文件夹,然后在建立一个application.js文件。
需求是作一个全局的获取时间的扩展。比如用app.currentTime( )这个全局的方法,就可以获得当前时间,并显示在页面上。
module.exports = {
//方法扩展
currentTime(){
const current = getTime();
return current;
}
};
function getTime(){
let now = new Date();
let year = now.getFullYear(); //得到年份
let month = now.getMonth()+1;//得到月份
let date = now.getDate();//得到日期
let hour= now.getHours();//得到小时数
let minute= now.getMinutes();//得到分钟数
let second= now.getSeconds();//得到秒数
let nowTime = year+'年'+month+'月'+date+'日 '+hour+':'+minute+':'+second;
return nowTime;
}
这样就完成了 一个application对象的方法扩展。扩展写好后,不用再作过多的配置,直接在一个Controller方法里使用就可以了。比如我们要在/app/controller/han.js的index( )方法里使用。
async index() {
const { ctx ,app } = this;
await ctx.render(
'han.html',{
nowTime: app.currentTime()
})
}
这样就可以在/app/view/han.html模板中使用了。
<%=nowTime%>
对application对象的属性扩展
一般来说属性的计算只需要进行一次,那么一定要实现缓存,否则在多次访问属性时会计算多次,这样会降低应用性能。
推荐的方式是使用 Symbol + Getter 的模式。
对属性( property) 的扩展的关键字是get,也需要写在application.js文件里。
module.exports = {
//方法扩展
currentTime(){
const current = getTime();
return current;
},
//属性扩展
get timeProp(){
return getTime();
}
};
加入get,就会默认是一个属性,可以直接以属性的形式在controller方法里进行调用。
Egg.js的Extend-context
Context 指的是 Koa 的请求上下文,这是 请求级别 的对象,每次请求生成一个 Context 实例,通常我们也简写成 ctx。
编写一个方法
在/app/controller/han.js文件里编写一个新的方法newContext( )。先用最简单的方式,在页面显示一个方法名称newContext就可以了。
async newContext(){
const {ctx} = this ;
ctx.body = 'newContext';
}
写完方法,到/app/router.js里,配置路由。
router.get('/newcontext',controller.han.newContext);
路由配置完成后,可以在终端中用yarn dev打开测试服务,然后在浏览器里输入[http://127.0.0.1:7001/newcontext]如果能正常在页面中显示出newContext就说明正常了,可以继续下面的操作。
编写Context扩展
以前通过上下文来获取传递参数时,get方法请求和post方法请求的获取方式是不同的,我们编写的方法就是让这两个请求获取参数的方法统一化,都用params( )方法。
在/app/extend文件夹下,新建一个文件context.js(此文件名称是Egg.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/controller/han.js文件,使用刚编写的params获取参数,然后打印在服务端控制台。
async newContext(){
const {ctx} = this ;
const params = ctx.params();
console.log(params)
ctx.body = 'newContext';
}
这样应该就可以接收到post和get请求的参数了
测试请求参数获得情况
get请求参数获取
直接在浏览器里输入
http://127.0.0.1:7001/newcontext?username=hanhan
在到VSCode的终端中查看结果,是可以接收到{ username: 'hanhan' }参数的。
post请求参数获取
get请求没问题了,先修改router.js,把请求方式改为post。
router.post('/newcontext',controller.han.newContext);
这里可以使用postman或者REST Client插件进行Post请求测试。在根目录下,打开test.http文件,然后编写测试代码。
POST http://127.0.0.1:7001/newcontext
Content-Type: application/json
{
"name":"小红",
"age":18
}
写好后,点击Send Request按钮测试,可以看到也是可以接收到POST传递的参数的。
17.Egg.js的Extend-request
Request 中的扩展一般是扩展的属性。比如扩展 Request 中的一个属性,通过属性直接得到请求头中的 token 属性。
我们新写一个方法,用来获取请求头中的token属性。所以我们在/app/controller/han.js中添加一个方法。
async newRequest(){
const { ctx } = this;
const token = ctx.request.token
ctx.body = {
status:200,
body:token
}
}
然后再到router.js中设置路由。
router.post("/newRequest", controller.han.newRequest);
request 对象扩展 token 属性
Egg.js 对 Request 的扩展也需要在/app/extend文件夹下,新建一个request.js文件,然后在这个文件里写扩展属性。
module.exports = {
get token() {
console.log("token", this.get("token"));
return this.get("token");
},
};
使用 REST Client 进行测试
写完上面的代码之后,就可以使用REST Client 插件进行测试了。编写测试代码如下。
POST http://127.0.0.1:7001/newRequest
Content-Type: application/json
token: 'hanhan'
{
"name":"小红",
"age":18
}
在点击send Request按钮,在 VSCdoe 控制台和请求后返回的数据中,就可以看到token了。这样一个简单的 Request 扩展就完成了。
18.Egg.js的Extend-response、helper
Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象,它提供了大量响应相关的属性和方法供使用。
编写Respose扩展
在/app/extend文件夹下,新建立一个response.js文件。我们的需求是和上节课向对应的,作一个设置token的扩展。打开response.js文件,编写下面的代码。
module.exports={
set token(token){
this.set('token',token)
}
};
需要设置的方法以set关键字开头,然后用this.set( )就可以设置返回的token了。
编写使用方法和路由
在/app/controll/han.js文件下,新建一个方法。然后用response.token来设置token 的值。
async newResponse(){
const {ctx} = this;
ctx.response.token='han.com'
ctx.body = 'newRespose'
}
然后设置路由,router.js 。
router.get('/newResponse',controller.han.newResponse);
在终端里开启服务yarn dev,然后输入http://127.0.0.1/newResponse。然后按F12打开浏览器的调试模式,然后进入Network标签,再次刷新浏览器,再选择All,点击newResponse。就可以看到token。
编写helper扩展
Helper 函数用来提供一些实用的 utility 函数。
它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数,这样可以用 JavaScript 来写复杂的逻辑,避免逻辑分散各处。另外还有一个好处是 Helper 这样一个简单的函数,可以让我们更容易编写测试用例。
我们作一个把字符串进行base64加密的方法。在/app/extend/文件夹下,新建一个helper.js文件。
module.exports = {
base64Encode(str = '' ){
return new Buffer(str).toString('base64');
}
}
虽然Buffer( )方法显示已经弃用,但是学习的时候还是可以使用的。
使用Helper扩展
我们省事一点,直接在/app/controller/han.js文件中的newResponse方法里,直接使用。
async newResponse(){
const {ctx} = this;
ctx.response.token='han.com'
const testBase64 = ctx.helper.base64Encode('han.com')
ctx.body = testBase64
}
然后到浏览器,刷新一下页面。就可以看到输出的结果编程了base64加密的文字了。
Egg.js的定时任务编写
虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务,例如:
- 定时上报应用状态。
- 定时从远程接口更新本地缓存。
- 定时进行文件切割、临时文件删除。
框架提供了一套机制来让定时任务的编写和维护更加优雅。
定时任务的编写
定时任务需要按照Egg的约定,/app目录下,新建shedule文件夹。然后再shedule文件夹下,新建一个get_time.js文件。设置每3秒钟,在控制台输出当前时间戳。
const Subscription = require('egg').Subscription
class GetTime extends Subscription{
static get schedule(){
return {
interval:'10s',//时间间隔
type:'worker'//每台机器上只有一个 worker 会执行这个定时任务,每次执行定时任务的 worker 的选择是随机的。
};
}
async subscribe(){
console.log(Date.now())
}
};
module.exports =GetTime;
更复杂的定时
我们也可以使用更复杂的cron属性进行定时。cron属性有6个参数。
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
比如我们还是要设置每3秒钟,返回时间戳,就可以写成下面的样子。
static get schedule(){
return {
cron: '*/3 * * * * *',
type:'worker'
};
}
20. Egg.js配置连接MySql数据库
此处的前提是你会数据库,不需要多精通,了解都可以 。此处我用的是MySql数据库。
安装egg-mysql插件
打开VSCode中的终端,然后在项目根目录下输入yarn命令进行安装。正常来讲安装的速度是非常快的。
yarn add egg-mysql -S
安装完成后,在终端中,再启动这个项目(你也可以先不启用)。
yarn dev
然后在项目根目录,找到并打开package.json文件,查看安装是否成功和对应的版本。
"dependencies": {
"egg": "^2.15.1",
"egg-mysql": "^3.0.0",
"egg-scripts": "^2.11.0",
"egg-view-ejs": "^2.0.1"
},
可以看到,我这里的egg-mysql版本为3.0.0.
配置egg-mysql插件
安装完的插件并不能正常使用,需要在plugin.js中配置插件。打开/config/plugin.js文件,然后在最后面编写。
exports.mysql = {
enable:true,
package:'egg-mysql'
}
然后再到/config/config.default.js当中进行 进一步配置。
config.mysql ={
app:true, //是否挂载到app下面
agent:false, //是否挂载到代理下面
client:{
host:'127.0.0.1', // 数据库地址
prot:'3306', // 端口
user:'root', // 用户名
password:'123456', // 密码
database:'test-egg' // 连接的数据库名称
}
}
如果这些连接信息正确,就可以连接成功了。
新建数据库test-egg
使用图形化界面创建一个数据库,使用什么软件都行,个人喜欢就好。
利用软件,新建一个数据库test-egg, 在新建一个girls表。
Egg.js操作MySQL数据库
由于对 MySQL 数据库的访问操作属于 Web 层中的数据处理层,因此我们强烈建议将这部分代码放在 Service 层中维护。
准备一下相关的方法
在/service文件夹下面,新建一个文件testdb.js文件,专门用于操作数据库。在文件中编写下面的方法。方法分别代表增、删、改、查。
"use strict";
const Service = require("egg").Service;
class testdbService extends Service {
// 添加数据库
async addGirl() {}
// 删除数据库
async delGirl() {}
// 修改数据库
async updateGirl() {}
// 查询数据库
async getGirls(id) {}
}
module.exports = testdbService;
写完之后在/controller文件夹下面新增一个控制器girlsManage.js文件,是对女孩的管理。
"use strict";
const Controller = require("egg").Controller;
class GirlManage extends Controller {
async addGirl() {
const { ctx } = this;
ctx.body = "添加女孩";
}
async delGirl() {
const { ctx } = this;
ctx.body = "删除女孩";
}
async updateGirl() {
const { ctx } = this;
ctx.body = "修改女孩";
}
async getGirls() {
const { ctx } = this;
ctx.body = "查询女孩";
}
}
module.exports = GirlManage;
配置相关路由,这里我们全部使用get请求方式,在router.js文件下加入下面的代码。
router.get("/addGirl", controller.girlManage.addGirl);
router.get("/delGirl", controller.girlManage.delGirl);
router.get("/updateGirl", controller.girlManage.updateGirl);
router.get("/getGirls", controller.girlManage.getGirls);
然后打开终端,开启服务,然后打开浏览器,输入地址,查看写的方法是否可用(http://127.0.0.1:7001/addGirl) 。
查询方法的编写
来学习一个最简单的方法,就是查询方法。进入/servic/testdb.js文件下的getGirls( )方法。然后编写代码。
// 查询数据库
async getGirl(id){
try{
const app = this.app;
const res= await app.mysql.select('girls')
return res
}catch(error){
console.log(error)
return null
}
}
写完代码以后,再编写/controller/girlManage.js文件下的getGirls( )方法。
async getGirls() {
const { ctx } = this;
const res = await ctx.service.testdb.getGirl()
ctx.body = '查询女孩:'+JSON.stringify(res);
}
这时候再到浏览器中进行预览,就可以看到可以返回数据了。
添加数据操作
打开/app/service/testdb.js文件,在addGirl( ) 方法下,编写下面的代码。这里插入使用的insert( )方法,
// 添加数据库
async addGirl(params){
try {
const { app } = this;
const res = await app.mysql.insert('girls',params);
return res;
} catch (error) {
console.log(error);
return null;
}
}
insert( ) 方法接收两个参数,第一个参数是表名称,第二个参数是插入到表里的值。
然后再到/app/controller/girlManage.js文件里addGirl( )方法里编写代码。
async addGirl() {
const { ctx } = this;
const params = {
name:'小白',
age:18,
skill:'头疗'
}
const res = await ctx.service.testdb.addGirl(params)
ctx.body = '添加女孩-成功!';
}
写完之后打开终端,运行yarn dev开启服务,然后在浏览器中输入http://127.0.0.1/addGirl就可以看到结果了。再打开Navicat for MySQL查看数据表的内容。如果没有任何错误,应该是可以顺利添加成功的。
修改数据操作
再来看一下如何来修改数据。打开/app/service/testdb.js文件,在updateGirl( )方法中编写代码。
// 修改数据库
async updateGirl(params){
try {
const { app } = this;
const res= await app.mysql.update('girls',params);
return res;
} catch (error) {
console.log(error);
return null;
}
}
然后再到/app/controller/girlManage.js文件中的updateGirl( )方法中,编写代码如下。
async updateGirl() {
const { ctx } = this;
const params = {
id:3,
name:'小白',
age:20,
skill:'头疗'
}
const res = await ctx.service.testdb.updateGirl(params)
if(res){
ctx.body = '修改女孩-成功';
}else{
ctx.body = '修改失败';
}
}
写完以后,就可以到浏览器中访问,http://127.0.0.1/updateGirl
删除数据操作
最后我们再看一下如何删除数据。还是到/app/service/testdb.js文件下的delGirl( )方法。编写下面的代码。
// 删除数据库
async delGirl(id){
try {
const { app } = this;
const res = await app.mysql.delete('girls',id)
return res;
} catch (error) {
console.log(error);
return null;
}
}
然后再到controller里边,进行编写delGirl( )方法。
async delGirl() {
const { ctx } = this;
const id={"id":3};
const res = await ctx.service.testdb.delGirl(id)
console.log(res)
if(res){
ctx.body = '删除女孩-成功';
}else{
ctx.body = '删除失败';
}
}
写好后,我们到浏览器里访问http://127.0.0.1/delGirl就可以了。
我基础就学到这里了。
补充
在VSCode中安装插件REST Client
在VSCode中的插件管理器,查找REST Client,安装。
在项目的根目录下,新建一个以.http结尾的文件,名字随意。然后
在里面编写
POST http://127.0.0.1:7001/add
Content-Type: application/x-www-form-urlencoded
name=han
或者
POST http://127.0.0.1:7001/add
Content-Type: application/json
{
"name":"憨憨"
}
需要注意的是,这个插件你需要完全按照这个格式来写,不能随意多出空行,否则就会发送失败。得不到数据。