手把手带你进阶全栈打工人,手撸H5商城项目从0到1实战分享(附源码)

1,369 阅读15分钟

前言

最近很流行的一句话,“打工人,打工魂,打工都是人上人”。

分享我的NodeJS全栈商城项目实战系列(处女篇)之前,先来了解一下什么是全栈工程师?

全栈工程师是指掌握多种技能,胜任前端与后端,能利用多种技能独立完成产品的人。

现代项目的开发,需要掌握多种技术。互联网项目,需要用到后端开发、前端开发、界面设计、产品设计、数据库、各种移动客户端、三屏兼容、restFul API设计和OAuth2.0等等,比较前卫的项目,还会用到SPA、Web Socket、HTML5/CSS3这些技术以及第三方开发像微信公众号网页、小程序等等。

Web前端也远远不是从前的切个图用个jQuery上个AJAX兼容各种浏览器那么简单了。现代的Web前端,你需要用到模块化开发、多屏兼容、MVC/MVVM,各种复杂的交互与优化,甚至你需要用到Node.js来协助前端的开发。

所以说一个现代化的项目,是一个非常复杂的构成,我们需要一个人来掌控全局,他不需要是各种技术的资深专家,但他需要熟悉到各种技术。对于一个团队特别是互联网企业来说,有一个全局性思维的人非常非常重要。

项目越大,沟通成本越高,做过项目管理的都知道,项目中的人力是1+1<2的,人越多效率越低。因为沟通是需要成本的,不同技术的人各说各话,前端和后端是一定会掐架的。每个人都会为自己的利益而战,毫不为己的人是不存在的。

而全栈工程师的沟通成本几乎为零,因为各种技术都懂,胸有成竹,自己就全做了🐶。即使是在团队协作中,与不同技术人员的沟通也会容易得多,让一个后端和一个前端去沟通,那完全是鸡同鸭讲,更不用说设计师与后端了。但如果有一个人懂产品懂设计懂前端懂后端,那沟通的结果显然不一样,因为他们讲的,彼此都能听得懂。

作为一名职业小菜鸟前端人,每天做的事就是制作静态界面,写写业务及交互,改改BUG,给别人挖个坑什么的。觉得自己没啥技术含量,缺少核心竞争力,感觉自己是淘汰的边缘人。钻研专精吧学识不够,实力又不允许,就想着往全栈之路试着走一走,多一把刷子就多一个武器。

小编有点啰嗦,其实全栈和技术大牛是殊途同归的,所以所有前端人应该明白,技术是为解决问题服务的,找准自己的位置,通过时间经历不小心成为全栈,而不是成为书本里的全栈,做一个务实的技术从业者,那么你们的明天一定是光明美好的。🤭

最近小编就用业务时间,捣鼓捣鼓我的H5移动端电商项目,为自己充充电,阅读之前,建议小伙伴可以先去看看我写的NodeJS全栈入门篇 - NodeJS全栈开发一个功能完善的Express项目-附完整源码,这次重点讲解后端部分,因为小编每天拿挤牙膏的时间,只完成小部分功能,就想着先回顾一下分享出来。如果真心觉得不错的话,请点个赞,欢迎留下宝贵的意见。🙏

前后端分离

前端项目采用的技术栈是基于Vue + Vant + Axios,用vue-cli构建前端界面,后端项目采用的技术栈是基于Node.js + Express + MySQL,用Express搭建的后端服务器。后续开发后台管理系统采用的技术栈是基于Vue + Element-ui前端架构。

在线演示DEMO地址👉:http://106.55.168.13

部分界面截图

需求清单

  • 验证码登录
  • 微博/github/微信/QQ登录
  • 密码登录
  • 个人资料
  • 上传头像
  • 退出登录
  • 首页界面
  • 发布功能(产品/内容)
  • 详情页(产品/内容)
  • 分页功能(产品/内容)
  • 评论功能(产品/内容)
  • 常用地址
  • 添加地址
  • 确认订单
  • 订单详情
  • 问题反馈
  • 我的关注
  • 我的粉丝(关注者)
  • 我的评论
  • 我的收藏
  • 我的订单(待付款/待发货/已发货)
  • 在线咨询
  • 消息列表
  • 消息详情
  • 账户设置
  • 绑定账号
  • 关于我们

后端

基础环境

安装配置 Node.js

Node.js 安装包及源码下载地址为:nodejs.org/en/download…

不同操作系统环境的安装配置 Node.js,请移步到菜鸟教程 www.runoob.com/nodejs/node…

创建工程目录

首先假定你已经安装了 Node.js,接下来为你的应用创建一个目录,然后进入此目录并将其作为当前工作目录。

mkdir myapp
cd myapp

为你的应用创建 package.json 文件

npm init -y

在 myapp 目录下安装 Express 并将其保存到依赖列表中

npm install express -S

在 myapp 目录中,创建名为 app.js 的文件,然后添加以下代码:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
})

app.listen(port, () => {
  console.log(`服务已启动 http://localhost:${port}`);
})

使用以下命令运行应用程序

node app.js

然后,在浏览器中输入 http://localhost:3000/ 以查看输出 Hello World!

或者选择Express应用程序生成器

npm install -g express-generator

通过应用生成器工具 express-generator 可以快速创建一个应用的骨架,具体操作流程请移步到 Express 官方文档 www.expressjs.com.cn/starter/gen…

项目结构

│  app.js                             // 入口文件
│  package.json                       // npm包管理所需模块及配置信息
├─db
│      dbConfig.js                    // mysql数据库基础配置    
├─public
│  └─uploads                          // 图片文件夹
├─routes
│      index.js                       // 初始化路由信息,自定义全局异常处理
│      users.js                       // 用户路由模块
├─services
│      authService.js                 // 业务逻辑处理 - 任务相关接口
│      userService.js                 // 业务逻辑处理 - 用户相关接口
└─utils
        constant.js                   // 自定义常量
        index.js                      // 封装连接mysql模块
        md5.js                        // 后端封装md5方法
        smsConfig.js                  // 腾讯云短信接入Nodejs SDK
        user-jwt.js                   // jwt-token验证和解析函数

技术栈

  • Node.js v10
  • express v4
  • mysql v5.7
  • express-jwt
  • express-session
  • nodemon
  • crypto
  • cors
  • boom
  • moment
  • multer
  • svg-captcha
  • tencentcloud-sdk-nodejs

功能模块

  • 验证码登录
  • 图形验证码
  • 密码登录
  • 微博/github登录
  • 修改头像
  • 编辑/获取个人信息

代码实现

后端用户登录功能使用了 jwt-token 认证模式来实现。使用 Expressexpress-sessionbody-parsercookie-parserboomcorsjsonwebtokenexpress-jwtMySQLmultersvg-captchamoment 等中间件和JS库来简化开发。

  • express-session:是基于 Express 框架专门用于处理 session 的中间件。session 的认证机制离不开 cookie,需要同时使用 cookieParser 中间件。
  • body-parser:对 post 请求的请求体进行解析的 Express 中间件。
  • cookie-parser:是 Node.js 中间件用来对cookie进行解析,主要包括普通cookie的解析和签名cookie的解析。
  • boom:处理程序异常状态,boom 是一个兼容 HTTP 的错误对象,他提供了一些标准的 HTTP 错误,比如400(参数错误)等。
  • cors:实现 Node 服务端跨域的 JS 库。
  • jsonwebtoken:基于 jwt 的概念实现安全的加密方案库,实现加密 token 和解析 token 的功能。
  • express-jwt:express-jwt 是在 jsonwebtoken 的基础上做了上层封装,基于 Express 框架下认证 jwt 的中间件,来实现 jwt 的认证功能。
  • MySQL:Node.js 连接 MySQL 数据库。
  • multer:是 Node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件.
  • svg-captcha:在 Node.js 中生成一个 SVG 格式的验证码。
  • moment: JavaScript 日期处理类库。

安装依赖库

npm i -S express body-parser cookie-parser express-session express-validator express-jwt
npm i -S boom cors jsonwebtoken mysql multer svg-captcha moment

功能实现

工具类方法

上一篇文章有详细讲解工具类方法,这里就不重复,请移步到我的另一篇博文《NodeJS全栈开发一个功能完善的Express项目》

业务逻辑层

在services文件夹下新建userService.js文件,定义用户验证码登录、密码登录、第三方登录、图形验证码校验、上传文件等API接口开发。

登录模块

先来看下登录模块整个流程图,包括验证码登录、密码登录、第三方登录,如下图:

验证码登录功能实现包含图形验证和短信验证组合,也可以单独分开,都是比较常见的校验方式,先来说说这两种。

图形验证码

使用 nodejs 做后端开发,需要请求验证码,搜寻了很多 nodejs 验证码库都没找到合适的。最后无意中在 github 上看到了 svg-captcha 这个库,发现他是将 text 转 svg 进行返回的,安全性也有保证,不会被识别成文字,能满足自己的需求。

附上地址👉:github.com/produck/svg…

安装

npm install -S svg-captcha

创建图形验证码

const svgCaptcha = require('svg-captcha');
 
const getCaptcha = (req, res) => {
  let codeConfig = {
    size: 4, // 验证码长度
    ignoreChars: '0o1i', // 验证码字符中排除 0o1i
    noise: 0, // 干扰线条数
    width: 60, // 宽度
    height: 30, // 高度
    inverse: false, // 翻转颜色
    fontSize: 35, // 字体大小
    // color: true, // 验证码的字符是否有颜色,默认没有,如果设定了背景,则默认有
    // background: '#cc9966' // 验证码图片背景颜色
  }
  let getCapCode = svgCaptcha.create(codeConfig); // 图形验证码
  req.session.captcha = getCapCode.text.toLowerCase(); // session存储

  res.type('svg'); // 响应类型
  res.status(200).send(getCapCode.data);
}

示例

创建算数验证码

let getCapCode = svgCaptcha.createMathExpr(codeConfig);

示例

短信验证

开通短信服务

短信验证需要用到服务器,腾讯云有免费短信开通功能,首次使用免费赠送国内短信100条,用于测试,挺不错的。

短信开通步骤:注册账号 -> 实名认证 -> 进入控制台 -> 选择短信模块,如下图所示:

已封装短信API接口,代码如下:

// smsConfig.js
const tencentcloud = require("tencentcloud-sdk-nodejs");

// 导入对应产品模块的client models。
const SmsClient = tencentcloud.sms.v20190711.Client;
const models = tencentcloud.sms.v20190711.Models;

const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;

// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey
let cred = new Credential("secretId", "secretKey");

// 实例化一个http选项,可选的,没有特殊需求可以跳过。
let httpProfile = new HttpProfile();
// 指定接入地域域名(默认就近接入)
httpProfile.endpoint = "sms.tencentcloudapi.com";

// 实例化一个client选项,可选的,没有特殊需求可以跳过。
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;

module.exports = {
    client: new SmsClient(cred, "", clientProfile),
    reqSms: new models.SendSmsRequest()
}

// userService.js
const sendCoreCode = (req, res) => {
  let { phone } = req.query;
  let sms = randomCode(1000, 9999);
  let params = {
    'PhoneNumberSet': [
      `+86${phone}`
    ],
    'TemplateID': '738936',
    'Sign': '懒人码农',
    'TemplateParamSet': [
      sms,
      '30'
    ],
    'SmsSdkAppid': '1400433036'
  }
  smsConfig.reqSms.from_json_string(JSON.stringify(params));

  smsConfig.client.SendSms(smsConfig.reqSms, (errMsg, response) => {
    // 请求异常返回,打印异常信息
    if (errMsg) {
      res.send({
        code: 400,
        msg: '短信发送失败'
      })
      return;
    }
    // 请求正常返回,打印response对象
    res.send({
      code: 200,
      msg: '短信发送成功'
    })
    validatePhoneCode.push({
      phone: phone,
      sms: sms
    })
  });
}

示例

第三方登录

OAuth 2.0 是目前最流行的授权机制,用来授权第三方应用,获取用户数据。比如常用的第三方账号微博、微信、QQ、github 登录方式思路是一样的。

简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

微博登录

申请接入流程:注册账号 -> 成为开发者 -> 创建应用 -> 获取appid & appkey -> 接入微博登录

微博开放平台网址:open.weibo.com/connect

其实过程都比较简单,逻辑基本上分成四个步骤:

  • 前端在登录界面进行跳转第三方授权界面进行授权,用户授权成功会跳转我们的回调界面并附加code参数。前端获取到code,将code传参给后端。
  • 后端调用获取access_token的接口,将code以及申请应用得到的app_id、app_secret参数进行传递。获取access_token。
  • 使用获得的access_token调用获取用户基本信息的接口,获取用户第三方平台的基本信息。
  • 将用户基本信息保存到数据库,然后将用户信息返回给前端。

参考微博API接口文档:open.weibo.com/wiki/%E6%8E…

代码实现

// utils/index.js weibo配置
const weiboConfig = {
  // oauth_uri: 'https://api.weibo.com/oauth2/authorize',
  access_token_url: 'https://api.weibo.com/oauth2/access_token',
  // 获取 weibo 用户信息 url // eg: https://api.weibo.com/2/users/show.json?access_token=******&uid=******
  user_url: 'https://api.weibo.com/2/users/show.json',
  
  redirect_uri: 'http://localhost:8080',
  client_id: '',
  client_secret: '',
};

// authService.js
const oauthWeibo = async (req, res, next) => {
  const err = validationResult(req);
  if (!err.isEmpty()) {
    const [{ msg }] = err.errors;
    next(boom.badRequest(msg));
  } else {
    let { code } = req.query;
    let tokenResponse = await axios({
      method: "post",
      url: `${weiboConfig.access_token_url}?client_id=${weiboConfig.client_id}&client_secret=${weiboConfig.client_secret}&grant_type=authorization_code&redirect_uri=${weiboConfig.redirect_uri}&code=${code}`,
      headers: {
        accept: "application/json",
      }
    }).then().catch(err => {
      return err.response;
    });

    let accessToken = tokenResponse.data.access_token;

    if (accessToken) {
      let uid = tokenResponse.data.uid;
      let result = await axios({
        method: "get",
        url: `${weiboConfig.user_url}?access_token=${accessToken}&uid=${uid}`,
        headers: {
          accept: "application/json",
        }
      });
      
      let token = getToken(accessToken);
      if (result.status == 200) {
        let user = await validateAuthUser(uid);
        if (user) {
          user[0].login_times += 1;
          let updateAuthUser = await setAuthUser(result.data, 2, accessToken, 0, user[0].login_times);
          if (updateAuthUser.affectedRows == 1) {
            let userinfo = {
              id: user[0].id,
              openid: user[0].openid,
              user_id: user[0].user_id,
              username: user[0].username,
              nickname: user[0].nickname,
              type: user[0].type,
              avatar_url: user[0].avatar_url,
              create_time: user[0].create_time,
              expire_time: user[0].expire_time,
              expires_in: user[0].expires_in,
              login_time: user[0].login_time,
              login_times: user[0].login_times,
            };
            res.send({
              code: CODE_SUCCESS,
              msg: "weibo账号登录成功",
              data: {
                token,
                userinfo,
              },
            });

          } else {
            res.send({
              code: CODE_ERROR,
              msg: "weibo账号登录失败",
              data: null,
            });
          }

        } else {
          let addAuthUser = await setAuthUser(result.data, 2, accessToken, 1, null);
          console.log("addAuthUser===", addAuthUser);
          if (addAuthUser.affectedRows == 1) {
            let queryUser = await getAuthUser(addAuthUser.insertId);
            let userImage = await addUserImage(queryUser[0].openid);

            if (userImage) {
              let userinfo = {
                id: queryUser[0].id,
                openid: queryUser[0].openid,
                user_id: queryUser[0].user_id,
                username: queryUser[0].username,
                nickname: queryUser[0].nickname,
                type: queryUser[0].type,
                avatar_url: queryUser[0].avatar_url,
                create_time: queryUser[0].create_time,
                expire_time: queryUser[0].expire_time,
                expires_in: queryUser[0].expires_in,
                login_time: queryUser[0].login_time,
                login_times: queryUser[0].login_times,
              };
              
              res.send({
                code: CODE_SUCCESS,
                msg: "weibo账号登录成功",
                data: {
                  token,
                  userinfo,
                },
              });
            }
          } else {
            res.send({
              code: CODE_ERROR,
              msg: "weibo账号登录失败",
              data: null,
            });
          }
        }

      } else {
        res.send({
          code: CODE_ERROR,
          msg: "weibo账号登录失败",
          data: null,
        });
      }

    } else {
      if (tokenResponse.data.error_code == 21325) {
        res.send({
          code: CODE_ERROR,
          msg: "code码无效或已过期",
        });
      } else if (tokenResponse.data.error_code == 21327) {
        res.send({
          code: -2,
          msg: "token已过期",
        });
      } else {
        res.send({
          code: CODE_ERROR,
          msg: tokenResponse.data.error_description,
        });
      }
    } 
  }
};
github登录

github应用登记流程如下图所示:

创建完之后记住页面上方的 Client ID 和 Client Secret,后面开发时需要用到这两个参数。

代码实现

// utils/index.js github配置
const githubConfig = {
    // oauth_uri: 'https://github.com/login/oauth/authorize',
    access_token_url: 'https://github.com/login/oauth/access_token',
    // 获取 github 用户信息 url // eg: https://api.github.com/user?access_token=******&scope=&token_type=bearer
    user_url: 'https://api.github.com/user',

    redirect_uri: 'http://localhost:8080',
    client_id: '',
    client_secret: '',
};

// authService.js
const oauthGithub = async (req, res, next) => {
  const err = validationResult(req);
  // 如果验证错误,empty不为空
  if (!err.isEmpty()) {
    // 获取错误信息
    const [{ msg }] = err.errors;
    // 抛出错误,交给我们自定义的统一异常处理程序进行错误返回
    next(boom.badRequest(msg));
  } else {
    let { code } = req.query;
    let tokenResponse = await axios({
      method: "post",
      url: `${githubConfig.access_token_url}?client_id=${githubConfig.client_id}&client_secret=${githubConfig.client_secret}&code=${code}`,
      headers: {
        accept: "application/json",
      },
    });

    let accessToken = tokenResponse.data.access_token;
    if (accessToken) {
      let result = await axios({
        method: "get",
        url: `${githubConfig.user_url}`,
        headers: {
          accept: "application/json",
          Authorization: `token ${accessToken}`,
          "User-Agent": "jackchen0120",
        },
      });

      let token = getToken(accessToken);
      if (result.status == 200) {
        let user = await validateAuthUser(result.data.id);
        if (user) {
          user[0].login_times += 1;
          let updateAuthUser = await setAuthUser(result.data, 3, accessToken, 0, user[0].login_times);
          if (updateAuthUser.affectedRows == 1) {
            let userinfo = {
              id: user[0].id,
              openid: user[0].openid,
              user_id: user[0].user_id,
              username: user[0].username,
              nickname: user[0].nickname,
              type: user[0].type,
              avatar_url: user[0].avatar_url,
              create_time: user[0].create_time,
              expire_time: user[0].expire_time,
              expires_in: user[0].expires_in,
              login_time: user[0].login_time,
              login_times: user[0].login_times,
            };
            res.send({
              code: CODE_SUCCESS,
              msg: "github账号登录成功",
              data: {
                token,
                userinfo,
              },
            });
          } else {
            res.send({
              code: CODE_ERROR,
              msg: "github账号登录失败",
              data: null,
            });
          }
        } else {
          let addAuthUser = await setAuthUser(result.data, 3, accessToken, 1, null);
          if (addAuthUser.affectedRows == 1) {
            let queryUser = await getAuthUser(addAuthUser.insertId);
            let userImage = await addUserImage(queryUser[0].openid);
            
            if (userImage) {
              let userinfo = {
                id: queryUser[0].id,
                openid: queryUser[0].openid,
                user_id: queryUser[0].user_id,
                username: queryUser[0].username,
                nickname: queryUser[0].nickname,
                type: queryUser[0].type,
                avatar_url: queryUser[0].avatar_url,
                create_time: queryUser[0].create_time,
                expire_time: queryUser[0].expire_time,
                expires_in: queryUser[0].expires_in,
                login_time: queryUser[0].login_time,
                login_times: queryUser[0].login_times,
              };

              res.send({
                code: CODE_SUCCESS,
                msg: "github账号登录成功",
                data: {
                  token,
                  userinfo,
                },
              });
            }
          } else {
            res.send({
              code: CODE_ERROR,
              msg: "github账号登录失败",
              data: null,
            });
          }
        }

      } else {
        res.send({
          code: CODE_ERROR,
          msg: "github账号登录失败",
        });
      }

    } else {
      res.send({
        code: CODE_ERROR,
        msg: "code码无效或已过期",
      });
    }
  }
};
上传文件

图片上传是项目开发中经常用到的功能,Node 社区在这方面也有了相对完善的支持。常用的开源组件有 multerformidable 等,借助这两个开源组件,可以轻松搞定图片上传。

本地创建图片服务器

安装 multer,借助 Express、multer支持单图、多图上传

npm install -S multer

代码实现

// routes/users.js
const multer = require('multer');
const upload = multer({
    dest: path.join(__dirname, '../public/uploads/')
}).single('file');
// 上传头像接口
router.post('/editUserAvatar', upload, user.editUserAvatar);

// userService.js单图上传
const editUserAvatar = async (req, res, next) => {
  const err = validationResult(req);
  if (!err.isEmpty()) {
    const [{ msg }] = err.errors;
    next(boom.badRequest(msg));
  } else {
    let file = req.file;
    console.log(req.file);
    if (!file) {
        // 判断图片文件是否存在
        res.send({
          code: CODE_ERROR,
          msg: '上传失败'
        })
    } else {
        // 存储上传对象信息
        let fileInfo = {};
        // 获取后缀扩展
        // let extName = file.originalname.slice(file.originalname.lastIndexOf('.'));
        let extName = file.mimetype.slice(file.mimetype.lastIndexOf('/')).replace('/', '.');
        let upload = path.join(__dirname, '../public/uploads/');
        // 修改名字,第一个参数为旧路径,第二个参数为新路径(注意:旧路径要和上面的dest保持一致)
        fs.renameSync(upload + file.filename, upload + file.filename + extName);
        // 获取文件信息
        // fileInfo.mimetype = file.mimetype;
        // fileInfo.originalname = file.originalname;
        // fileInfo.size = file.size;
        // fileInfo.path = file.path;

        // 设置响应类型及编码
        res.set({
            'content-type': 'application/json; charset=utf-8'
        })

        let { user_id } = req.body;
        let imgUrl = 'http://localhost:3000/static/uploads/' + file.filename + extName;
        
        if (user_id) {
            let sql = `update user_image set url='${imgUrl}', create_time='${moment().format('YYYY-MM-DD HH:mm:ss')}' where user_id='${user_id}'`;
            let image = await queryOne(sql);

            if (image) {
              res.send({
                code: CODE_SUCCESS,
                msg: '上传成功',
                url: imgUrl
              })
            } else {
              res.send({
                code: CODE_ERROR,
                msg: '用户user_id不存在'
              })
            }
            
        } else {
            res.send({
                code: CODE_ERROR,
                msg: '用户user_id不能为空'
            })
        }
    }
  }
}
本地上传图片到七牛云空间

注册账号 -> 实名认证 -> 控制台 -> 选择对象存储kodo -> 新建空间 -> 申请空间成功

免费获取10GB存储空间,七牛云网址:www.qiniu.com/

可以参考Nodejs SDK 七牛云开发者文档:developer.qiniu.com/kodo/sdk/12…

安装

npm install -S qiniu

代码实现

// utils/index.js
const qiniu = require('qiniu');
// 创建七牛云上传凭证
let bucket = ''; // 上传的空间名
let imageUrl = ''; // 域名名称
let accessKey = ''; // 实名认证后可以获取密钥
let secretKey = '';
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
let options = {
  scope: bucket,
};
let putPolicy = new qiniu.rs.PutPolicy(options);
let uploadToken = putPolicy.uploadToken(mac);
let qnConfig = new qiniu.conf.Config();
qnConfig.zone = qiniu.zone.Zone_z2;
module.exports = uploadToken;

// userService.js 获取上传凭证
const uploadQiniu = (req, res, next) => {
  const err = validationResult(req);
  if (!err.isEmpty()) {
    const [{ msg }] = err.errors;
    next(boom.badRequest(msg));
  } else {
    res.send({
      code: CODE_SUCCESS,
      msg: '上传凭证获取成功',
      upToken: uploadToken
    })
  }
}

请求路由处理

在routes文件夹下新建index.js和user.js文件。

index.js文件是初始化路由信息,自定义全局异常处理,代码如下:

const express = require('express');
const userRouter = require('./users'); // 引入user路由模块
const { jwtAuth, decode } = require('../utils/user-jwt'); // 引入jwt认证函数
const router = express.Router(); // 注册路由 

router.use(jwtAuth); // 注入认证模块

router.use('/api/users', userRouter); // 注入用户路由模块

// 自定义统一异常处理中间件,需要放在代码最后
router.use((err, req, res, next) => {
  // 自定义用户认证失败的错误返回
  if (err && err.name === 'UnauthorizedError') {
    const { status = 401, message } = err;
    // 抛出401异常
    res.status(status).json({
      code: status,
      msg: 'token失效,请重新登录',
      data: null
    })
  } else {
    const { output } = err || {};
    // 错误码和错误信息
    const errCode = (output && output.statusCode) || 500;
    const errMsg = (output && output.payload && output.payload.error) || err.message;
    res.status(errCode).json({
      code: errCode,
      msg: errMsg
    })
  }
})

module.exports = router;

user.js文件是用户路由模块,代码如下:

const express = require('express');
const router = express.Router();
const user = require('../services/userService');
const auth = require('../services/authService');
const path = require('path');

const multer = require('multer');
const upload = multer({
    dest: path.join(__dirname, '../public/uploads/')
}).single('file');

// 验证码登录
router.post('/login', user.login);
// 获取图形验证码
router.get('/getCaptcha', user.getCaptcha);
// 发送短信验证码
router.get('/sendCoreCode', user.sendCoreCode);
// 密码登录
router.post('/loginPwd', user.loginPwd);
// github登录
router.get('/oauthGithub', auth.oauthGithub);
// weibo登录
router.get('/oauthWeibo', auth.oauthWeibo);
// 获取个人信息
router.get('/getMemberInfo', user.getMemberInfo);
// 修改个人信息
router.post('/modifyUser', user.modifyUser);
// 上传头像
router.post('/editUserAvatar', upload, user.editUserAvatar);
// 获取七牛云上传凭证
router.get('/qiniuToken', user.uploadQiniu);

module.exports = router;

入口文件配置

在根目录 app.js 程序入口文件中,导入 Express 模块,再引入常用的中间件和自定义 routes 路由的中间件,代码如下:

const bodyParser = require('body-parser'); // 引入body-parser模块
const cookieParser = require('cookie-parser'); // 引入cookie-parser模块,用于客户端操作cookie
const express = require('express'); // 引入express模块
const path = require('path'); // 引入path模块,处理文件路径小工具
const cors = require('cors'); // 引入cors模块,用于解决跨越
const routes = require('./routes'); //导入自定义路由文件,创建模块化路由
const session = require('express-session'); // 引入express-session模块,用于服务器端操作session
const app = express();

const port = process.env.PORT || 3000;

app.use('/static', express.static(path.join(__dirname, 'public'))); // 静态资源
app.use(bodyParser.json()); // 解析json数据格式
app.use(bodyParser.urlencoded({extended: true})); // 解析form表单提交的数据application/x-www-form-urlencoded

app.use(cookieParser());
app.use(session({
	secret: 'keyboard cat',
	resave: false,
	saveUninitialized: true,
	cookie: ('name', 'value', {maxAge:  5 * 60 * 1000, secure: false})
}));

app.use(cors()); // 注入cors模块解决跨域

app.use('/', routes);

app.listen(port, () => { // 监听3000端口
	console.log('服务已启动 http://localhost:3000');
})

前端

目录结构

│  package.json                      // npm包管理所需模块及配置信息
│  vue.config.js                     // webpack配置
├─public
│      favicon.ico                   // 图标
│      index.html                    // 入口html文件
└─src
    │  App.vue                       // 根组件
    │  main.js                       // 程序入口文件
    │  router.js                     // 单页面路由注册组件
    ├─assets                         // 存放公共图片文件夹
    ├─components
    │  │  Footer.vue                 // 页面底部公用组件
    │  │  Comment.vue                // 评论列表组件
    │  │  List.vue                   // 公用列表组件
    │  │  Loading.vue                // 加载状态组件
    │  │  popupDialog.vue            // 对话框组件
    │  │  Suggest.vue                // 意见反馈表单组件
    │  │  
    │  └─address
    │          add.vue               // 添加地址组件
    │          list.vue              // 常用地址列表组件
    ├─js
    │      apis.js                  // API接口统一管理
    │      area.js                  // 省市区JSON数据
    │      axios.js                 // axios封装与拦截器配置
    │      env.js                   // 自动部署服务器环境
    │      utils.js                 // 统一封装工具类方法   
    ├─store
    │      index.js                 // 状态管理仓库入口文件
    │      types.js                 // 状态管理常量定义  
    ├─style
    │      common.less               // 公共样式文件 
    │      style.less                // 基础样式文件                  
    └─view
        ├─advise
        │      detail.vue            // 问题反馈详情
        │      list.vue              // 问题反馈列表
        ├─client
        │      index.vue             // 我的评论列表  
        ├─collect
        │      index.vue             // 我的收藏列表 
        ├─contentDetail
        │      index.vue             // 内容详情
        ├─contentsList
        │      index.vue             // 内容列表
        ├─fans
        │      index.vue             // 我的粉丝列表
        ├─follow
        │      index.vue             // 我的关注列表
        ├─login
        │      index.vue             // 登录界面
        ├─message
        │      index.vue             // 消息列表
        ├─my
        │  │  card.vue               // ta的IP详情
        │  │  info.vue               // 我的资料详情
        │  └─card-item
        │          focus.vue         // 是否关注弹框
        ├─online
        │      index.vue             // 在线咨询
        ├─order
        │      detail.vue            // 订单详情
        │      list.vue              // 我的订单列表
        │      placeOrder.vue        // 确认订单
        ├─productDetail 
        │      index.vue             // 产品详情   
        ├─productsList
        │      index.vue             // 产品列表
        ├─publishCon
        │      index.vue             // 内容发布
        ├─publishPro
        │      index.vue             // 产品发布
        ├─setting
        │      index.vue             // 设置
        └─user
                index.vue            // 个人中心

技术栈

  • vue2.6
  • vue-router
  • vuex
  • axios
  • vant
  • amfe-flexible
  • postcss-pxtorem
  • js-cookie
  • flex

MySQL数据库

MySQL数据库安装使用流程,可以移步到我的另一篇博客<前端必知必会MySQL的那些事儿 - NodeJS全栈成长之路>有详细介绍。

表结构设计

使用SQL命令或可视化工具来创建数据库,再根据实际业务需求理清表之间的关系建表,目前有用户表user,用户信息表user_info,第三方用户表user_third_auth,文件存储表user_image等等。

--创建数据库

CREATE DATABASE `node_test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

--创建用户表

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自动递增',
  `user_id` varchar(40) NOT NULL COMMENT '关联的本站用户ID',
  `openid` varchar(64) DEFAULT NULL COMMENT '第三方唯一ID',
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` bigint(11) DEFAULT NULL COMMENT '手机号',
  `password` varchar(50) DEFAULT NULL COMMENT '密码',
  `status` tinyint(1) DEFAULT NULL COMMENT '用户状态:0-禁用,1-开启',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';

--创建用户信息表

CREATE TABLE `user_info` (
  `id` tinyint(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(36) NOT NULL COMMENT '关联的本站用户ID',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  `nickname` varchar(30) DEFAULT NULL COMMENT '昵称',
  `age` varchar(3) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别:u-未知 m-男 w-女',
  `birthday` varchar(10) DEFAULT NULL COMMENT '生日',
  `code` varchar(6) DEFAULT NULL COMMENT '省市区编码',
  `area` varchar(50) DEFAULT NULL COMMENT '所在地区',
  `address` varchar(100) DEFAULT NULL COMMENT '详细地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户信息表';

--创建第三方用户表

CREATE TABLE `user_third_auth` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(40) DEFAULT '' COMMENT '关联的本站用户ID',
  `openid` varchar(64) DEFAULT NULL COMMENT '第三方唯一ID',
  `type` tinyint(1) DEFAULT NULL COMMENT '类型:1-QQ 2-微博 3-github 4-微信',
  `nickname` varchar(30) DEFAULT NULL COMMENT '第三方昵称',
  `access_token` varchar(128) DEFAULT NULL COMMENT '第三方获取的token(可有可无)',
  `avatar_url` varchar(255) DEFAULT NULL COMMENT '头像',
  `username` varchar(50) DEFAULT NULL COMMENT '第三方用户名',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `login_time` datetime DEFAULT NULL COMMENT '登录时间',
  `login_times` bigint(12) DEFAULT NULL COMMENT '登录次数',
  `expires_in` bigint(13) DEFAULT NULL COMMENT '有效期',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='第三方用户表';

--创建图片存储表

CREATE TABLE `user_image` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(40) NOT NULL COMMENT '关联的本站用户ID',
  `url` varchar(500) DEFAULT NULL COMMENT '图片地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='图片存储表';

部署上线

前后端部署上线流程,可以移步到我的另一篇博客<NodeJS全栈开发一个功能完善的Express项目>有详细介绍。

写在最后

由于篇幅太长,先写到这吧,后续会作为商城项目实战系列篇进行一一分享出来,主要是介绍自己开发总结及挖坑,不断优化填坑~🐶

如果小伙伴看完真觉得不错,那就点个👍或者给个💖吧!你们的赞和 star 是我编写更多更精彩文章的动力!

附上github地址:github.com/jackchen012…

此项目还有很多不足或优化的地方,期待提供宝贵意见。

欢迎关注公众号👉【懒人码农】,我们一起学习一起进步,还会不定期输出前端精彩文章(实战篇系列)。💪