Egg.js
注意:
小米商城:需要修改的东西
- md5加密
- tools工具方法的书写位置
- 中间件的文件与函数方法命名
- mongoose数据库命名规范
- 后台登陆的时候需要在前端后台进行做表单数据验证用户名和密码的判断
- 只查询一条数据的可以用findOne 替代find
- 注意修改管理员密码的时候需要前台验证,重复输入
- input readonly的样式
- 未分配角色bug,无法访问bug
- 菜单表示右侧的菜单
- 操作表示没有界面的操作
- 文件上个大小的错误警告
- 时间格式化插件: silly-datetime
- egg-view-ejs egg-mongoose
一、基础部分
创建
//npm 创建
npm init egg --type=simple
npm i
npm run dev
//yarn 创建 https://blog.yiiu.co/2018/04/20/eblog-egg/
1. npm 全局安装egg-init
2. egg-init --type=simple
3. yarn install
调试
- 打断点
- 先启动npm run debug
- 再 打开vscode调试选择 egg debug 文件启动
- 发送请求即可
app目录结构
/app
/controller
/public 静态资源,css,img
/view 视图层
/service 模型层次
/middleware 中间件(例如权限判断)
/extend 扩展写法
/config 配置文件
编写过程:MODEL->ROUTER->CONTROLLER->VIEW
基础语法
ctx对象
https://blog.yiiu.co/2018/04/20/eblog-egg/
controller里取值方法 请求数据有三种: query, params, body query取值方式:const id = this.ctx.request.query.id 适用于:/post?id=1 params 取值方式:const id = this.ctx.params.id 适用于:/post/1 body 取值方式 const id = this.ctx.request.body.id 适用于form表单提交 //相应消息内容 this.ctx.body = "响应的信息到前台"; //get let id = this.ctx.request.query.id; //post let id = this.ctx.request.body.id; //请求连接 console.log('ctx.request.url :', ctx.request.url);
获取get传值:
let query = this.ctx.query;动态路由(带参数路由)
router.get('/newslist/:id', )
加载模板引擎
egg-view-ejs
L6 extend写法对内置对象进行扩展,silly-datetime格式化时间,在模板中进行调用
全部配置属性
module.exports = appInfo => { return { logger: { dir: path.join(appInfo.baseDir, 'logs'), }, }; };
中间件 记得await next();
post 路由的页面不能直接用this.ctx.body返回,需要使用页面跳转this.ctx.redirect
字符串和objectid 的类型转换this.app.mongoose.Types.ObjectId(module_id);
config的default.js配置及获取
const userConfig = { proxy: { pageSize: 10 } } //控制器非路由方法中获取 const appkey = this.app.config.proxy.pageSize; //控制器路由方法 let pageSize = this.config.proxy.pageSize;
CSRF防范
方法一
// 方法一
class OrderController extends Controller {
//
async index() {
//渲染表单的时候把csrf带上
await this.ctx.render('order', {
csrf: this.ctx.csrf
});
}
// 打印post提交的数据
async submit() {
console.log(this.ctx.request.body);
}
}
方法二
利用中间件将 csrf传递到koa的state中成为全局变量
// 在/middleware/auth.js目录下添加中间件
module.exports=(option, app)=> {
return async function auth(ctx, next) {
//设置模板全局变量
ctx.state.csrf = ctx.csrf;
await next();
}
}
// default.config.js中引入中间件
config.middleware = ['auth'];
//之后再render中就不再需要传递csrf了
//////// 之后再表单的post请求时候带上_csrf
CryptoJS:
关于CryptoJS AES后输出hex的16进制和Base64的问题。 - 简书
[Use CryptoJS encrypt message by DES and direct decrypt ciphertext, compatible with Java Cipher.getInstance("DES") · GitHub]
JavaScript Crypto-JS 使用手册 – 抒写(https://gist.github.com/ufologist/5581486)
let mid = '12345';
let secretkey = '74aaaaa655aaaaab97fe3793aaaa2a';
let {packages, accountName, idCard, phone, address} = this.ctx.request.body;
let post_json = JSON.stringify({
mid,
packages,
accountName,
idCard,
phone,
address
});
//CRYPTOJS的toString的用法
// let secretkey = '74d4936557a54c3b97fe3793bbd1442a';
// let mid = '10536';
let keyHex = CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse(secretkey).toString(CryptoJS.enc.Hex));
let encrypted = CryptoJS.DES
.encrypt(post_json, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
.ciphertext.toString();
console.log(encrypted);
文件上传
- fieldname: type=file的字段名
- filename: 本地文件的文件名
- parts.fields 除文件外的其他字段组成的数组
- parts.field 除文件外的其他字段组成的对象
模板引擎
1.ejs
2.art-template
L7 利用中间件屏蔽指定IP
some方法
L9 cookies session
cookies保存在客户端
session保存在服务端,当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对,然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie,找到对应的session(value)。
cookie和session的必须为字符串
cookies
this.ctx.cookies.set('username', 'zhangsj');
this.ctx.cookies.get('username');
实现同一个浏览器访问同一个域的时候,不同页面之间的数据共享
实现数据的持久化
egg.js中的cookies默认情况下无法设置中文
//方法一 cookie加密之后可以设置中文 this.ctx.cookies.set('username', 'vhzngsj', { maxAge: 1000*60*60*24 //存储一天, httpOnly: true, signed: true, //对cookie进行签名防止用户手动修改 encrytp: true //对cookie进行加密, 获取的时候需要进行解密,加密之后的cookie可以设置中文 }); //方法二 console.log(new Buffer('hello, world!').toString('base64')); // 转换成 base64 字符串: aGVsbG8sIHdvcmxkIQ== console.log(new Buffer('aGVsbG8sIHdvcmxkIQ==', 'base64').toString()); // 还原 base64 字符串: hello, world!
cookie用JSON.stringify和JSON.parse可以存储对象
清除cookie
async loginOut() { //cookies 置空 this.ctx.cookies.set('unserinfo', null); //或者设置过期时间为0 this.ctx.cookies.set('username', null, { maxAge: 0 }); this.ctx.rediredc('/news'); //路由跳转 }
session
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对,然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie,找到对应的session(value)。
要访问session必须借助本地保存的cookies
使用
// 设置 this.ctx.session.userinfo = { name: '张三', age: '20' }, // 获取 var userinfo = this.ctx.session.username; // this.ctx.session.username = "";
session的配置
// /config.default.js config.session= { key: 'SESSION_ID', //设置session对应cookie在浏览器端的key maxAge: 2000, httpOnly: true, encrypt: true, renew: true //每次刷新页面时session会自动延期 } //或者 修改session 过期时间 this.ctx.session.maxAge = 2000; //秒
url重写 https://www.cnblogs.com/549294286/p/4462953.html
L8 中间件 middleware
全局中间
mdoule.exports=(options, app)=>{ return asycn function auth(ctx, next) { //xxxx await next(); } } //配置: config.middleware=['auth'] //传参 config.auth={ title: 'dfsfsf' }
路由中间件
- 在/middleware下定义中间件
- 在router.js下使用中间件
/router.js module.exports = app => { const { router, controller } = app; // 路由中获取中间件 const auth = app.middleware.auth({ attr: 'this is router.js middleware' }); //配置路由中间件 router.get('/', auth,controller.home.index); router.get('/news', controller.news.index); router.get('/shop', controller.shop.index); };
框架默认中间节
//config.default.js
config.bodyParser={ jsonLimit: '10mb' //Default is 1mb. }
使用koa
- 规范的(标准的)koa中间件
//koa-jsonp // 1.安装 npm install koa-jsonp --save //引入 /middleware/jsonp.js let jsonp = require('koa-jsonp'); module.exports= jsonp; //在config.default.js config.middleware=['jsonp'];
//koa-compress
//1. 安装
//npm i koa-compress -S
//中间件的配置:
//config.default.js
config.cmpress = {
threshold: 1024
}
非标准中间件的引入
```js
const webpackMiddleware = require('some-koa-middleware');
module.exports = (options, app) => {
return webpackMiddleware(options.compiler, options.others);
}- KOA获取域名 this.ctx.protocol /this.ctx.host / hostname/this.ctx.origin 协议,主机带端口,不带端口,完整域名
通用中间件配置
//config.default.js config.compress = { enable: false, match: '/news', ignore: '/news', // ignore和match是相反的配置,不能同时配置到同一个中间件中 //通过match()方法配置 mathc(ctx) { // ctx 上下文 可以获得请求地址 console.log(ctx.request.url); if(ctx.request.url=='/shop' || ctx.request.url=='/news') { return true; } return false; } threshold: 1024 }
中间件配合项目 LESSON13
中间件命名, auth_api.js => authApi
路由配置的
- 路由命名
- 路由重定向
- 外部重定向
this.ctx.status = 301;
this.ctx.redirect('\nnn');
- 内部重定向
//302 301重定向
// 有利于seo优化
router.redirect('\news', '\', 302);
- 路由分组
//新建router文件夹进行分组
// app/router/admin.js
module.exports = app => {
app.router.get('/admin/user', app.controller.admin.user);
app.router.get('/admin/log', app.controller.admin.log);
};
// 在router.js中进行引入
// app/router.js
module.exports = app => {
require('./router/news')(app);
require('./router/admin')(app);
};
BaseControllerde,兼容写法
content特殊用法
过几秒跳转到某个网页
在/app/core/base.js中定义base基类, 继承
控制器的兼容性写法
'use strict'; const Controller = require('egg').Controller; class HomeController extends Controller { //把ctx当做参数传入 等同于在函数内使用 this.ctx async index(ctx) { await ctx.render('home'); } } module.exports = HomeController;
定时任务schedule--爬虫
定时执行某些任务,定时清理缓存,定时报告
/app/schedule
watchfile.js
1.继承schedule写法
2.导出对象写法
3.导出函数,函数返回对象
cherrio 模块爬虫
- 安装cnpm i cheerio --save
- 加载要解析的内容
const $=cheerio.load('
Hello world
") - 用法
$('title').html()
获取了要匹配的标题的内容 - 抓取网站内容
var url="http://news.baidu.com//";
- 解析数据
MongoDB L18-32
2. mongodb简单使用
安装插件
npm i egg-mongo-native --save聚合管道:关联查询与数据统计
$project 增加、删除、重命名字段 投影筛选
$match 条件匹配。只满足条件的文档才能进入下
$limit 限制结果的数量
$skip 跳过文档的数量
$sort 条件排序
$group 条件组合结果 统计$lookup 用以引入其它集合的数据 (表关联查询)
scheme-》表-》集合
定义model-》操作数据库
3. L31 在egg中使用mongoose
安装npm i egg-mongoose --save
在/app/model中操作数据库
字符串和objectid 的类型转换this.app.mongoose.Types.ObjectId(module_id);
schema表结构
- 特殊类型goods_id: { type: type: Schema.Types.ObjectId }
关联查询:对id的查询要现将字符串类型转换为 ObjectID类型,
let _id = this.ctx.session.proxyuserinfo._id; // console.log('--------_id---------'); // console.log(_id); result = await this.ctx.model.Proxy.aggregate([ { $lookup: { from: 'proxy', localField: '_id', foreignField: 'pid', as: 'items' } }, { $match: { _id } } ]); } //从订单表order中查商品goods,order表是主表格,localField是order表中的外键名称,foreignField是在goods表中 let result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } } ]);
增删改查
//增 let access = new this.ctx.model.Access(addResult); await access.save(); //改 let result = await this.ctx.model.ContractTpl.update({ _id }, form); console.log(result); //查 let result = await this.ctx.model.Access.find({ module_id: '0' }); //删除多个 $in let result = await this.ctx.model.ContractTplAttr.find({ tplId: tplId }); // console.log(result); let tplIdArr = []; if (result) { for (let i = 0; i < result.length; i++) { tplIdArr.push(result[i]._id); } } if (tplIdArr) { result = await this.ctx.model.ContractTplAttr.deleteMany({ _id: { $in: tplIdArr } }) }
筛选查询
// 注意筛选查询的顺序 $look->$sort->筛选 const result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'order_item', localField: '_id', foreignField: 'order_id', as: 'orderItems', }, }, { $sort: {"add_time":-1} //时间 }, { $match:{"uid":this.app.mongoose.Types.ObjectId(uid)} //条件 }, { $skip: (page - 1) * pageSize, }, { $limit: pageSize, } ]);
mongodb查询时间 https://stackoverflow.com/questions/11973304/mongodb-mongoose-querying-at-a-specific-date https://blog.csdn.net/qq_27093465/article/details/53647930
文件下载
excel的mime类型: https://stackoverflow.com/questions/4212861/what-is-a-correct-mime-type-for-docx-pptx-etc
https://github.com/eggjs/examples 文件下载实例
https://github.com/eggjs/egg/issues/965 文件下载git问题
//获取将要提供下载的文件的绝对路径 const filePath = path.resolve('./hello.xlsx'); console.log('-------filePath--------'); console.log(filePath); //读取文件到缓冲区 const buf = fs.readFileSync(filePath); //删除文件 fs.unlinkSync(filePath); //设置下载时候的文件名 this.ctx.attachment('hello.xlsx'); //设置下载文件的mime类型 this.ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); //文件赋值给body,下载 this.ctx.body = buf;
二、EGG实践笔记
egg-jwt的用法
https://github.com/okoala/egg-jwt/blob/master/test/fixtures/apps/jwt-app.jwt/app/controller/success.js
用jsonwebtoken对application进行了extend进而实现egg-jwt
用法:
npm 安装
plgin.js
// {app_root}/config/plugin.js exports.jwt = { enable: true, package: "egg-jwt" };
config.js
// {app_root}/config/config.default.js exports.jwt = { secret: "123456" };
在路由中使用,在router.js 中引入然后在router中使用即可
全局使用,通过app.js引入使用:
//config.default.js中 config.jwt = { secret: '123456', enable: true, ignore: '/login', } ///////////剩下两步不知道啥用处 //app.js中引入 module.exports = app => { app.config.appMiddleware.unshift('jwtErrorHandler'); }; //???????? exports.keys = 'egg-jwt'; //?????
html生成pdf
https://www.cnblogs.com/daysme/p/10250224.html
centos 7安装phantomjs
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
yum install bzip2 # 安装bzip2
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
mv phantomjs-2.1.1-linux-x86_64 /usr/local/src/phantomjs
ln -sf /usr/local/src/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
yum install fontconfig freetype2
phantomjs -v # 测试版本号
安装中文字体
yum install bitmap-fonts bitmap-fonts-cjk
yum groupinstall "fonts" -y # 安装字体相关的依赖包
fc-cache # 刷新字体缓存
三、小米商城
- 验证码: svg-captcha 插件
L39登陆功能
全局指定中间件登陆路由
权限判断:
- 获取表单提交的数据
- 判断验证码是否正确
- 验证码正确
- 密码md5加密
- 安装 md5模块 npm install md5 -S
- 引入 const md5 = require('md5');
- 使用 md5(str)
- 在用户集合 中查询当前用户是否存在
- egg-mongoose 安装
- 创建数据库
- 安装数据库
- 如果数据库有用户信息: 保存用户信息到session 跳转到后台
- 密码md5加密
- 验证码错误 : 跳转到登录页面 提示验证码错误
- 验证码正确
egg-mongoose 安装 配置 L31
安装 npm i egg-mongoose -S
/config/plugin.js
//mongodb://127.0.0.1/eggcms //mongodb://eggadmin:123456@localhost:27017/eggcms exports.mongoose = { client: { url: 'mongodb://127.0.0.1/eggcms', options: {}, }, };
/config/config.default.js
创建model /model user.js
module.exports = app => { const mongoose = app.mongoose; /*引入建立连接的mongoose */ const Schema = mongoose.Schema; //数据库表的映射 const UserSchema = new Schema({ username: { type: String }, password: { type: String }, status:{ type:Number, default:1 } }); return mongoose.model('User', UserSchema,'user'); }
在控制器中使用mongoose.find
'use strict'; const Controller = require('egg').Controller; class UserController extends Controller { async index() { var userList=await this.service.user.getUserList(); console.log(userList); this.ctx.body='我是用户页面'; } async addUser() { //增加数据 var user=new this.ctx.model.User({ username:'李四', password:'123456' }); var result=await user.save(); console.log(result) this.ctx.body='增加用户成功'; } async editUser() { //增加数据 await this.ctx.model.User.updateOne({ "_id":"5b84d4405f66f20370dd53de" },{ username:"哈哈哈", password:'1234' },function(err,result){ if(err){ console.log(err); return; } console.log(result) }) this.ctx.body='修改用户成功'; } async removeUser() { //增加数据 var rel =await this.ctx.model.User.deleteOne({"_id":"5b84d4b3c782f441c45d8bab"}); cnsole.log(rel); this.ctx.body='删除用户成功'; } } module.exports = UserController;
RBAC权限管理模型
L40 RBAC管理模型概述
- 用户
- 角色
- 权限
用户属于某种角色角,色拥有权限
L41-42 RBAC角色管理 增删改
L42 用户管理用户增删改
- 定义model
- 注意修改管理员密码的时候需要前台验证,重复输入
L45 权限管理
- 菜单 与 操作
- /model/access.js
- 模块名称,例如,管理员管理,角色管理,权限管理
- 操作名称:例如 管理员的增删改查
- 节点类型:
- url:操作地址
- module_id 和access 自关联的表 如果module=0 表示为模块,
- 数据库mogoose的mixed 混合类型
- 在权限表access中,moduleid 要保存为objectid类型 /access/doAdd
- 注意权限列表 自己和自己关联
L46 权限修改
权限的级联删除,级联更新(模块修改之后,属于模块的操作将无法显示)
L47 授权
- 一次传送多个checkbox, 使用数组[]
- model的文件命名含有下划线则访问model的时候要使用驼峰式命名
L51公共的ajax方法---特别注意
注意几个点: dom中的this,jquery中的$(function)
- es6属性名表达式
- dom this
- dom $init()
L52单文件上传
form 表单中必须加 enctype="multipart/form-data"。表单默认提交数据的方式是
application/x-www-form-urlencoded 不是不能上传文件,是只能上传文本格式的文件,
multipart/form-data 是将文件以二进制的形式上传,这样可以实现多种类型的文件上传。enctype="multipart/form-data"以后,后台将没法通过 this.ctx.request.body来接收表单的数据。
需要使用egg-multipart
注意上传文件时候的csrf的写法
安装pump模块,防止module卡死
上传文件的stream示例
FileStream { _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: [Object], tail: [Object], length: 1 }, length: 63967, pipes: null, pipesCount: 0, flowing: null, ended: false, endEmitted: false, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: true, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: [Object: null prototype] { end: [Function] }, _eventsCount: 1, _maxListeners: undefined, truncated: false, _read: [Function], //重点部分 fieldname: 'focus_img', filename: 's3.png', encoding: '7bit', transferEncoding: '7bit', mime: 'image/png', mimeType: 'image/png' }
L53多文件上传
可以将除了文件的其它字段提取到 parts 的 filed 中
getFileStream, 上传且只能上传一个文件
//注意在循环读取多个文件的时候使用continue时要把stream消费掉 //上传到本地模式 //循环读取多个文件 while ((stream = await parts()) != null) { if (!stream.filename) { //await pump(stream, writeStream); //写入并销毁当前流 (egg demo提供的) //continue; 销毁 break; } // 表单中的file类型的name let fieldname = stream.fieldname; // 图片上传的目录 let dir = await this.service.tools.getUploadFile(stream.filename); let target = dir.uploadDir; let writeStream = fs.createWriteStream(target); await pump(stream, writeStream); //写入并销毁当前流 (egg demo提供的) files = Object.assign(files, { [fieldname]: dir.saveDir }); } //上传到oss模式 //上传文件到oss async doAdd() { let parts = this.ctx.multipart({ autoFields: true }); //autoFileds可以将除了文件的其它字段提取到 parts 的 filed 中 let files = {}; let stream; console.log('---------fieldname--------'); //循环读取多个文件 while ((stream = await parts()) != null) { if (!stream.filename) { break; } // 表单中的file类型的name let fieldname = stream.fieldname; let d = await this.service.tools.getTime(); let name ='goods/' +d+ path.extname(stream.filename); const result = await this.ctx.oss.put(name, stream); files = Object.assign(files, { [fieldname]: result.url }); } // 写入数据库 let goods = new this.ctx.model.Goods(Object.assign(files, parts.field)); await goods.save(); console.log('------files--------------'); console.log(Object.assign(files, parts.field)); await this.success('/admin/goods', '添加号卡成功'); }
L54轮播轮上传
- 封装函数,以时间为单位存储轮播图
- /service/tools
- silly-datetime
- 创建文件夹的库 mz-module/mkdir
- https://github.com/node-modules/mz-modules
- path.extname(filename)获取文件后缀名称
- ObjectAssign的用法
L56单击数量方法
- 注意es6属性表达式
- jquery时间连缀写法
- jquery的基本操作
- 在浏览器中为了兼容性尽量不要使用es6的语法
L57 helper扩展修改日期格式
- css手风琴收缩特效
- css箭头方向变换特效 /public/admin/css/basic.css
- css命名规范base.js toogleAside $().find()
- iframe重构后台菜单结构
- iframe target
- $(window).resize
L59商品表概览
- 实现商品类型的增删改查
- 实现商品类型属性的增删改查,并实现类型和类型对应属性的关联
- 实现商品分类的增删改查,并实现商品分类表的自关联
- 实现商品模块的增删改查、并且实现商品和商品分类、商品类型、颜色等其他表的关联
注意: 在/service和/model中多个单词的文件命名使用下划线隔开,调用时使用大驼峰
而控制器中使用小驼峰命名,使用时用小驼峰。
L60 商品类型属性
goodsTypeAttribute中的cate_id关联到商品类型(手机、电视)
attr_type属性的录入方式,check_box,
L61 商品类型属性的添加
设置iframe的高度,在/view/main/index.html
多个组件的事件触发
$(function(){ $("input[name='attr_type']").change(function(){ console.log($(this).val()); if($(this).val()==3){ $('#attr_value').attr('disabled',false) }else{ $('#attr_value').attr('disabled',true) } }); })
3. iframe找不到
### L62 商品类型属性的修改
//添加分类
**注意**checked==true的用法, /view/goodsTypeAttribute/edit.html
### l63 商品分类增删改查
1. 商品分类和商品关联,用于商品的筛选
2. 上传图片
3. string类型和objectid 类型转换
4. **jimp包动态生成缩略图**
- npm i --save jimp
- 引入·
```js
//上传图片成功以后生成缩略图
Jimp.read(target, (err, lenna) => {
if (err) throw err;
lenna
.resize(200, 200) // resize
.quality(90) // set JPEG quality
.write(target + '_200x200' + path.extname(target)); // save
});
L64商品分类的增加**
- 注意二级列表,可以进行无限级列表
L65 goods_cate的编辑
商品分类的修改
注意busboy的坑,multipart上传上传时候有表单field数量的限制,注意
在config.default.js中进行配置//配置表单数量 exports.multipart = { fields: '50' };
L66添加商品布局
商品界面布局:
添加商品属性的后台界面编写
'注意:'默认列表合并 /pulic/admin/js/base.js
toggleAside
$('.aside>li:nth-child(1) ul, .aside>li:nth-childe(2) ul, .aside>li:nth-childe(3) ul').hide();
bootstrap tab切换 bootstrap tabs
在page_header中引入bootstrap.min.js
需要自己增加颜色的增删改查
$(function) $.get() $().hide()
L67商品颜色、类型属性、相册
- 颜色
- 规格
L68-L69 商品描述wysiwyg editor
- 针对某些地址关闭csrf验证
404 路由错误
403 csrf错误
500 服务器错误
L70 商品相册批量上传
L75 数据分页+jqPaginator
mongodb分页
db.表名.find().skip((page-1)*pageSize).limit(pageSize)
#规定每页 8 条数据的查询方式 #第一页 查询0-7条数据 db.表名.find().skip(0).limit(8) #查询第二页(page=2): db.表名.find().skip(8).limit(8) #查询第四页(page=4): db.表名.find().skip(24).limit(8)
mongoose分页 https://mongoosejs.com/docs/queries.html
Person. find({ occupation: /host/, 'name.last': 'Ghost', age: { $gt: 17, $lt: 66 }, likes: { $in: ['vaporizing', 'talking'] } }). limit(10). sort({ occupation: -1 }). select({ name: 1, occupation: 1 }). exec(callback); // Using query builder Person. find({ occupation: /host/ }). where('name.last').equals('Ghost'). where('age').gt(17).lt(66). where('likes').in(['vaporizing', 'talking']). limit(10). sort('-occupation'). select('name occupation'). exec(callback);
jqPaginator
引入jQuery
定义一个div,div的class为pagination
jqPaginator初始化
$("#page").jqpaginatork({ totalPages: 100, visiblePages:10, currentPage:1, onPagechange:function(num, type){ $('#text').html('当前第'+num+'页'); }});
聚合管道+计数
async index() { let proxy_id = this.app.mongoose.Types.ObjectId(this.ctx.request.query.id); let proxy = await this.ctx.model.Proxy.find({ _id: proxy_id }); let page = this.ctx.request.query.page || 1; //每一页数量 let pageSize = 10; //获取数据总数量 let totalNum = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } }, { //计数 $group: { _id: proxy_id, count: { $sum: 1 } } } ]); if(totalNum[0] != null) { totalNum = totalNum[0].count; } else { totalNum = 0; } let result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } }, { //跳过 $skip: (page-1)*pageSize }, { //限制 $limit: pageSize } ]); // console.log(JSON.stringify(result); await this.ctx.render('sell/order/index', { list: result, proxy: proxy[0], proxy_id, totalPages: Math.ceil(totalNum / pageSize), page }); }
L121前后端分离跨域
- 版本号区分
https://a.itying.com/api1/newslist 或 https://a.itying.com/api2/newslist
- 在 RESTful 架构中,每个网址代表一种资源(resource),所以网址中建议不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以 API 中的名词也应该使用复数。
- http请求数据方式:
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- DELETE(DELETE):从服务器删除资源。
- 不常用三个
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
egg-cors 允许跨域
npm i egg-cors --save //plug.js cors = { enable: true, package: 'egg-cors' } //在egg.config.js中配置 //和允许的请求方法 config.cors = { origin: '*', allowMethos: 'GET, HEAD, PUT, DELETE, POST, PATCH' } //允许的域名 config.security ={ //csrf 忽略某些url casrf: { ignore: ctx=> { if(ctx.request.url.indexOf('/api') != -1) { return true; } else { return false; } } } //不知道这个有啥用? csrf白名单,如果没有配置或者为空则默认csrf放过所有域名 domainWhiteList: ['http://localhost:8080'] }
L122 cookie session失效
//config.default.js
exports.cors = {
credentials: true;
origin: '*', //cookie需要修改为具体地址
allowMethods: 'GET, PUT, POST, DELETE',
credentials: true //cookies跨域
}
前端:
{credentials: true}
Egg.js
注意:
小米商城:需要修改的东西
- md5加密
- tools工具方法的书写位置
- 中间件的文件与函数方法命名
- mongoose数据库命名规范
- 后台登陆的时候需要在前端后台进行做表单数据验证用户名和密码的判断
- 只查询一条数据的可以用findOne 替代find
- 注意修改管理员密码的时候需要前台验证,重复输入
- input readonly的样式
- 未分配角色bug,无法访问bug
- 菜单表示右侧的菜单
- 操作表示没有界面的操作
- 文件上个大小的错误警告
- 时间格式化插件: silly-datetime
- egg-view-ejs egg-mongoose
一、基础部分
创建
//npm 创建
npm init egg --type=simple
npm i
npm run dev
//yarn 创建 https://blog.yiiu.co/2018/04/20/eblog-egg/
1. npm 全局安装egg-init
2. egg-init --type=simple
3. yarn install
调试
- 打断点
- 先启动npm run debug
- 再 打开vscode调试选择 egg debug 文件启动
- 发送请求即可
app目录结构
/app
/controller
/public 静态资源,css,img
/view 视图层
/service 模型层次
/middleware 中间件(例如权限判断)
/extend 扩展写法
/config 配置文件
编写过程:MODEL->ROUTER->CONTROLLER->VIEW
基础语法
ctx对象
https://blog.yiiu.co/2018/04/20/eblog-egg/
controller里取值方法 请求数据有三种: query, params, body query取值方式:const id = this.ctx.request.query.id 适用于:/post?id=1 params 取值方式:const id = this.ctx.params.id 适用于:/post/1 body 取值方式 const id = this.ctx.request.body.id 适用于form表单提交 //相应消息内容 this.ctx.body = "响应的信息到前台"; //get let id = this.ctx.request.query.id; //post let id = this.ctx.request.body.id; //请求连接 console.log('ctx.request.url :', ctx.request.url);
获取get传值:
let query = this.ctx.query;动态路由(带参数路由)
router.get('/newslist/:id', )
加载模板引擎
egg-view-ejs
L6 extend写法对内置对象进行扩展,silly-datetime格式化时间,在模板中进行调用
全部配置属性
module.exports = appInfo => { return { logger: { dir: path.join(appInfo.baseDir, 'logs'), }, }; };
中间件 记得await next();
post 路由的页面不能直接用this.ctx.body返回,需要使用页面跳转this.ctx.redirect
字符串和objectid 的类型转换this.app.mongoose.Types.ObjectId(module_id);
config的default.js配置及获取
const userConfig = { proxy: { pageSize: 10 } } //控制器非路由方法中获取 const appkey = this.app.config.proxy.pageSize; //控制器路由方法 let pageSize = this.config.proxy.pageSize;
CSRF防范
方法一
// 方法一
class OrderController extends Controller {
//
async index() {
//渲染表单的时候把csrf带上
await this.ctx.render('order', {
csrf: this.ctx.csrf
});
}
// 打印post提交的数据
async submit() {
console.log(this.ctx.request.body);
}
}
方法二
利用中间件将 csrf传递到koa的state中成为全局变量
// 在/middleware/auth.js目录下添加中间件
module.exports=(option, app)=> {
return async function auth(ctx, next) {
//设置模板全局变量
ctx.state.csrf = ctx.csrf;
await next();
}
}
// default.config.js中引入中间件
config.middleware = ['auth'];
//之后再render中就不再需要传递csrf了
//////// 之后再表单的post请求时候带上_csrf
CryptoJS:
关于CryptoJS AES后输出hex的16进制和Base64的问题。 - 简书
[Use CryptoJS encrypt message by DES and direct decrypt ciphertext, compatible with Java Cipher.getInstance("DES") · GitHub]
JavaScript Crypto-JS 使用手册 – 抒写(https://gist.github.com/ufologist/5581486)
let mid = '12345';
let secretkey = '74aaaaa655aaaaab97fe3793aaaa2a';
let {packages, accountName, idCard, phone, address} = this.ctx.request.body;
let post_json = JSON.stringify({
mid,
packages,
accountName,
idCard,
phone,
address
});
//CRYPTOJS的toString的用法
// let secretkey = '74d4936557a54c3b97fe3793bbd1442a';
// let mid = '10536';
let keyHex = CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse(secretkey).toString(CryptoJS.enc.Hex));
let encrypted = CryptoJS.DES
.encrypt(post_json, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
.ciphertext.toString();
console.log(encrypted);
文件上传
- fieldname: type=file的字段名
- filename: 本地文件的文件名
- parts.fields 除文件外的其他字段组成的数组
- parts.field 除文件外的其他字段组成的对象
模板引擎
1.ejs
2.art-template
L7 利用中间件屏蔽指定IP
some方法
L9 cookies session
cookies保存在客户端
session保存在服务端,当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对,然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie,找到对应的session(value)。
cookie和session的必须为字符串
cookies
this.ctx.cookies.set('username', 'zhangsj');
this.ctx.cookies.get('username');
实现同一个浏览器访问同一个域的时候,不同页面之间的数据共享
实现数据的持久化
egg.js中的cookies默认情况下无法设置中文
//方法一 cookie加密之后可以设置中文 this.ctx.cookies.set('username', 'vhzngsj', { maxAge: 1000*60*60*24 //存储一天, httpOnly: true, signed: true, //对cookie进行签名防止用户手动修改 encrytp: true //对cookie进行加密, 获取的时候需要进行解密,加密之后的cookie可以设置中文 }); //方法二 console.log(new Buffer('hello, world!').toString('base64')); // 转换成 base64 字符串: aGVsbG8sIHdvcmxkIQ== console.log(new Buffer('aGVsbG8sIHdvcmxkIQ==', 'base64').toString()); // 还原 base64 字符串: hello, world!
cookie用JSON.stringify和JSON.parse可以存储对象
清除cookie
async loginOut() { //cookies 置空 this.ctx.cookies.set('unserinfo', null); //或者设置过期时间为0 this.ctx.cookies.set('username', null, { maxAge: 0 }); this.ctx.rediredc('/news'); //路由跳转 }
session
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对,然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie,找到对应的session(value)。
要访问session必须借助本地保存的cookies
使用
// 设置 this.ctx.session.userinfo = { name: '张三', age: '20' }, // 获取 var userinfo = this.ctx.session.username; // this.ctx.session.username = "";
session的配置
// /config.default.js config.session= { key: 'SESSION_ID', //设置session对应cookie在浏览器端的key maxAge: 2000, httpOnly: true, encrypt: true, renew: true //每次刷新页面时session会自动延期 } //或者 修改session 过期时间 this.ctx.session.maxAge = 2000; //秒
url重写 https://www.cnblogs.com/549294286/p/4462953.html
L8 中间件 middleware
全局中间
mdoule.exports=(options, app)=>{ return asycn function auth(ctx, next) { //xxxx await next(); } } //配置: config.middleware=['auth'] //传参 config.auth={ title: 'dfsfsf' }
路由中间件
- 在/middleware下定义中间件
- 在router.js下使用中间件
/router.js module.exports = app => { const { router, controller } = app; // 路由中获取中间件 const auth = app.middleware.auth({ attr: 'this is router.js middleware' }); //配置路由中间件 router.get('/', auth,controller.home.index); router.get('/news', controller.news.index); router.get('/shop', controller.shop.index); };
框架默认中间节
//config.default.js
config.bodyParser={ jsonLimit: '10mb' //Default is 1mb. }
使用koa
- 规范的(标准的)koa中间件
//koa-jsonp // 1.安装 npm install koa-jsonp --save //引入 /middleware/jsonp.js let jsonp = require('koa-jsonp'); module.exports= jsonp; //在config.default.js config.middleware=['jsonp'];
//koa-compress
//1. 安装
//npm i koa-compress -S
//中间件的配置:
//config.default.js
config.cmpress = {
threshold: 1024
}
非标准中间件的引入
```js
const webpackMiddleware = require('some-koa-middleware');
module.exports = (options, app) => {
return webpackMiddleware(options.compiler, options.others);
}- KOA获取域名 this.ctx.protocol /this.ctx.host / hostname/this.ctx.origin 协议,主机带端口,不带端口,完整域名
通用中间件配置
//config.default.js config.compress = { enable: false, match: '/news', ignore: '/news', // ignore和match是相反的配置,不能同时配置到同一个中间件中 //通过match()方法配置 mathc(ctx) { // ctx 上下文 可以获得请求地址 console.log(ctx.request.url); if(ctx.request.url=='/shop' || ctx.request.url=='/news') { return true; } return false; } threshold: 1024 }
中间件配合项目 LESSON13
中间件命名, auth_api.js => authApi
路由配置的
- 路由命名
- 路由重定向
- 外部重定向
this.ctx.status = 301;
this.ctx.redirect('\nnn');
- 内部重定向
//302 301重定向
// 有利于seo优化
router.redirect('\news', '\', 302);
- 路由分组
//新建router文件夹进行分组
// app/router/admin.js
module.exports = app => {
app.router.get('/admin/user', app.controller.admin.user);
app.router.get('/admin/log', app.controller.admin.log);
};
// 在router.js中进行引入
// app/router.js
module.exports = app => {
require('./router/news')(app);
require('./router/admin')(app);
};
BaseControllerde,兼容写法
content特殊用法
过几秒跳转到某个网页
在/app/core/base.js中定义base基类, 继承
控制器的兼容性写法
'use strict'; const Controller = require('egg').Controller; class HomeController extends Controller { //把ctx当做参数传入 等同于在函数内使用 this.ctx async index(ctx) { await ctx.render('home'); } } module.exports = HomeController;
定时任务schedule--爬虫
定时执行某些任务,定时清理缓存,定时报告
/app/schedule
watchfile.js
1.继承schedule写法
2.导出对象写法
3.导出函数,函数返回对象
cherrio 模块爬虫
- 安装cnpm i cheerio --save
- 加载要解析的内容
const $=cheerio.load('
Hello world
") - 用法
$('title').html()
获取了要匹配的标题的内容 - 抓取网站内容
var url="http://news.baidu.com//";
- 解析数据
MongoDB L18-32
2. mongodb简单使用
安装插件
npm i egg-mongo-native --save聚合管道:关联查询与数据统计
$project 增加、删除、重命名字段 投影筛选
$match 条件匹配。只满足条件的文档才能进入下
$limit 限制结果的数量
$skip 跳过文档的数量
$sort 条件排序
$group 条件组合结果 统计$lookup 用以引入其它集合的数据 (表关联查询)
scheme-》表-》集合
定义model-》操作数据库
3. L31 在egg中使用mongoose
安装npm i egg-mongoose --save
在/app/model中操作数据库
字符串和objectid 的类型转换this.app.mongoose.Types.ObjectId(module_id);
schema表结构
- 特殊类型goods_id: { type: type: Schema.Types.ObjectId }
关联查询:对id的查询要现将字符串类型转换为 ObjectID类型,
let _id = this.ctx.session.proxyuserinfo._id; // console.log('--------_id---------'); // console.log(_id); result = await this.ctx.model.Proxy.aggregate([ { $lookup: { from: 'proxy', localField: '_id', foreignField: 'pid', as: 'items' } }, { $match: { _id } } ]); } //从订单表order中查商品goods,order表是主表格,localField是order表中的外键名称,foreignField是在goods表中 let result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } } ]);
增删改查
//增 let access = new this.ctx.model.Access(addResult); await access.save(); //改 let result = await this.ctx.model.ContractTpl.update({ _id }, form); console.log(result); //查 let result = await this.ctx.model.Access.find({ module_id: '0' }); //删除多个 $in let result = await this.ctx.model.ContractTplAttr.find({ tplId: tplId }); // console.log(result); let tplIdArr = []; if (result) { for (let i = 0; i < result.length; i++) { tplIdArr.push(result[i]._id); } } if (tplIdArr) { result = await this.ctx.model.ContractTplAttr.deleteMany({ _id: { $in: tplIdArr } }) }
筛选查询
// 注意筛选查询的顺序 $look->$sort->筛选 const result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'order_item', localField: '_id', foreignField: 'order_id', as: 'orderItems', }, }, { $sort: {"add_time":-1} //时间 }, { $match:{"uid":this.app.mongoose.Types.ObjectId(uid)} //条件 }, { $skip: (page - 1) * pageSize, }, { $limit: pageSize, } ]);
mongodb查询时间 https://stackoverflow.com/questions/11973304/mongodb-mongoose-querying-at-a-specific-date https://blog.csdn.net/qq_27093465/article/details/53647930
文件下载
excel的mime类型: https://stackoverflow.com/questions/4212861/what-is-a-correct-mime-type-for-docx-pptx-etc
https://github.com/eggjs/examples 文件下载实例
https://github.com/eggjs/egg/issues/965 文件下载git问题
//获取将要提供下载的文件的绝对路径 const filePath = path.resolve('./hello.xlsx'); console.log('-------filePath--------'); console.log(filePath); //读取文件到缓冲区 const buf = fs.readFileSync(filePath); //删除文件 fs.unlinkSync(filePath); //设置下载时候的文件名 this.ctx.attachment('hello.xlsx'); //设置下载文件的mime类型 this.ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); //文件赋值给body,下载 this.ctx.body = buf;
二、EGG实践笔记
egg-jwt的用法
https://github.com/okoala/egg-jwt/blob/master/test/fixtures/apps/jwt-app.jwt/app/controller/success.js
用jsonwebtoken对application进行了extend进而实现egg-jwt
用法:
npm 安装
plgin.js
// {app_root}/config/plugin.js exports.jwt = { enable: true, package: "egg-jwt" };
config.js
// {app_root}/config/config.default.js exports.jwt = { secret: "123456" };
在路由中使用,在router.js 中引入然后在router中使用即可
全局使用,通过app.js引入使用:
//config.default.js中 config.jwt = { secret: '123456', enable: true, ignore: '/login', } ///////////剩下两步不知道啥用处 //app.js中引入 module.exports = app => { app.config.appMiddleware.unshift('jwtErrorHandler'); }; //???????? exports.keys = 'egg-jwt'; //?????
html生成pdf
https://www.cnblogs.com/daysme/p/10250224.html
centos 7安装phantomjs
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
yum install bzip2 # 安装bzip2
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
mv phantomjs-2.1.1-linux-x86_64 /usr/local/src/phantomjs
ln -sf /usr/local/src/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
yum install fontconfig freetype2
phantomjs -v # 测试版本号
安装中文字体
yum install bitmap-fonts bitmap-fonts-cjk
yum groupinstall "fonts" -y # 安装字体相关的依赖包
fc-cache # 刷新字体缓存
三、小米商城
- 验证码: svg-captcha 插件
L39登陆功能
全局指定中间件登陆路由
权限判断:
- 获取表单提交的数据
- 判断验证码是否正确
- 验证码正确
- 密码md5加密
- 安装 md5模块 npm install md5 -S
- 引入 const md5 = require('md5');
- 使用 md5(str)
- 在用户集合 中查询当前用户是否存在
- egg-mongoose 安装
- 创建数据库
- 安装数据库
- 如果数据库有用户信息: 保存用户信息到session 跳转到后台
- 密码md5加密
- 验证码错误 : 跳转到登录页面 提示验证码错误
- 验证码正确
egg-mongoose 安装 配置 L31
安装 npm i egg-mongoose -S
/config/plugin.js
//mongodb://127.0.0.1/eggcms //mongodb://eggadmin:123456@localhost:27017/eggcms exports.mongoose = { client: { url: 'mongodb://127.0.0.1/eggcms', options: {}, }, };
/config/config.default.js
创建model /model user.js
module.exports = app => { const mongoose = app.mongoose; /*引入建立连接的mongoose */ const Schema = mongoose.Schema; //数据库表的映射 const UserSchema = new Schema({ username: { type: String }, password: { type: String }, status:{ type:Number, default:1 } }); return mongoose.model('User', UserSchema,'user'); }
在控制器中使用mongoose.find
'use strict'; const Controller = require('egg').Controller; class UserController extends Controller { async index() { var userList=await this.service.user.getUserList(); console.log(userList); this.ctx.body='我是用户页面'; } async addUser() { //增加数据 var user=new this.ctx.model.User({ username:'李四', password:'123456' }); var result=await user.save(); console.log(result) this.ctx.body='增加用户成功'; } async editUser() { //增加数据 await this.ctx.model.User.updateOne({ "_id":"5b84d4405f66f20370dd53de" },{ username:"哈哈哈", password:'1234' },function(err,result){ if(err){ console.log(err); return; } console.log(result) }) this.ctx.body='修改用户成功'; } async removeUser() { //增加数据 var rel =await this.ctx.model.User.deleteOne({"_id":"5b84d4b3c782f441c45d8bab"}); cnsole.log(rel); this.ctx.body='删除用户成功'; } } module.exports = UserController;
RBAC权限管理模型
L40 RBAC管理模型概述
- 用户
- 角色
- 权限
用户属于某种角色角,色拥有权限
L41-42 RBAC角色管理 增删改
L42 用户管理用户增删改
- 定义model
- 注意修改管理员密码的时候需要前台验证,重复输入
L45 权限管理
- 菜单 与 操作
- /model/access.js
- 模块名称,例如,管理员管理,角色管理,权限管理
- 操作名称:例如 管理员的增删改查
- 节点类型:
- url:操作地址
- module_id 和access 自关联的表 如果module=0 表示为模块,
- 数据库mogoose的mixed 混合类型
- 在权限表access中,moduleid 要保存为objectid类型 /access/doAdd
- 注意权限列表 自己和自己关联
L46 权限修改
权限的级联删除,级联更新(模块修改之后,属于模块的操作将无法显示)
L47 授权
- 一次传送多个checkbox, 使用数组[]
- model的文件命名含有下划线则访问model的时候要使用驼峰式命名
L51公共的ajax方法---特别注意
注意几个点: dom中的this,jquery中的$(function)
- es6属性名表达式
- dom this
- dom $init()
L52单文件上传
form 表单中必须加 enctype="multipart/form-data"。表单默认提交数据的方式是
application/x-www-form-urlencoded 不是不能上传文件,是只能上传文本格式的文件,
multipart/form-data 是将文件以二进制的形式上传,这样可以实现多种类型的文件上传。enctype="multipart/form-data"以后,后台将没法通过 this.ctx.request.body来接收表单的数据。
需要使用egg-multipart
注意上传文件时候的csrf的写法
安装pump模块,防止module卡死
上传文件的stream示例
FileStream { _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: [Object], tail: [Object], length: 1 }, length: 63967, pipes: null, pipesCount: 0, flowing: null, ended: false, endEmitted: false, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: true, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: [Object: null prototype] { end: [Function] }, _eventsCount: 1, _maxListeners: undefined, truncated: false, _read: [Function], //重点部分 fieldname: 'focus_img', filename: 's3.png', encoding: '7bit', transferEncoding: '7bit', mime: 'image/png', mimeType: 'image/png' }
L53多文件上传
可以将除了文件的其它字段提取到 parts 的 filed 中
getFileStream, 上传且只能上传一个文件
//注意在循环读取多个文件的时候使用continue时要把stream消费掉 //上传到本地模式 //循环读取多个文件 while ((stream = await parts()) != null) { if (!stream.filename) { //await pump(stream, writeStream); //写入并销毁当前流 (egg demo提供的) //continue; 销毁 break; } // 表单中的file类型的name let fieldname = stream.fieldname; // 图片上传的目录 let dir = await this.service.tools.getUploadFile(stream.filename); let target = dir.uploadDir; let writeStream = fs.createWriteStream(target); await pump(stream, writeStream); //写入并销毁当前流 (egg demo提供的) files = Object.assign(files, { [fieldname]: dir.saveDir }); } //上传到oss模式 //上传文件到oss async doAdd() { let parts = this.ctx.multipart({ autoFields: true }); //autoFileds可以将除了文件的其它字段提取到 parts 的 filed 中 let files = {}; let stream; console.log('---------fieldname--------'); //循环读取多个文件 while ((stream = await parts()) != null) { if (!stream.filename) { break; } // 表单中的file类型的name let fieldname = stream.fieldname; let d = await this.service.tools.getTime(); let name ='goods/' +d+ path.extname(stream.filename); const result = await this.ctx.oss.put(name, stream); files = Object.assign(files, { [fieldname]: result.url }); } // 写入数据库 let goods = new this.ctx.model.Goods(Object.assign(files, parts.field)); await goods.save(); console.log('------files--------------'); console.log(Object.assign(files, parts.field)); await this.success('/admin/goods', '添加号卡成功'); }
L54轮播轮上传
- 封装函数,以时间为单位存储轮播图
- /service/tools
- silly-datetime
- 创建文件夹的库 mz-module/mkdir
- https://github.com/node-modules/mz-modules
- path.extname(filename)获取文件后缀名称
- ObjectAssign的用法
L56单击数量方法
- 注意es6属性表达式
- jquery时间连缀写法
- jquery的基本操作
- 在浏览器中为了兼容性尽量不要使用es6的语法
L57 helper扩展修改日期格式
- css手风琴收缩特效
- css箭头方向变换特效 /public/admin/css/basic.css
- css命名规范base.js toogleAside $().find()
- iframe重构后台菜单结构
- iframe target
- $(window).resize
L59商品表概览
- 实现商品类型的增删改查
- 实现商品类型属性的增删改查,并实现类型和类型对应属性的关联
- 实现商品分类的增删改查,并实现商品分类表的自关联
- 实现商品模块的增删改查、并且实现商品和商品分类、商品类型、颜色等其他表的关联
注意: 在/service和/model中多个单词的文件命名使用下划线隔开,调用时使用大驼峰
而控制器中使用小驼峰命名,使用时用小驼峰。
L60 商品类型属性
goodsTypeAttribute中的cate_id关联到商品类型(手机、电视)
attr_type属性的录入方式,check_box,
L61 商品类型属性的添加
设置iframe的高度,在/view/main/index.html
多个组件的事件触发
$(function(){ $("input[name='attr_type']").change(function(){ console.log($(this).val()); if($(this).val()==3){ $('#attr_value').attr('disabled',false) }else{ $('#attr_value').attr('disabled',true) } }); })
3. iframe找不到
### L62 商品类型属性的修改
//添加分类
**注意**checked==true的用法, /view/goodsTypeAttribute/edit.html
### l63 商品分类增删改查
1. 商品分类和商品关联,用于商品的筛选
2. 上传图片
3. string类型和objectid 类型转换
4. **jimp包动态生成缩略图**
- npm i --save jimp
- 引入·
```js
//上传图片成功以后生成缩略图
Jimp.read(target, (err, lenna) => {
if (err) throw err;
lenna
.resize(200, 200) // resize
.quality(90) // set JPEG quality
.write(target + '_200x200' + path.extname(target)); // save
});
L64商品分类的增加**
- 注意二级列表,可以进行无限级列表
L65 goods_cate的编辑
商品分类的修改
注意busboy的坑,multipart上传上传时候有表单field数量的限制,注意
在config.default.js中进行配置//配置表单数量 exports.multipart = { fields: '50' };
L66添加商品布局
商品界面布局:
添加商品属性的后台界面编写
'注意:'默认列表合并 /pulic/admin/js/base.js
toggleAside
$('.aside>li:nth-child(1) ul, .aside>li:nth-childe(2) ul, .aside>li:nth-childe(3) ul').hide();
bootstrap tab切换 bootstrap tabs
在page_header中引入bootstrap.min.js
需要自己增加颜色的增删改查
$(function) $.get() $().hide()
L67商品颜色、类型属性、相册
- 颜色
- 规格
L68-L69 商品描述wysiwyg editor
- 针对某些地址关闭csrf验证
404 路由错误
403 csrf错误
500 服务器错误
L70 商品相册批量上传
L75 数据分页+jqPaginator
mongodb分页
db.表名.find().skip((page-1)*pageSize).limit(pageSize)
#规定每页 8 条数据的查询方式 #第一页 查询0-7条数据 db.表名.find().skip(0).limit(8) #查询第二页(page=2): db.表名.find().skip(8).limit(8) #查询第四页(page=4): db.表名.find().skip(24).limit(8)
mongoose分页 https://mongoosejs.com/docs/queries.html
Person. find({ occupation: /host/, 'name.last': 'Ghost', age: { $gt: 17, $lt: 66 }, likes: { $in: ['vaporizing', 'talking'] } }). limit(10). sort({ occupation: -1 }). select({ name: 1, occupation: 1 }). exec(callback); // Using query builder Person. find({ occupation: /host/ }). where('name.last').equals('Ghost'). where('age').gt(17).lt(66). where('likes').in(['vaporizing', 'talking']). limit(10). sort('-occupation'). select('name occupation'). exec(callback);
jqPaginator
引入jQuery
定义一个div,div的class为pagination
jqPaginator初始化
$("#page").jqpaginatork({ totalPages: 100, visiblePages:10, currentPage:1, onPagechange:function(num, type){ $('#text').html('当前第'+num+'页'); }});
聚合管道+计数
async index() { let proxy_id = this.app.mongoose.Types.ObjectId(this.ctx.request.query.id); let proxy = await this.ctx.model.Proxy.find({ _id: proxy_id }); let page = this.ctx.request.query.page || 1; //每一页数量 let pageSize = 10; //获取数据总数量 let totalNum = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } }, { //计数 $group: { _id: proxy_id, count: { $sum: 1 } } } ]); if(totalNum[0] != null) { totalNum = totalNum[0].count; } else { totalNum = 0; } let result = await this.ctx.model.Order.aggregate([ { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', as: 'items' } }, { $match: { proxy_id } }, { //跳过 $skip: (page-1)*pageSize }, { //限制 $limit: pageSize } ]); // console.log(JSON.stringify(result); await this.ctx.render('sell/order/index', { list: result, proxy: proxy[0], proxy_id, totalPages: Math.ceil(totalNum / pageSize), page }); }
L121前后端分离跨域
- 版本号区分
https://a.itying.com/api1/newslist 或 https://a.itying.com/api2/newslist
- 在 RESTful 架构中,每个网址代表一种资源(resource),所以网址中建议不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以 API 中的名词也应该使用复数。
- http请求数据方式:
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- DELETE(DELETE):从服务器删除资源。
- 不常用三个
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
egg-cors 允许跨域
npm i egg-cors --save //plug.js cors = { enable: true, package: 'egg-cors' } //在egg.config.js中配置 //和允许的请求方法 config.cors = { origin: '*', allowMethos: 'GET, HEAD, PUT, DELETE, POST, PATCH' } //允许的域名 config.security ={ //csrf 忽略某些url casrf: { ignore: ctx=> { if(ctx.request.url.indexOf('/api') != -1) { return true; } else { return false; } } } //不知道这个有啥用? csrf白名单,如果没有配置或者为空则默认csrf放过所有域名 domainWhiteList: ['http://localhost:8080'] }
L122 cookie session失效
//config.default.js
exports.cors = {
credentials: true;
origin: '*', //cookie需要修改为具体地址
allowMethods: 'GET, PUT, POST, DELETE',
credentials: true //cookies跨域
}
前端:
{credentials: true}