# 大前端项目笔记
# Docker入门使用🚙
安装并配置加速地址 (opens new window)
常用命令
docker ps 查看进程
docker ps -a 查看所有进程
systemctl restart docker 重启服务
docker stop 服务名
docker rm 服务名
实操
docker run --name root -e MYSQL_ROOT_PASSWORD=123456 -p 8270:3306 -d mysql 拉取mysql docker镜像并且开启服务 将端口号映射到8270端口
docker ps 查看进程
docker stop root 停止root mysql服务
docker rm root 销毁mysql服务
docker logs -f root 查看服务具体日志
docker
服务不需要考虑环境问题就能跑起服务,非常有效
docker-compose
集合命令 用于创建多个docker
镜像服务
#下载:
curl -L https://get.daocloud.io/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
#权限:
chmod a+x /usr/local/bin/docker-compose
#查看版本:
docker-compose --version
创建docker-compose.yml
version: '3'
services:
mysql1:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=123456
ports:
- 8271:3306
mysql2:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=123456
ports:
- 8272:3306
# Use root/example as user/password credentials
version: '3.1'
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example
运行 docker-compose up -d
部分实例实战
service docker start 启动docker服务
# 创建持久化sql服务
docker run --restart=always --name root -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql
# 数据持久化
docker run --restart=always --name root -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d -v /home/mysql_local_data:/data/sql mysql
# 创建持久化redis服务,建议数据持久化
docker run -itd --restart=always --name redis-test -p 8271:6379 -v/home/redistest1:/data redis redis-server --requirepass 123456
# 创建持久化mongdb服务
docker run -d --restart=always --name mongo-test -p 27017:27017 mongo
# 数据持久化
docker run --name my_mongo -v /home/mongo_local_data:/data/db --rm -d -p 27017:27017 mongo
# 创建持久化nginx服务
docker run --restart=always --name mynginx -p 80:80 -d nginx:latest
# 区分生产环境和线上环境 运行springboot
java -jar 生成的jar包 --spring.profiles.active=prod
# 后台运行
nohub java -jar 生成的jar包 --spring.profiles.active=prod
#运行日志文件输出
nohup java -jar imissyou-0.0.1-SNAPSHOT.jar >temp.txt &
nohup java -jar imissyou-0.0.1-SNAPSHOT.jar >log.log 2>&1 &
# ps -ef
查看当前所有进程
# kill -9 Pid
杀死某个进程
# 查看端口号情况 不加后面的查看所有的端口号
netstat -ano |findstr 8080
# 杀死进程
taskkill -PID 进程号 -F
# 连接远程服务器
ssh root@47.97.180.232
# Nginx学习🉑
# 反向代理解决跨域问题(待补充学习)
nginx.conf
server
{
listen 80;
server_name yuba.yangxiansheng.top;
location / {
proxy_pass https://yuba.douyu.com;
add_header Access-Control-Allow-Origin *;
}
#禁止访问的文件或目录
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
{
return 404;
}
#一键申请SSL证书验证目录相关设置
location ~ \.well-known{
allow all;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
error_log off;
access_log /dev/null;
}
location ~ .*\.(js|css)?$
{
expires 12h;
error_log off;
access_log /dev/null;
}
access_log /www/wwwlogs/ceshi.yangxiansheng.top.log;
error_log /www/wwwlogs/ceshi.yangxiansheng.top.error.log;
}
# ls
conf.d fastcgi_params mime.types modules nginx-conf nginx.conf scgi_params uwsgi_params
# vim nginx-conf
# cat nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
# vim nginx.conf
server {
listen 8888;
server_name localhost;
location / {
proxy_pass https://api.juejin.cn;
add_header Access-Control-Allow-Origin *;
}
}
server {
listen 8999;
server_name localhost;
charset utf-8;
root /Users/zyb/Desktop/upload;
autoindex on;
add_header Cache-Control "no-cache, must-revalidate";
location / {
add_header Access-Control-Allow-Origin *;
}
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
~
~
~
~
-- INSERT -- 33,2 Bot
# 项目开发
# Sass学习⭐️
sass和之前学习的stylus相同,只不过语法方面有点不同
语法
- sass必须要有大括号和分号结尾
css函数
定义函数
@function px2rem($px){
@return $px/$ratio +rem;
}
@mixin center{
display:center;
justify-content:center;
align-items:center;
}
使用方法
@include
+函数名
css变量
$text-large:20px;
使用案例
@import "./mixin";
$text-large: px2rem(18);
$text-big: px2rem(16);
$text-medium: px2rem(14);
$text-small: px2rem(12);
$text-tiny: px2rem(10);
$text-large-lh: px2rem(20);
$text-big-lh: px2rem(18);
$text-medium-lh: px2rem(16);
$text-small-lh: px2rem(15);
$text-tiny-lh: px2rem(12);
$text-big-max-height3: px2rem(54);
$text-medium-max-height3: px2rem(48);
$text-samll-max-height3: px2rem(42);
$text-big-max-height2: px2rem(36);
$text-medium-max-height2: px2rem(32);
$text-medium-max-height: px2rem(16);
$text-small-max-height2: px2rem(30);
$text-small-max-height: px2rem(15);
$text-tiny-max-height: px2rem(12);
.title-big {
line-height: $text-big-lh;
font-size: $text-big;
max-height: $text-big-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(3);
}
.title-medium {
font-size: $text-medium;
line-height: $text-medium-lh;
max-height: $text-medium-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(3);
}
.title-small {
font-size: $text-small;
line-height: $text-small-lh;
max-height: $text-small-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(2);
}
.sub-title-medium {
line-height: $text-medium-lh;
font-size: $text-medium;
max-height: $text-medium-max-height2;
color: #666;
@include ellipsis2(2);
}
.sub-title {
line-height: $text-small-lh;
font-size: $text-small;
max-height: $text-small-max-height;
color: #666;
@include ellipsis2(1);
}
.sub-title-tiny {
line-height: $text-tiny-lh;
font-size: $text-tiny;
max-height: $text-tiny-max-height;
color: #666;
@include ellipsis2(1);
}
.third-title {
line-height: $text-small-lh;
font-size: $text-small;
max-height: $text-small-max-height;
color: #999;
@include ellipsis2(1);
}
.third-title-tiny {
line-height: $text-tiny-lh;
font-size: $text-tiny;
max-height: $text-tiny-max-height;
color: #999;
@include ellipsis2(1);
}
# Element-Ui
# 安装并使用
安装并且使用
npm i element-ui -S
import 'element-ui/lib/theme-chalk/index.css'
import ElementUI from 'element-ui'
Vue.use(ElementUI)
# 按需加载项目
npm install babel-plugin-component -D
然后修改 babel.config.js
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
引入组件
import {Button} from 'element-ui'
Vue.use(Button)
单独js文件引入
import { Message } from 'element-ui' Message({ type: 'error', message: error.response.data.msg, duration: 4000 })
完整的组件按需引入
import {
Pagination,
Dialog,
Autocomplete,
Dropdown,
DropdownMenu,
DropdownItem,
Menu,
Submenu,
MenuItem,
MenuItemGroup,
Input,
InputNumber,
Radio,
RadioGroup,
RadioButton,
Checkbox,
CheckboxButton,
CheckboxGroup,
Switch,
Select,
Option,
OptionGroup,
Button,
ButtonGroup,
Table,
TableColumn,
DatePicker,
TimeSelect,
TimePicker,
Popover,
Tooltip,
Breadcrumb,
BreadcrumbItem,
Form,
FormItem,
Tabs,
TabPane,
Tag,
Tree,
Alert,
Slider,
Icon,
Row,
Col,
Upload,
Progress,
Spinner,
Badge,
Card,
Rate,
Steps,
Step,
Carousel,
CarouselItem,
Collapse,
CollapseItem,
Cascader,
ColorPicker,
Transfer,
Container,
Header,
Aside,
Main,
Footer,
Timeline,
TimelineItem,
Link,
Divider,
Image,
Calendar,
Backtop,
PageHeader,
CascaderPanel,
Loading,
MessageBox,
Message,
Notification
} from 'element-ui';
Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Autocomplete);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Menu);
Vue.use(Submenu);
Vue.use(MenuItem);
Vue.use(MenuItemGroup);
Vue.use(Input);
Vue.use(InputNumber);
Vue.use(Radio);
Vue.use(RadioGroup);
Vue.use(RadioButton);
Vue.use(Checkbox);
Vue.use(CheckboxButton);
Vue.use(CheckboxGroup);
Vue.use(Switch);
Vue.use(Select);
Vue.use(Option);
Vue.use(OptionGroup);
Vue.use(Button);
Vue.use(ButtonGroup);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(DatePicker);
Vue.use(TimeSelect);
Vue.use(TimePicker);
Vue.use(Popover);
Vue.use(Tooltip);
Vue.use(Breadcrumb);
Vue.use(BreadcrumbItem);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Tabs);
Vue.use(TabPane);
Vue.use(Tag);
Vue.use(Tree);
Vue.use(Alert);
Vue.use(Slider);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.use(Progress);
Vue.use(Spinner);
Vue.use(Badge);
Vue.use(Card);
Vue.use(Rate);
Vue.use(Steps);
Vue.use(Step);
Vue.use(Carousel);
Vue.use(CarouselItem);
Vue.use(Collapse);
Vue.use(CollapseItem);
Vue.use(Cascader);
Vue.use(ColorPicker);
Vue.use(Transfer);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Timeline);
Vue.use(TimelineItem);
Vue.use(Link);
Vue.use(Divider);
Vue.use(Image);
Vue.use(Calendar);
Vue.use(Backtop);
Vue.use(PageHeader);
Vue.use(CascaderPanel);
Vue.use(Loading.directive);
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
# 表单组件🐤
表单引入
model
绑定表单对象,inline
设置单行显示,:rules
设置表单的校验规则
<el-form
label-position="top"
:rules="rules"
:model="userInfo"
ref="form"
>
<el-form-item class="item" label="邮箱地址" prop="userName">
<el-input v-model="userInfo.userName" placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item class="item" label="登录密码" prop="password">
<el-input
show-password
v-model="userInfo.password"
placeholder="密码"
></el-input>
</el-form-item>
<el-form-item class="item-code" label="验证码" prop="code">
<el-row type="flex" justify="space-between">
<el-col :span="12">
<el-input
v-model="userInfo.code"
placeholder="验证码"
></el-input>
</el-col>
<el-col :span="12">
<span @click="_getCode()" v-html="code"></span>
</el-col>
</el-row>
</el-form-item>
<el-row class="link">
<el-col :offset="20"
><el-link type="primary" @click="Toreset()"
>忘记密码</el-link
></el-col
>
</el-row>
<el-form-item>
<el-button style="width:100%" type="primary" @click="onSubmit"
>登录</el-button
>
</el-form-item>
</el-form>
表单校验常用
在
data
中定义校验规则对象,例如:rules
,然后再form
引入,并指定每个表单项的校验规则先指定规则
:rules
,然后指定元素的校验规则prop="规则里面的对象"
<el-form inline :model="data" :rules="rules" ref="form"> <el-form-item label="审批人" prop="user">
编写校验规则,
rules
参考async-validator (opens new window)
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback()
} else {
callback(new Error('用户名长度必须大于3'))
}
}
return {
data: {
user: 'sam',
region: '区域二'
},
rules: {
// user的校验规则 可以为数组
user: [
// 参考async-validator trigger: 什么时候触发,常用的是blur失焦 change改变,message:错误信息
{ required: true, trigger: 'change', message: '用户名必须录入' },
// 自定义校验规则
{ validator: userValidator, trigger: 'change',message:'校验不符合规范' }
]
}
}
},
// 最后提交也需要校验
method:{
submitForm(formName) {
this.$refs.form.validate(async(valid) => {
if (!valid) {
return false
}
})
}
}
校验参考
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
date1: [
{ type: 'date', required: true, message: '请选择日期', trigger: 'change' }
],
date2: [
{ type: 'date', required: true, message: '请选择时间', trigger: 'change' }
],
type: [
{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
}
};
password: [
{
required: true, message: '密码为必填项', trigger: 'blur'
},
{
pattern: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, message: '密码应包含大写字母小写字母和数字和特殊字符', trigger: 'blur'
}
],
- 表单常见属性 参考属性 (opens new window)
# 其他组件用法总结
布局
默认24分栏
一行 一列
:span
指定分布区域大小
:gutter
指定每列间隔 ,定义在行
:offset
指定分栏偏移,定义在列
<el-row :gutter="20">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
指定flex布局
<el-row type="flex" class="row-bg">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="center">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="end">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-around">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
弹出框
Tab
解决重复请求问题 切换到另一个
tab
才请求数据,记录对象,如果重复点击 直接退出
data() {
return {
activeName: 'first',
x: {
name: 'first'
}
};
},
methods: {
handleClick(tab, event) {
if(this.x.name !== this.activeName){
// 第一次点击 或者点击到其他tab
this.x = tab
console.log('success')
}else if(tab.name === this.x.name){
return
}
}
}
选择框
<el-select v-model="collegeValue" clearable style="margin-left:6px" placeholder="请选择学院" @change="OnselectCollege">
<el-option
v-for="item in collegeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
options = [{label:'1',value:'1'}...]
消息框
this.$confirm('确定要删除这名学生的信息吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
分页器
<el-pagination
:background="true"
:page-sizes="[10,15,20,25]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
分页逻辑
// 初始化分页数据
async initData() {
this.listLoading = true
const res = await Student.getList(this.page, this.pageSize)
if (res.code === 200) {
this.totalPage = res.data.total_page
this.totalCount = res.data.total
this.listLoading = false
this.list = res.data.items
} else {
this.$message.error(res.msg)
}
},
// 监听分页器分页大小
handleSizeChange(value) {
this.pageSize = value
this.initData()
},
// 监听当前页改变
handleCurrentChange(value) {
this.page = value - 1
this.initData()
},
Dialog
<el-dialog title="收货地址" :visible.sync="dialogFormVisible">
<el-form :model="form">
<el-form-item label="活动名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="活动区域" :label-width="formLabelWidth">
<el-select v-model="form.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogFormVisible = false">确 定</el-button>
</div>
</el-dialog>
文件上传
<el-upload
class="avatar-uploader"
action="https://imgkr.com/api/files/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>
// 用户头像处理
handleAvatarSuccess(res, file) {
this.userInfo.avatar = res.data
console.log(this.userInfo)
},
beforeAvatarUpload(file) {
const formatType = file.type === 'image/jpeg' || 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!formatType) {
this.$message.error('上传头像图片只能是 JPG或者PNG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return formatType && isLt2M
}
# 登录注册找回密码🙉🙉
# 验证码接口
这个使用到了svg-capthcha
插件生成验证码
async getCode(ctx,next){
const newCaptcha = svgCaptcha.create({
// 指定验证长度
size:4,
// 指定忽略的字符
ignoreChars:"0o1il",
color:true,
// 指定干扰线数量
noise:Math.floor(Math.random()*5),
width: 150,
height: 38
})
ctx.body={
code:200,
data:newCaptcha.data
}
}
改造验证码接口,这里我们使用redis
验证码的值放进缓存,然后设置时效性
首先前端通过uuid
生成随机串码,然后存储到localstorage
再保存在vuex
中 ,vuex
使用mixin
混入
storeSid(){
let sid
if(localStorage.getItem('sid')){
sid =localStorage.getItem('sid')
}else{
sid = uuid()
localStorage.setItem('sid',sid)
}
this.setSid(sid)
}
// 前端发送请求
const sid = this.sid
const code = await Public.getCode(sid)
后端保存
const sid = ctx.request.query.sid
// 十分钟键值
setValue(sid,newCaptcha.text,10*60)
# 登录注册接口(jwt鉴权)📢
引入
koa-jwt
鉴权,然后编写登陆注册接口
首先引入koa-jwt
插件帮助我们快速集成jwt鉴权
在app.js
中使用
const JWT = require('koa-jwt')
const config = require('./config/config')
app.use(exception)
// 选择鉴权跳过的路由前缀 比如公共路由
app.use(JWT({ secret: config.JWT_SECRET }).unless({ path: [/^\/v1\/public/] }))
如果接口是保护的接口,就会抛出401
状态码,这里需要对异常进行处理**,全局异常处理**参考异常处理 (opens new window)
鉴权异常
Http异常 未知异常
const { HttpExecption } = require('../core/http-execption')
// 定义全局异常中间件
const eception = async (ctx, next) => {
try {
await next()
} catch (error) {
const IsHttpexecption = error instanceof HttpExecption
// 校验token
if(error.status == 401){
ctx.status = 401
ctx.body = {
msg: 'token令牌不合法',
code: 401,
request: `${ctx.method} ${ctx.path}`
}
}else{
// 如果是Http已知异常
if (IsHttpexecption) {
ctx.body = {
msg: error.msg,
code: error.errorCode,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = error.code
} else {
ctx.body = {
msg: '未知异常发生',
code: 999,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = 500
}
}
}
}
了解jwt
的API
jwt.sign()
// 生成令牌 需要传入参数:1.传入自定义信息(后面可封装在auth里) 2.secretKey秘钥(用户自定义) 3.配置(失效时间)单位为秒
expiresIn: expressed in seconds or a string describing a time span zeit/ms.
Eg: 60, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms")
例:jwt.sign(
{uid,scope},secretKey,{expiresIn}
)
//1h 第一种方式
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: 'foobar'
}, 'secret');
//1h 第二种方式 推荐
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });
jwt.verify() //校验令牌 如果token无效会抛出异常
// 需要传入 token 秘钥 两个个参数
最好是放在try catch中捕获异常
try{
var decode = jwt.verify(UserToken,secretKey)
// 校验令牌合法 不合法抛出异常
}catch(error){
if (error.name == 'TokenExpiredError') {
errMsg = 'token令牌已经过期'
}
throw new ForbidenException(errMsg)
}
登录逻辑:
- 接收用户的数据
- 验证图形验证码的有效性,正确性
- 数据库判断用户名密码(比较盐)是否正确
- 返回
token
相关业务代码
// 登录
async login(ctx, next) {
const v = await new LoginValidator().validate(ctx)
const userInfo = {
userName: v.get('body.userName'),
password: v.get('body.password'),
code: v.get('body.code'),
sid: v.get('body.sid'),
}
// 校验验证码合法性
const result = await UserService._checkCode(userInfo.sid, userInfo.code)
if (result) {
// 比对数据库
const res = await User.findOne({ userName: userInfo.userName })
if(res == null){
throw new NotFoundException('用户名不存在')
}
// 比对密码
const checkpwd = bcryptjs.compareSync(userInfo.password, res.password)
if (checkpwd) {
const token = JWT.sign({ _id: 'pi' }, config.JWT_SECRET, {
expiresIn: '2 days',
})
ctx.body = {
code: 200,
msg: '登录成功',
data: {
userInfo: res,
token,
},
}
} else {
// 用户名密码错误
throw new NotFoundException('密码错误')
}
} else {
// 验证码不正确异常
throw new ParameterException('验证码错误')
}
}
明文加密
```js const bcryptjs = require('bcryptjs') // 保存加密密码 const sault = bcryptjs.genSaltSync(10) const pwd = bcryptjs.hashSync(body.passsword,sault) const user = new User({ userName:userName, password:pwd }) user.save() // 比较密码 const user = User.findOne({userName:body.userName}) // 后面是加密的 const result = bcryptjs.compareSync(body.password,user.password) ```注册逻辑:
- 接收数据,处理数据
- 核验验证码正确性和有效性
- 比对数据中的
userName
是否唯一,插入数据
// 注册
async register(ctx, next) {
const v = await new RegisterValidator().validate(ctx)
const userInfo = {
userName: v.get('body.userName'),
password: v.get('body.password'),
code: v.get('body.code'),
sid: v.get('body.sid'),
nickName: v.get('body.nickName'),
}
const result = await UserService._checkCode(userInfo.sid, userInfo.code)
if (result) {
// 校验用户名是否已存在
const res = await User.findOne({ userName: userInfo.userName })
if (res == null) {
// 加密
const sault = bcryptjs.genSaltSync(10)
userInfo.password = bcryptjs.hashSync(userInfo.password, sault)
const userDocument = new User({
userName: v.get('body.userName'),
password: userInfo.password,
name: v.get('body.nickName'),
existed: moment().format('YYYY-MM-DD HH:mm:ss'),
})
await userDocument.save()
ctx.body = {
code: 200,
msg: '注册成功',
create_time: moment().format('x'),
}
} else {
throw new AllExistedException('用户名已存在')
}
} else {
throw new ParameterException('验证码错误')
}
}``
# 邮箱找回密码接口☺️
配置公共邮箱的开启stmp服务
wsrnpqaeinswbjcc
这里需要使用nodemailer
插件
配置
nodemailer
初始方法async function send(sendInfo) { let transporter = nodemailer.createTransport({ // 发送的主机地址 host: 'smtp.qq.com', port: 587, secure: false, //配置授权邮箱 和授权码 auth: { user: '251205668@qq.com', // generated ethereal user pass: 'wsrnpqaeinswbjcc', // generated ethereal password }, }) // 配置跳转的路由 let url = 'http://www.imooc.com' let info = await transporter.sendMail({ from: '"认证邮件" <251205668@qq.com>', // sender address to: sendInfo.email, // 发送的邮箱账号 // 配置主题 subject: sendInfo.user !== '' ? `你好开发者,${sendInfo.user}!《论坛》验证码` : '《论坛》验证码', // Subject line text: `您在《论坛》中注册,您的邀请码是${ sendInfo.code },邀请码的过期时间: ${sendInfo.expire}`,// 模拟生成的30分钟倒计时 // 邮件主题内容 html: ` <div style="border: 1px solid #dcdcdc;color: #676767;width: 600px; margin: 0 auto; padding-bottom: 50px;position: relative;"> <div style="height: 60px; background: #393d49; line-height: 60px; color: #58a36f; font-size: 18px;padding-left: 10px;">论坛社区——欢迎来到官方社区</div> <div style="padding: 25px"> <div>您好,${sendInfo.user}童鞋,重置链接有效时间30分钟,请在${ sendInfo.expire }之前重置您的密码:</div> <a href="${url}" style="padding: 10px 20px; color: #fff; background: #009e94; display: inline-block;margin: 15px 0;">立即重置密码</a> <div style="padding: 5px; background: #f2f2f2;">如果该邮件不是由你本人操作,请勿进行激活!否则你的邮箱将会被他人绑定。</div> </div> <div style="background: #fafafa; color: #b4b4b4;text-align: center; line-height: 45px; height: 45px; position: absolute; left: 0; bottom: 0;width: 100%;">系统邮件,请勿直接回复</div> </div> `, // html body }) return 'Message sent: %s', info.messageId }
编写接口方法
async sendEmail(ctx,next){ const v = await new SendEmailValidator().validate(ctx) const userName = v.get('body.userName') const result = await send({ // 验证码: 暂时模拟为1234 code:1234, // 有效时间:创建模拟的格式化的时间 expire:moment().add(30,"minutes").format('YYYY-MM-DD HH:mm:ss'), email:userName, user:'努力中的杨先生' }) ctx.body = { code:200, message:"邮件发送成功", data:result } }
优化后的忘记密码
async senEmail(ctx,next){ // 发送邮箱 接收参数userName sid const v = await SenEmailValidator().validate(ctx) const userName = v.get('body.userName') const sid = v.get('body.sid') const code = v.get('body.code') // 校验验证码 if(code!==null && code.toLowerCase === getValue(sid).toLowerCase){ // 校验用户名是否存在 const user = await User.findOne({userName:userName}) if(user){ const key = uuid() const token = jwt.sign({_id:user._id},JWT_SECRET,{ expiresIn:60*30}) await setValue(key,token) const result = await send({ data:{ token, userName }, expire:moment().add(30,"minutes").format('YYYY-MM-DD HH:mm:ss'), email:userName, user:user.name }) ctx.body = { msg:'邮件发送成功,请注意查收', code:200, data:result } }else{ ctx.body = { code:500, msg:'用户名不存在' } } }else{ ctx.body = { code:500, msg: '验证码错误' } } } // 链接路由:localhost:8080/
忘记密码接口 async forgetPassword(ctx const v = await new ForgetPasswordValidator().valdate(ctx) const newPassword = v.get('body.newPassword') // 接收参数 newPassword const playLoad = await getJwtplayLoad(ctx.request.header.authorization) const _id = playLoad._id const user = await User.findOne({_id}) // 加密 const sault = bcryptjs.genSaltSync(10) password = bcryptjs.hashSync(newPassword, sault) // 更新 const result = await User.updateOne({_id},{password}) if(result.n === 1 && result.ok === 1){ ctx.body = { msg:'重置密码成功', code:200 } }else{ ctx.body = { msg:'重置密码失败', code:500 } } }
另一种方法: 发送邮件时 生成一个
随机四位数
,然后存储在redis中.当用户忘记密码需要重置密码时,传入新密码和验证码,后端查询redis
,如果正确就可以重置密码
# 配置项目
# 封装axios👽👽
初步封装
// 封装 axios
// 1.封装请求返回数据 2.异常统一处理
// 鉴权处理
import axios from 'axios'
import errorHandle from './errorHandle'
const instance = axios.create({
// 统一请求配置
baseURL:
process.env.NODE_ENV === 'development'
? config.baseURL.dev
: config.baseURL.pro,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(
config => {
return config
},
// 请求异常处理
err => {
errorHandle(err)
return Promise.reject(err)
}
)
// 请求结果封装
instance.interceptors.response.use(
res => {
if (res.status === 200) {
// 直接返回res.data
return Promise.resolve(res.data)
} else {
return Promise.reject(res)
}
},
error => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error 处理非200的请求返回的异常: 比如404 ,API服务暂停(没有状态码)等
errorHandle(error)
return Promise.reject(error)
}
)
异常处理初步封装
import {
Message
} from 'element-ui'
const errorHandle = (error) => {
const errorStatus = error.response.status
switch (errorStatus) {
case 401:
console.log('刷新token')
break
case 500:
Message({
type: 'error',
message: error.response.data.msg,
duration: 4000
})
break
case 404:
Message({
type: 'error',
message: '网络异常',
duration: 4000
})
break
default:
break
}
}
# 代理请求和路径代理(解决跨域)😩
配置
vue.conf.js
const path = require('path') module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000/v1' } } }, chainWebpack: (config) => { config.resolve.alias .set('scss', path.join(__dirname, './src/assets/scss/')) .set('@', path.join(__dirname, './src/')) } }
# 环境变量
创建
.env.development
和.env.production
VUE_APP_BASE_URL=http://localhost:3000
默认的环境变量
开发环境 process.env.NODE_ENV = 'development' 生产环境 process.env.NODE_ENV = 'production' axios.defaults.baseURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:3000' : 'http://your.domain.com'
然后使用环境变量
process.env.VUE_APP_BASE_URL
# 路由懒加载🉑
安装插件syntax-dynamic-import
然后书写babel.config
文件
plugins: [
['@babel/plugin-syntax-dynamic-import']
]
懒加载语法
const componentName = () => import('../pages/login')
# 优化路由切换动效👱♀
使用到了iview
的LoadingBar
组件
import { LoadingBar } from 'iview'
router.beforeEach((to, from, next) => {
LoadingBar.start()
next()
})
router.afterEach(() => {
LoadingBar.finish()
})
# 集成Mongdb和redis
# mongdb入门
mongdb是属于典型的非关系型数据库,很好的处理了分布式储存,以对象的形式存储数据
# Docker安装
# Use root/example as user/password credentials
version: '3.1'
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- 27017:27017
volumes
- /home/mongtest/data/db
# 增删改查
# 使用test数据库
use test
# 查看表 集合
show collections
# 统计集合中对象数量
db.users.find().count()
# 增加一个对象 集合习惯在后面加上s
db.users.insert({username:"马云"})
# 向username='马云'的对象中添加一个字段为地址='杭州'
db.users.update({username:"马云",
{
·$set:{
···{address:'杭州'}
···}
··}
})
db.users.update({username:"孙悟空"},{
$set:{
hobby:{
cities:["北京","上海","深圳"],
movies:["三国","英雄"]
}
}
})
# 删除一个对象
db.users.remove({username:'马云'})
# 删除users集合
db.users.remove({})
db.users.drop()
# 删除username=马云的address属性
db.users.update({username:'马云'},{$unset:{address:""}})
# 删除 name= aa city不存在的
db.users.deleteOne({name:'aa',city:{$exists:false}})
# 修改username为马云的对象为马化腾
db.users.replaceOne({username:‘马云’},{username:'马化腾'})
# 更改数据
db.users.update({name:"hjx"},{name:"hjx",age:20})
# 查询
db.users.find({"hobby.movies":"英雄"})
# 查找 age = 30的
db.users.find({age:30})
# 操作符 大于12的
db.users.find({age: {$gt:12}})
# 操作符 大于等于12的
db.users.find({age: {$gte:12}})
# 操作符 小于等于12的
db.users.find({age: {$lte:12}})
# 大于等于12 小于等于30
db.users.find({age: {$lte:30 , $gte:12}})
# 是否存在 city
db.users.find({city: {$exists:true}})
# 除了age其他字段都查出来
db.users.find({},{age:0})
# 按工资升序排列
db.emp.find({}).sort({sal:1})
# 按工资降序排列
db.emp.find({}).sort({sal:-1})
# 分页
# 查询numbers 21条到30条的数据
db.numbers.find().skip(20).limit(10)
skip((pageNo-1)*pageSize).limit(pageSize)
# Mongoose🍸🍸✴️
mongdb的orm
框架,让node操作mongdb
变得更加方便
mongoose操作的对象是
Schema
模式对象,对字段约束Model
相当于Mongdb的集合collections
Document
文档
使用mongoose
连接数据库
const mongoose = require('mongoose')
cosnt db = mongoose.connect("mongodb://数据库的ip地址:端口号/数据库名")
db.on('open',()=>{
console.log("数据库连接成功")
})
具体使用案例
首先明确这几个点mongoose需要通过Schema约束模型,然后通过模型对象进行增删改查
所有首先需要创建Schema对象,再创建Model,再编写业务
创建模型
const mongoose = require('mongoose')
await mongoose.connect('mongodb://localhost/my_database', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const Schema = mongoose.Schema
const UserSchema = new Schema({
name:String,
password:String
})
// 创建模型
const UserModel = mongoose.model('user',UserSchema)
这里Schema
约束规范实例参考
const UserSchema =Schema({
// type:类型
// require 是否必须
// unique 在mongodb里创建索引 类似mysql唯一主键
// enum:['aa','bb'] 指定他的值必须是什么
name:{type:String,require:true,unique:true,enum:['hjx','lisi']},
// 最简单写法
// age:Number
// 数字复杂的校验
// max 最大值
// min 最小值 如果是数组 第一个值是最小范围 第二个值是报错信息
age:{type:Number,max:90,min:[18,'不能小于18岁']}
})
如果集合已经存在 ,这里是个坑
const Schema = Mongoose.Schema() const ci = Mongoose.model('ci',Schema,'ci') // 删除一条数据 // ci.remove({'':'5ebe3c8de468cc2157db610b'},(err,res)=>{ // console.log('删除成功',res) // }) ci.find({author:'和岘'},(err,res)=>{ console.log(res) })
mongoose增删改查
**插入保存在数据库**// 创建好mongoose模型
cosnt UserModel = mongoose.model('user',UserSchema)
cosnt user = new UserModel({
username:'马云',
password:'123456'
})
// 保存
user.save((err,res)=>{
if(err){
console.log(err)
}else{
console.log(res)
}
})
更新数据
cosnt user = new UserModel({
username:'马云',
password:'123456'
})
// 更新数据
user.update({
username:'马化腾',
password:'hello'
})
// 根据id更新数据 find 查询的数组 findOne查询一条数据
let id ='1231231231'
UserModel.findByIdAndUpdate(id,{
password:'wohenpi'
},(err,res)=>{
})
// 更新数据 $set $inc增加
UserModel.updateOne({_id:'1'},{
$set:{userName:''},
$inc:{fav:}
})
删除数据
UserModel.remove({
username:'马云'
},(rer,res)=>{
})
Model.findByIdAndRemove(id,{},callback)
Model.findOneByIdAndRemove(id,{},callback)
查询
//普通查询
UserModel.find({
username:'马云'
},(err,res)=>{
})
// 根据Id查询
UserModel.findById({id=""},(err,res)=>{})
// 查询数量
USerModel.count({username:''},(err,res)=>{})
现已替换成 `countDocuments`
查询所有的数量: `estimatedDocumentCount`无法接condition
// 如果不想输出id值 选择性投影
UserModel.find({username:'马云'},{'username:1',"_id":0},(err,res)=>{
})
// 嵌套查询对象数组中 某个属性
// 例如: tags:[{class:'',name:''}],查询tag.name = 数值中的那么
model.find(tags:{$elemMathch:{name:xxx}})
// 条件查询 大于等于21,小于等于65
UserModel.find({userage: {gte: 21,lte: 65}}, callback);
$or 或关系
$nor 或关系取反
$gt 大于
$gte 大于等于
$lt 小于
$lte 小于等于
$ne 不等于
$in 在多个值范围内
$nin 不在多个值范围内
$all 匹配数组中多个值
$regex 正则,用于模糊查询
$size 匹配数组大小
$maxDistance 范围查询,距离(基于LBS)
$mod 取模运算
$near 邻域查询,查询附近的位置(基于LBS)
$exists 字段是否存在
$elemMatch 匹配内数组内的元素
$within 范围查询(基于LBS)
$box 范围查询,矩形范围(基于LBS)
$center 范围醒询,圆形范围(基于LBS)
$centerSphere 范围查询,球形范围(基于LBS)
$slice 查询字段集合中的元素(比如从第几个之后,第N到第M个元素)
// 模糊查询 使用正则表达式
let whereStr = {'username':{$regex:/m/i/}}// 查询username包含m并且不区分大小写
// 分页查询
var pageSize = 5; //一页多少条
var currentPage = 1; //当前第几页
var sort = {'logindate':-1}; //排序(按登录时间倒序)
var condition = {}; //条件
var skipnum = (currentPage - 1) * pageSize; //跳过数
User.find(condition).skip(skipnum).limit(pageSize).sort(sort).exec((err,res)=>{})
async function query(){
const db =await Mongoose.connect('mongodb://localhost:27017/poems',{useNewUrlParser:true,useUnifiedTopology:true})
if(db){
console.log('连接成功')
}
const Schema = Mongoose.Schema()
const ci = Mongoose.model('ci',Schema,'ci')
const writer =Mongoose.model('writer',Schema,'writer')
const result1 = await ci.findById({_id:'5ebe3c8de468cc2157db610b'})
console.log('ByID:'+result1)
const result2 = await ci.countDocuments({_id:'5ebe3c8de468cc2157db610b'})
console.log('count:'+result2)
const result3 = await ci.findById({_id:'5ebe3c8de468cc2157db610b'},{_id:0})
console.log('不返回某些值'+result3)
const result4 = await ci.find({author:{$regex:/和/i}})
console.log('模糊查询:'+result4)
const pageSize = 10
const currentPage = 1
const sortstr = {'id':0}
let skipNums = (currentPage-1)*pageSize
const result5 = await ci.find({id:{$lt:100}}).skip(skipNums).limit(pageSize).sort(sortstr)
console.log('分页查询:'+result5)
const result6 = await writer.find({},{_id:0}).skip(skipNums).limit(pageSize)
console.log(result6)
}
query()
除了这些 mongoose
还支持在初始化Schema后添加一些静态方法,相当于添加原型链
const PostSchema = new Schema({
...
})
PostSchema.static = {
getList:function(options,sort,page,limit){
// options: 前台传递的 catelog : ask,isTop:0 等
return this.find(options).sort({[sort]:-1}).skip((page)*limit).limit(limit)
}
}
// 此外 还可以在初始化schema后添加中间件 对一些属性添加默认值
PostShcma.pre('save',async (next)=>{
this.create = moment().format('YYYY-MM-DD HH:mm:ss')
await next()
})
# redis
# 开启redis服务
# 创建持久化redis服务
docker run -itd --restart= always --name redis-test -p 8271:6379 -v/home/redistest1:/data redis redis-server --requirepass 123456
# 本地开启
默认端口是:6379
.conf文件设置 密码
requirepass 123456
# 配置开启服务
.\redis-server.exe redis.windows.conf
# redis相关命令
进入cli
docker exec -it redis-test /bin.bash
# 使用命令
redis-cli
# 输入密码
auth 123456
# 退出
quit
# 切换数据库 默认有16个
select 1
# 设置键值
set name test1
# 获取键值
get name
# keys test*
匹配test开头的键值
# 匹配键值是否存在
exists test
# 删除键值
del test
发布订阅
发布
subscribe imooc imooc1
订阅
publish imooc "hello"
// 输入完整命令 发布者会受到这个命令
# node.js操作redis🍓🍓
安装依赖redis
,并配置相关
const redis = require('redis')
const options ={
host: '47.97.180.232',
port: 8271,
password: '123456',
detect_buffers: true,
// 来自官方
retry_strategy: function (options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error('The server refused the connection')
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error('Retry time exhausted')
}
if (options.attempt > 10) {
// End reconnecting with built in error
return undefined
}
// reconnect after
return Math.min(options.attempt * 100, 3000)
}
}
操作redis
通过
client
操作redis
一般分为设置键值,区间值,而值可能存在string
或者Object
两种,所以需要对其封装
// 创建客户端
const client = redis.createClient(options)
const setValue = (key,value,time)=>{
//去空
if(typeof value === 'undefined'|| value === ''|| value === null){
return
}
if(typeof value === 'string'){
// 设置时效性 秒为单位
if(time !== 'undefined'){
return client.set(key,value,'EX',time)
}
return client.set(key,value)
}else if(typeof value === 'object'){
// 对象{key1,:value1,key2:value2}
// hset(key,对象的key,对象的值,client.print) 对象的key可以通过Object.keys(value)获取
Object.keys(value).forEach((item)=>{
return client.hset(key,item,value[item],client.print)
})
}
}
// 获取值有些特殊 需要将方法转为promise进行操作
// 获取键值
const { promisify } = require('util')
const getValue = (key) => {
const getAsync = promisify(client.get).bind(client)(key)
return getAsync
}
// 获取对象的哈希值
const gethValue= (key)=>{
const getHAsync = promisify(client.hgetall).bind(client)(key)
return getHAsync
}
# 首页❤️
# 前端部分技巧✊
# 时间格式处理
filters:{
moment(date){
// 两个月之内 显示为几天前
if((moment(date).isBefore(moment().subtract(1,'months')){
return moment(date).format('YYYY-MM-DD HH:mm:ss')
}else{
return moment(date).formNow()
}
}
}
##### 使用过滤器
{item.created | moment}
# 加载更多基本逻辑
data:{
return(){
isRepeat:false,
list:[],
isEnd:false
}
}
options:{
page:this.page,
limit:this.limit
}
async getlist(){
// 是否存在重复请求
if(isRepeat){
return
}
if(this.End){
return
}
this.repeat = true
const res =await Public.getlist(options)
this.repeat = false
if(res.code === 200){
if(this.list.length === 0){
this.list = res.data
}else{
this.list = this.list.concat(res.data)
}
// 最后一页
if(res.data.length < options.limit){
this.isEnd = true
}
}
}
next(){
this.page ++
this.getlist(options)
}
# 组件和路由拆分
抽离组件
头部导航 挂载APP
panel 分类面板组件 控制路由
// 相关路由配置 { path:'/', component:Home, children:[ // 分类动态路由和默认的路由 { path:'', name:'index', component:Index }, { path:'/index/:categroy', name:'catelog', component:Template1 } ] }
sort排序 tab面板 请求数据,不改变路由
content 长列表面板
使用
iview-LoadingBar
组件优化路由跳转router入口文件 router.beforeEach((to,from,next)=>{ LoadingBar.start() next() }) router.afterEach((to,from)=>{ LoadingBar.finish() })
# qs库的作用处理get请求url传对象参数解析
将
options = {
page :0,
limit:20
}
qs.Stringify(options) // page=0&limit=20 这个库可以将对象转换为url参数形式 常用在get请求携带参数上
# 后端模型和开发细节🚛
文章模型 定义schema 联合user表查询 获取文章列表(筛选) 通过id查询文章 通过uid查询文章列表 通过uid查询文章数量
用户模型 定义schema 添加唯一索引 保存的时候设置默认常见时间 updated的时候也定义时间 过滤敏感数据 定义捕获重复异常 删除注册前面的异常判断
编写,获取列表数据的接口 引入校验器
文章详情接口 引入校验器
本周热议接口 (筛选出七天内按answer数倒叙排序的数据)
友情链接接口
首页后端开发细节概要
# 定义模型时 规范定义 例如文章
const PostSchema = new Schema({
// 连表查询
userInfo: { type: String, ref: 'users' },
title: { type: String },
content: { type: String },
created_time: { type: Date,default:moment().format('x')},
updated_time:{type:Date,default:moment().format('x')},
category: { type: String },
fav: { type:Number },
isEnd: { type: Number,default:0,enum:[0,1] }, // 是否结贴
reads: { type: Number, default: 0 },
answer: { type: Number, default: 0 },
status: { type: Number,default:0,enum:[0,1]}, // 是否打开回复
isTop: { type: Number,default:0,enum:[0,1] }, // 是否置顶
sort: { type: String, default: 100 }, // 随着数值的减小,按照数值的倒序进行排列
tags: {
type: Array,
default: [
// { name: '', class: '' }
]
}
})
连表查询 : 父表需要有
ref
指定字表,字表需要定义唯一索引//父表 userInfo{ type:String,ref:"users" } // 子表 userName:{ type:String, index:{ unique:true }, // 当集合没有userName 不执行查询 sparse: true } // 查询时附带字表相关内容 userModel.find(...).populate({ // 指定替代 'path':"userInfo", // 指定返回的字段 'select':"isvip avatar name" })
# 多种查询条件查询 最好定义静态方法
// 添加静态方法
PostSchema.statics = {
getList: function (options, sort, page, limit) {
return this.find(options)
.sort({ [sort]: -1 }) // -1:代表倒序排列
.skip(page * limit) // 跳过多少页
.limit(limit) // 取多少条数据
.populate({
path: 'userInfo', // 要替换的字段是uid
/**
* 联合查询的时候,有些字段是需要的,有些字段是不需要的。
* 可以通过select筛选出我们需要的字段,去隐藏敏感字段。
*/
select: 'name isVip avatar'
})
}
}
# 利用校验器 设置默认值
const options = {}
if (typeof body.tag !== 'undefined' && body.tag !== '') {
options.tags = { $elemMatch: { name: body.tag } }
}
options.category =
v.get('query.category') === null ? 'index' : v.get('query.category')
options.isEnd = v.get('query.isEnd') === null ? 0 : v.get('query.isEnd')
options.status = v.get('query.status') === null ? 0 : v.get('query.status')
options.isTop = v.get('query.isTop') === null ? 0 : v.get('query.isTop')
const sort =
v.get('query.sort') === null ? 'created_time' : v.get('query.sort')
const page = v.get('query.page') === null ? 0 : v.get('query.page')
const limit = v.get('query.limit') === null ? 20 : v.get('query.limit')
const result = await Post.getList(options, sort, page, limit)
# 利用mongose
的钩子函数 在查询语句执行前执行操作
// 设置调用save和update 保存的属性
UserSchema.pre('update',(next)=>{
this.updated = moment().format('x')
this.login_time = moment().format('x')
next()
})
// 过滤重复的userName和重复的Name
UserSchema.post('save', function (error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('Error: Mongoose has a duplicate key.'))
} else {
next(error)
}
})
# 防止重复点击
x:{
index:0
}
selectType (item) {
this.currentIndex1 = item.key
if (this.currentIndex1 !== this.x.index) {
this.x.index = item.key
console.log('success')
}
},
# 当数据有很多种分类,但是默认是返回全部分类的数据时,需要删除掉默认对象的值
const options = {}
// 默认,不传type 全站
const type =v.get('query.type')
if(type !== null && type !== 0){
options.type = v.get('query.type')
}else{
if(type == null || type===0){
// 不传的时候 type=null
delete options.type
}
}
# 个人中心页面和设置👤
# 前端细节👿👿👿👿
# 头像区菜单项显隐原理
当鼠标移入头像区域或者移入菜单
li
区域时触发显示,当鼠标移出时触发隐藏事件,但是考虑到头像区域和菜单区域有间距,必须要设置定时器触发事件防止显示事件立即隐藏show(){ seTimeout(()=>{ this.IsShow = true },200) }, hide(){ seTimeout(()=>{ this.isShow = false },500) }
# 路由拆分
个人中心路由
{ // 显示信息分为:全部 文章 讨论 分享 求助 收藏集 点过赞的帖子 关注列表 粉丝 path:'/user/:uid/:type', name:'UserCenter', components:'UserCenter' }, { // 个人设置 profile 个人资料 password 密码修改 账号关联 account path:'/setting/:type' }
# 组件划分
个人中心组件:
userBanner
,路由是当前登录用户的uid
时,显示编辑资料按钮,不是则显示关注按钮
tabs
,切换不同的type
的tab
组件,切换路由监听路由的变化加载不同的请求数据,需要提供全部 文章 讨论 分享 求助 收藏集 点过赞的帖子 关注列表 粉丝
user-postContent
,文章列表组件
userInfo
组件: 显示关注了多少人,粉丝数量,获取点赞数量,签到天数,个人积分
# 创建路由守卫,在用户未登录状态不允许进入用户设置页面👿👿
{
path:'/setting/:type',
name:'setting',
component: settting,
beforeEach((to,from,next)=>{
const userInfo = parse(localStroage.getItem('userInfo'))
if(!userInfo){
next('/login')
}else{
next()
}
})
}
// 优化版本 创建全局路由守卫 增加meta标签
{
path:'/setting/:type',
name:'setting',
component:setting,
meta:{requireAuth:true}
}
router.beforeEach((to,from,next)=>{
const userInfo = localStorage.getItem('userInfo')
const token = localstorage.getItem('token')
// 官方写法 判断路由元信息 是否需要鉴权
if(to.matched.some(record=>record.meta.requireAuth)){
// jwt令牌合法校验
const isValid = await Token.isValid(token).result
if(!userInfo || userInfo && !Valid){
localStorage.clear()
next('/login')
}else{
next()
}
}else{
// 不需鉴权
next()
}
})
# sessionStorage
和localstorage
区别
sessionStorage
是指当会话存在时的缓存,当关闭浏览器或者tab
,缓存会清空
localstorage
指保存在本地的缓存
# 添加404导航页面
在router
配置文件最后一行添加这样的路由配置
{
path:‘/404’,
name:'NotFound'
components:NotFound,
},
// 所有不属于路由配置的路由都导向404
{ path: '*', redirect: '/404', hidden: true }
# 上传头像
原生做法
<input id="pic" type="file" name="file" accept="image/png.image/jpg,image/gif" @change="changeImage" />
<script>
export default{
data:{
return (){
pic:'',
formData:''
}
},
method:{
changeImage(e){
// 上传图片
let file = e.target.files
const formData = new FormData()
if(file.length>0){
// 添加表单 键值序列
formData.append('file',files[0])
this.formData = formData
}
// 调用服务器接口
user.uploadImg(this.formData)
// 然后调用更新用户信息接口
}
}
}
</script>
<style lang="scss">
#pic
display:none;
</style>
# 后端接口💋
# 获取用户基本信息
async getUserInfo(ctx){
const uid = ctx,request.query.uid
const result = await User.findOne({_id:uid},{password:0,mobile:0})
if(result === null){
ctx.body = {
msg:'找不到该用户信息',
code:500
}
}else{
ctx.body = {
msg:'获取用户信息成功',
code:200,
data:result
}
}
}
# 修改基本信息
async updateUserInfo(ctx){
const body = ctx.request.body
const AuthHeader = ctx.request.header.authorization
const playload = await getJwtPlayLoad(AuthHeader)
const user = await User.findOne({_id:playload._id})
// 不加入更改邮箱权限
// 过滤没必要的更改信息
const SkipInfo = ['password','mobile']
SkipInfo.map((item)=>{
delete body[item]
})
// 更改信息
const result = await User.update({_id:playload._id},body)
if(result.ok === 1 && result.n === 1){
ctx.body = {
code:200,
msg:'更新用户信息成功'
}
}else{
ctx.body = {
code:500,
msg:'发生异常,更新失败'
}
}
}
# 修改邮箱绑定
发送一封确认更改邮箱绑定的邮箱
const token = jwt.sign({_id:playload._id},JWT-SERCERT,{ expiresIn: 60 * 30 }) // 缓存在redis中 const key = uuid() setValue(key,token) await send({ expire:moment().add(30,'minutes').format('YYYY-MM-DD HH:mm:ss'), email:user.userName, name:user.name , userName:body.userName, key })
然后邮箱的重置链接需要携带数据-跳转的页面路由
ep
:localhost:8080/resetEmail?key=uuid()&userName=email
最后需要定义用户修改邮箱的接口
async updateUserName(ctx){ const v = await new UserNameValidator().validate(ctx) const key = v.get('query.key') const userName = v.get('query.userName') // 获取redis数据 获取_id if(key){ const token = await getValue(key) const playload = await getJwtplayLoad('Bearer '+token) // 判断邮箱是否存在 const user = await User.finOne({userName}) if(user && user.password){ throw new UserExistedException('邮箱已存在',200) }else{ await User.updateOne({_id:playload._id},{userName}) ctx.body = { code:200, msg:'换绑成功' } } } }
# 修改密码
async modifyPassword(ctx){
const playload = await getJwtPlayload(ctx.request.header.authorization)
const v = await new PasswordValidator().validate(ctx)
const password = v.get('body.password')
const newPassword = v.get('body.newPassword')
if(password === newPassword){
throw new Exception('两次密码不能相同')
}else{
const user = await User.findOne({_id:playload._id})
// 比对旧密码是否正确
const checkPassword = bcript.compareSync(password,user.password)
if(checkPassword){
const sault = bcryptjs.genSaltSync(10)
const pwd = bcryptjs.hashSync(newPassword,sault)
await User.updateOne({_id:playload._id},{
password:pwd
})
ctx.body = {
code:200,
msg:'修改密码成功'
}
}else{
ctx.body = {
code:500,
msg:'原密码不正确'
}
}
}
}
# 上传文件
定义上传的路径
const uploadPath = `${pwd.process()}/static`
Node.js判断路径是否目录存在 :
statis.isDirectory()
const fs = require('fs')
const mkdir = require('make-dir')
async upload(ctx){
// append的键
const file = ctx.request.files.file
// 获取图片名称 图片格式 图片的路径
const ext = file.spilt('.')[1]
// 获取当前文件的目录 没有则创建 分时间创建目录
const dir = `${updatePath}/${moment().format('YYYYMMDD')}`
// 创建目录
await mkdir(dir)
// 读写文件
const picname = uuid()
// 文件真实路径
const realPath = `${dir}/${picname}.${ext}`
// 读取上传的文件的流
const reader = fs.createReadStream(file.path)
// 写入目录中
const upStream = fs.createdWriteStream(realPath)
reader.pipe(upstream)
ctx.body = {
code:200,
msg:'图片上传成功',
data:{
path:realPath
}
}
}
判断目录是否存在,如果觉得麻烦就直接用
make-dir
第三方库
// D:User/1212/121.jpg
// 传入路径 获取文件状态 node.js的api fs.stats 查看文件状态,转换为promise
const getStats = (path)=>{
return new Promise((resolve)=>{
fs.stat(path,(err,stats)=>{
if(err){
// 获取失败 不是文件
resove(false)
}else{
// 获取成功
resolve(stats)
}
})
})
}
// 判断是否是目录 如果不是就创建目录
const dirExists = async(dir)=>{
const isExisted = await getStats(dir)
// 如果文件信息存在 分目录或者文件
if(isExisted && isExisted.isDirectory()){
return true
}else if(isExisted){
return false
}
// 如果不存在 创建目录
}
# 签到系统
# 设计签到记录表
字段 | 意义 |
---|---|
created_time | 创建时间 |
last_signTime | 上一次签到时间 |
user | 用户id |
favs | 当前签到积分 |
# 签到系统基本逻辑
查询用户签到信息
- 是否是连续签到 ,连表查询出
user
中的count
和总积分favs
sign_record
只记录用户单次签到的情况
无签到记录
一次积分是5,然后保存当前的记录表,更新用户表总积分和连续签到数量。
如果当前用户有签到记录
二种情况:
上一次签到时间是今天,此时抛出异常
上一次签到时间不是今天
上一次签到时间是昨天
此时先将用户连续签到的数量获取 然后设置积分逻辑,更新用户的
count
和总积分间隔时间签到
此时一次签到积分又变回5,用户连续签到天数设置为1
具体实现
定义Schema
const SignRecordSchema = new Schema({
created_time:Date,
favs:NUmber,
user:{
type:String,
ref:'users'
}
})
SignRecordSchema.pre('save',function(next){
this.created_time = moment().format('YYYY-MM-DD HH:mm:ss')
next()
})
// 查询出最新的一条签到记录
SignRecordSchema.statics = {
findByUid : function(uid){
return this.findOne({user:uid}).sort(created_time:-1)
}
}
签到逻辑具体
获取jwt
身份验证信息
async getJwtPlayLoad(AuthHeader){
// Authorization : Bearer token
return await jwt.verify(AuthHeader.split(' ')[1],JWT_SERVET)
}
积分签到
const SignRecord = require('../models/sign_record')
async Sign(ctx){
const res = await getJwtPlayLoad(ctx.request.header.authorization)
// 查询用户和签到记录
const record = await SignRecord.findByUid(res._id)
const signUser = await User.findOne({_id:res._id})
let data = {}
if(record!==null){
// 有签到记录
if(moment(record.created_time).format('YYYY-MM-DD')=== moment().format('YYYY-MM-DD')){
throw new AllExistedError('今日已经签到',200)
}else{
let fav
// 连续签到 积分逻辑
if(moment(record.created_time).format('YYYY-MM-DD') === moment().substr(1,'days').format('YYYY-MM-DD')){
const count = signUser.count +1
if(count<5){
fav = 5
}else if(count>=5&&count<15){
fav =10
}
await User.updateOne({
_id:res._id
},{
$set:{count:count},
$inc:{favs:fav}
})
data = {
favs:signUser.favs+SignFav,
count
}
}else{
// 不是连续签到
fav = 5
await signUser.update({_id:res.id},{
$set:{count:1},
$inc:{favs:fav}
})
data = {
favs:signUser.favs+5,
count:1
}
}}
}else{
// 无签到记录
let fav = 5
signUser.update({_id:res._id},{
$set:{count:1},
$inc:{favs:fav}
})
}
}
前端部分逻辑
每次取缓存中的userInfo数据,保存在变量userObject,如果缓存中有`count`就为count,否则为0
fav 签到接口调用的数据
签到 修改userObject的count和isSign状态
count: 0
const userInfo = parse(localstorage.userInfo)
if(userInfo){
this.userObject = userInfo
}else{
this.count =0
}
count(){
if(this.userObject.userName){
return this.userObject.count
}else{
return 0
}
}
sign(){
if(!this.userObject.userName){
this.$message.error('需要登录才能使用')
}else{
cosnt res = await User.sign()
if(res.code=200){
this.userObject.count = res.data.count
this.fav = re.data.fav
this.isSign = true
}else{
this,$message.error(res.msg)
}
}
}