# 大前端项目笔记

# Docker入门使用🚙

安装并配置加速地址 (opens new window)

image-20210707154648427

常用命令

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相同,只不过语法方面有点不同

语法

  1. 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>

表单校验常用

  1. data中定义校验规则对象,例如:rules,然后再form引入,并指定每个表单项的校验规则

    先指定规则:rules,然后指定元素的校验规则prop="规则里面的对象"

     <el-form inline :model="data" :rules="rules" ref="form">
          <el-form-item label="审批人" prop="user">
    
  2. 编写校验规则,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'
          }
        ],

  1. 表单常见属性 参考属性 (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
    }
    }  
  }
}

了解jwtAPI

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')

# 优化路由切换动效👱‍♀

使用到了iviewLoadingBar组件

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)
}
# 组件和路由拆分

抽离组件

  1. 头部导航 挂载APP

  2. panel 分类面板组件 控制路由

// 相关路由配置
{
    path:'/',
    component:Home,
    children:[
        // 分类动态路由和默认的路由
        {
            path:'',
            name:'index',
            component:Index
        },
        {
            path:'/index/:categroy',
            name:'catelog',
            component:Template1
        }
    ]
}
  1. sort排序 tab面板 请求数据,不改变路由

  2. content 长列表面板

  3. 使用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,切换不同的typetab组件,切换路由监听路由的变化加载不同的请求数据,需要提供全部 文章 讨论 分享 求助 收藏集 点过赞的帖子 关注列表 粉丝

  • 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()
    }
})

# sessionStoragelocalstorage区别

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)
    } 
    }
}

Last update: 7/28/2021, 7:33:21 PM