前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >那些年初级前后端一起撕过的逼

那些年初级前后端一起撕过的逼

作者头像
一粒小麦
发布2019-07-30 15:18:33
1.8K0
发布2019-07-30 15:18:33
举报
文章被收录于专栏:一Li小麦一Li小麦

一个项目一开始总是出于还不错愿景,但做着做着,就越来越乱了。万丈高楼平地起,有些基础的问题解决好,后面改需求就不会那么痛苦了。

在笔者之前的工作经历中,遇到用户上传(跨域+鉴权+上传)的扯皮多了去了。现在就尝试用标准的姿态,更加前端的角度去回答这几个问题。

写了好多天原理,现在就来实战一下吧。这是我个人项目中的一个商城,基于以下技术栈:

代码语言:javascript
复制
- vue
- vant
- router
- vuex
- axios

后端沿用用上篇文章的egg+mongo。

虽然笔者主要使用的是react,但作为一手得来的经验,文章内容比很多使用vue的初级工程师要深入的多。

跨域

[前端]vue配置跨域

前端配置跨域,在根目录新建 vue.config.js

代码语言:javascript
复制
module.exports = {
  devServer: {
    proxy: 'http://localhost:7001'
  }
}

以上是研发环境配置跨域。

[后端]egg配置跨域

后端沿袭上一篇的egg框架。在后端设置跨域:

代码语言:javascript
复制
// 步骤一:下载 egg-cors 包
npm i egg-cors -S

// 步骤二:plugin.js中设置开启cors
cors : {
    enable: true,
    package: 'egg-cors',
};

// 步骤三:config.default.js中配置,注意配置覆盖的问题
  config.security = {
    // 关闭csrf
    // csrf: {
    //   enable: false,
    //   ignoreJSON: true
    // },
    domainWhiteList: ['http://localhost:8080']
  };

  // 允许跨域,需要允许Options请求,并且允许携带cookie
  config.cors = {
    origin: 'http://localhost:8080',
    credentials:true,
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
  };

因为前端header将携带token,对于预检请求,必须return true

用户登录

首先是做手机号码登录。需要一套符合jwt规范的接口,包括用户登录请求token。

egg的插件生态相当丰富。可安装相应的jwt模块。

代码语言:javascript
复制
npm i egg-jwt -s

在插件和设置中引入:

代码语言:javascript
复制
// plugin.js
jwt: {
  enable: true,
  package: 'egg-jwt',
}

// config.default.js
config.jwt = {
    secret: 'Great4-M',//jwt密钥
    enable: true, // default is false
    match: /^\/api/, // 所有需要鉴权的都用/api打头
  }
[后端]service(token生成校验)

在service下新建actionToken.js

代码语言:javascript
复制
// service/actionToken.js
const Service = require('egg').Service
class ActionTokenService extends Service {
    async apply(_id) {
        const { ctx } = this
        // 签名校验
        return ctx.app.jwt.sign({
            data: {
                _id: _id
            },
              //  保存7天!
            exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7)
        }, ctx.app.config.jwt.secret)
    }
}
module.exports = ActionTokenService

在service层下新建userAccess.js,在这写用户登录和注册方法:

代码语言:javascript
复制
// service/userAccess.js
const Service = require('egg').Service
class UserAccessService extends Service {
    async login(payload) {
        const { ctx, service } = this
        const user = await service.user.findByMobile(payload.mobile)
        // 查找用户
        if (!user) {
            ctx.throw(404, 'user not found')
        }
          // 校验密码
        let verifyPsw = await ctx.compare(payload.password, user.password)
        if (!verifyPsw) {
            ctx.throw(401, 'user password is error')
        }

        // 生成Token令牌
        return { token: await service.actionToken.apply(user._id) }
    }


      // 当前用户信息
    async current() {
        const { ctx, service } = this
        // ctx.state.user 可以提取到JWT编码的data
        const _id = ctx.state.user.data._id
        const user = await service.user.find(_id)
        if (!user) {
            ctx.throw(404, 'user is not found')
        }
        user.password = 'How old are you?'
        return user
    }
}
module.exports = UserAccessService
[后端]测试用例

在contract下新建userAccess的测试用例

代码语言:javascript
复制
// app/contract/userAccess.js
module.exports = {
    loginRequest: {
        mobile: { type: 'string', required: true, description: '手机号', example: '13800138000', format: /^1[34578]\d{9}$/, },
        password: { type: 'string', required: true, description: '密码', example: '123456', },
    },

    createUserRequest:{
        mobile:{type:'string',required:true,descption:'手机号码',example:'15626189507',format:/^1[34578]\d{9}$/ },
        password:{type:'string',required:true,descption:'密码',example:'123456'},
        realName:{type:'string',required:true,descption:'姓名',example:'djtao'},
    }
}
[后端]controller(接口)

在controller下新建userAccess控制器(接口):

代码语言:javascript
复制
// controller/userAccess.js
'use strict'
const Controller = require('egg').Controller
/**
* @Controller 用户鉴权
*/
class UserAccessController extends Controller {
    constructor(ctx) {
        super(ctx)
    }
    /**
    * @summary 用户登入
    * @description 用户登入
    * @router post /auth/jwt/login
    * @request body loginRequest *body
    * @response 200 baseResponse 创建成功 */
    async login() {
        const { ctx, service } = this
        // 校验参数
        ctx.validate(ctx.rule.loginRequest);
        // 组装参数
        const payload = ctx.request.body || {}
        // 调用 Service 进行业务处理
        const token = await service.userAccess.login(payload) 
        const user = await service.user.findByMobile(payload.mobile)
        const res={...token,user}
        // const res2=await service.user.show()
        // 设置响应内容和响应状态码
        ctx.helper.success({ ctx, res })
    }

     /**
    * @summary 用户注册
    * @description 注册
    * @router post /user/register
    * @request body createUserRequest *body
    * @response 200 baseResponse 创建成功 */
    async register(){
        const { ctx, service } = this
        // 校验参数
        ctx.validate(ctx.rule.createUserRequest)
        // 组装参数
        const payload = ctx.request.body || {}
        // 调用 Service 进行业务处理
        const res = await service.user.create(payload)
        console.log(111111,res)
        // 设置响应内容和响应状态码
        ctx.helper.success({ ctx, res })
    }
}
module.exports = UserAccessController
[前端]登录态

登录请求的登录态保存在store中

代码语言:javascript
复制
// ..拿到数据后
if (res.data.code == 0) {
  this.$store.commit("login");
  // 保存到vuex
  localStorage.setItem("mzUser", JSON.stringify(res.data.data));
  Toast.success({
    message: res.data.msg,
    duration: 400,
    onClose: () => {
      this.$router.push({
        path: "/mall/me"
      });
    }
  });
}

同理,登出(注销)也可以用 this.$store.commit(logout)

因此在store下的写法是:

代码语言:javascript
复制
// store/user.js
// 获取token作为初始登录态。
let token=window.localStorage.getItem("mzUser") ?
JSON.parse(window.localStorage.getItem("mzUser")).token : ''
export default {
    state:{
        isLogin:!!token
    },
    mutations:{
        login(state){
            state.isLogin=true;
        },
        logout(state){
            state.isLogin=false;
        }
    },
    actions:{

    },
}

这里的登录态将作为路由守卫的的依据。

[前端]路由守卫

路由守卫的包括所有登录之后的界面。

先给所有路由加一个flag.

代码语言:javascript
复制
// router.js
        // 商城内页
    {
      path:'/mall',
      name:'mall',
      component:Mall,
      meta:{auth:true},
      children:[
        {
          path:'me',
          component:Me,
          meta:{auth:true},
        },
        {
          path:'home',
          component:UserList,
          meta:{auth:true},
        },
      ]
    },

然后根据这个flag确定这些页面需要做守卫。

代码语言:javascript
复制
router.beforeEach((to,from,next)=>{
  if(to.meta.auth){
    // 从store中获取登录态
    const isLogin=store.state.user.isLogin;
    if(isLogin){
      next();
    }else{
      next({
        path:'/login',
        query:{
          redirect:to.path
        }
      })
    }
  }else{
    next()
  }
});
[前端]请求方法的封装(lib/http,api)

我们要封装一个请求方法,实现以下目的:

  • 把token带到header里去!
  • 对路由状态进行异常判断和处理;
  • 足够的业务覆盖面;
  • 很好地获取。

简单说就是一个具有路由拦截器功能的请求库。

在header里带上她的token

先来实现第一个目标:

代码语言:javascript
复制
import axios from 'axios'
import qs from 'qs'
import store from '../store'

axios.defaults.timeout = 5000;//响应时间
//配置请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
//配置接口地址
axios.defaults.baseURL = 'http://localhost:7001';
axios.defaults.withCredentials = true;

axios.interceptors.request.use(
    config => {
        // 获取token
        const token = window.localStorage.getItem("mzUser") ?
            JSON.parse(window.localStorage.getItem("mzUser")).token : '';
        if (token) {
            // 判断是否存在token,如果存在的话,则每个http header都加上token
            // Bearer是JWT的认证头部信息
            config.headers.common["Authorization"] = "Bearer " + token;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    }
)
token过期处理(响应/请求拦截器)

然后,需要做token过期的判断。(在此不妨把token有效期改为3s用以测试)

代码语言:javascript
复制
//返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) => {
    store.commit('change', false);
    //对响应数据做些事
    if (!res.data.success) {
        return Promise.resolve(res);
    }
    return res;
}, (error) => {
    // 登录过期判断
    if(error.response.status=401){
          // 你还可以在这里清理token。
        store.commit('logout');
        window.location.href='/login';
    }else{
        console.log('网络异常');
        return Promise.reject(error);
    }
});

同理,你还可以写一个请求拦截器

代码语言:javascript
复制
//POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => {
    //在发送请求之前做某件事
    store.commit('change', true);
    if (config.method === 'post') {
        config.data = qs.stringify(config.data);
    }
    return config;
}, (error) => {
    console.log('错误的传参')
    return Promise.reject(error);
});
封装post和get

接下来看第三个目标:封装post和get请求。其实根据业务场景,还可以封装put和delete。

代码语言:javascript
复制
//返回一个Promise(发送post请求)
export function post(url, params) {
    return new Promise((resolve, reject) => {
        console.log(params)
        axios.post(url, params)
            .then(response => {
                resolve(response);
            }, err => {
                reject(err);
            })
            .catch((error) => {
                reject(error)
            })
    })
}

//返回一个Promise(发送get请求)
export function get(url, param) {
    return new Promise((resolve, reject) => {
        axios.get(url, { params: param })
            .then(response => {
                resolve(response)
            }, err => {
                reject(err)
            })
            .catch((error) => {
                reject(error)
            })
    })
}

export default {
    post,
    get,
}

http.js就结束了。

挂载到vm

api.js主要是前端管理接口的文件。结构示例如下:

在main.js下,引入http.js和api.js,然后挂在到 Vue的原型链上,就可以很方便地使用了。

代码语言:javascript
复制
// 引入http
import http from './lib/http.js'
Vue.prototype.$http=http;
Vue.prototype.$axios = axios;
// 引入api
import api from './lib/api'
Vue.prototype.$api=api;

使用示例:

代码语言:javascript
复制
const api=this.$api;
const http=this.$http;

http.post(api.login,{mobile:'13800138000',password:'123456'}).then(res=>{
  // balabala...
})

上传

上传遇到的问题在在于

[后端]上传业务

上传业务很简单。当前端请求成功后,发回地址即可。

先安装插件;

代码语言:javascript
复制
npm i await-stream-ready stream-wormhole image-downloader -s

然后在controller层写逻辑:

代码语言:javascript
复制
// app/controller/upload.js
const fs = require('fs')
const path = require('path')
const Controller = require('egg').Controller
const awaitWriteStream = require('await-stream-ready').write
const sendToWormhole = require('stream-wormhole')
const download = require('image-downloader')

/**
* @Controller 上传
*/
class UploadController extends Controller {
    constructor(ctx) {
        super(ctx)
    }

    // 上传单个文件
    /**
    * @summary 修改头像
    * @description 上传单个文件
    * @router post /api/upload/single
    */
    async create() {
        const { ctx } = this
        // 要通过 ctx.getFileStream 便捷的获取到用户上传的文件,需要满足两个条件:
        // 只支持上传一个文件。
        // 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。
        const stream = await ctx.getFileStream()
        // 所有表单字段都能通过 `stream.fields` 获取到
        const filename = path.basename(stream.filename) // 文件名称
        const extname = path.extname(stream.filename).toLowerCase() // 文件扩展名称
        const uuid = (Math.random() * 999999).toFixed()

        // 组装参数 stream
        const target = path.join(this.config.baseDir, 'app/public/uploads',
            `${uuid}${extname}`)
        const writeStream = fs.createWriteStream(target) 
        // 文件处理,上传到云存储等等
        try {
            await awaitWriteStream(stream.pipe(writeStream))
              // 组装地址
            const res={imgUrl:`http://localhost:7001/public/uploads/${uuid}${extname}`}
            // 调用 Service 进行业务处理
            // 设置响应内容和响应状态码
            ctx.helper.success({ctx,res});
            return res

        } catch (err) {
            // 必须将上传的文件流消费掉,要不然浏览器响应会卡死
            await sendToWormhole(stream)
            throw err
        }
    }
}
module.exports = UploadController

那么上传接口就完成啦。

[前端]vant-ui留的问题

解决了上述问题之后,上传的坑主要在前端。

vant-ui框架upload组件有个钩子是这么写的:

和大多数UI框架不一样,这里需要自己写上传方法。需要注意以下问题:

  • content-type必须是 multipart/*(本质上是 multipart/formData
  • 必须带上token
代码语言:javascript
复制
// 依样画葫芦:
<van-uploader v-model="fileList" :max-count="1" :after-read="upload">

upload方法可以是:

代码语言:javascript
复制
upload(e) {
      this.isLoading=true;
      let params = { file: e.file };
            // 组装formdata
      let formData = new FormData();
      for (let i in params) {
        formData.append(i, params[i]);
      }

            // 允许携带cookie
      const instance = axios.create({
        withCredentials: true
      });

            //请求拦截器
      instance.interceptors.request.use(
        config => {
          const userinfo=JSON.parse(window.localStorage.getItem("mzUser"));
          const token=userinfo.token;
          if (token) {
            // 判断是否存在token,如果存在的话,则每个http header都加上token
            // Bearer是JWT的认证头部信息
            config.headers.common["Authorization"] = "Bearer " + token;
          }
          return config;
        },
        err => {
          return Promise.reject(err);
          this.isLoading=false
        }
      );

            // 。。。。
    }

接下来就可以用instance来发请求了。

另外一方面:上传进度条怎么处理?也是前端的问题。

上传进度是xhr的一个属性,原生js可以这么拿到:

代码语言:javascript
复制
xhr.upload.addEventListener("progress", (e)=>{
  console.log(e)
})

vant提供了onUploadProgress方法,你可以直接拿到

代码语言:javascript
复制
instance
        .post(this.$api.uploadSingle, formData, {
          headers: { "Contnent-type": "multipart/*" },
          onUploadProgress: progressEvent => {
            var complete = (progressEvent.loaded / progressEvent.total * 100 | 0) 
                this.progress = complete
            }
        })

        .then(res => {
          // balabala
        });
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 跨域
    • [前端]vue配置跨域
      • [后端]egg配置跨域
      • 用户登录
        • [后端]service(token生成校验)
          • [后端]测试用例
            • [后端]controller(接口)
              • [前端]登录态
                • [前端]路由守卫
                  • [前端]请求方法的封装(lib/http,api)
                    • 在header里带上她的token
                    • token过期处理(响应/请求拦截器)
                    • 封装post和get
                    • 挂载到vm
                • 上传
                  • [后端]上传业务
                    • [前端]vant-ui留的问题
                    相关产品与服务
                    对象存储
                    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档