NodeJS全栈开发一个功能完善的Express项目

5 篇文章 0 订阅

NodeJS全栈开发一个功能完善的Express项目1


前言

Node.js对前端来说无疑具有里程碑意义,与其越来越流行的今天,掌握Node.js技术已经不仅仅是加分项,而是前端攻城师们必须要掌握的一项技能。而Express基于Node.js平台,快速、开放、极简的Web开发框架,成为Node.js最流行的框架,所以使用Express进行web服务端的开发是个不错且可信赖的选择。但是Express初始化后,并不马上就是一个开箱即用,各种功能完善的web服务端项目,例如:MySQL连接、jwt-token认证、md5加密、中间件路由模块、异常错误处理、跨域配置、自动重启等一系列常见的功能,需要开发者自己手动配置安装插件和工具来完善功能,如果你对web服务端开发或者Express框架不熟悉,那将是一项耗费巨大资源的工作。


提示:以下是本篇文章正文内容,下面案例可供参考

一、前后端分离

前端项目采用的技术栈是基于Vue + iView,用vue-cli构建前端界面,后端项目采用的技术栈是基于Node.js + Express + MySQL,用Express搭建的后端服务器

二、部分效果截图

1.登录

在这里插入图片描述

2.注册

在这里插入图片描述

三、前端部分

1.基础环境

node: 10.15.3
npm:6.14.7
vue-cli:4.4.6
在这里插入图片描述
本项目前端使用的是vue-cli4.0版本创建项目
安装vue-cli4.0版本,有条件可以配置下cnpm,这样下载快点,用yarn也中

npm install @vue/cli -g

创建项目

vue create xxx(项目名称)
cd xxx
npm run serve

2.技术栈

vue2.6
vue-router
vuex
axios
webpack
ES6/7
flex
iViewUI

3.功能模块

登录(登出)
注册
记住密码

4.开发配置

在项目根目录创建vue.config.js

const path = require('path');
module.exports = {
  devServer: {
    overlay: {
      warnings: true,
      errors: true
    },
    host: "localhost",
    port: 8080,
    https: false,
    open: false,
    hotOnly: true,
    proxy: {
      "/api": {
        target: "http:127.0.0.1:8090",
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          "^/api": "/"
        }
      }
    }
  }
}

5.安装依赖

安装axios用于网路请求
安装view-design UI模块

6.登录、注册模块

<template>
  <div class="Login">
    <div class="Login-header">
      <img src="../assets/study.png" alt="logo" />
      <span>iview学习练习平台</span>
    </div>
    <div class="Login-box" v-if="typeView != 2">
      <div class="Login-text">
        <a
          href="javascript:;"
          :class="typeView == 0 ? 'active' : ''"
          @click="handleTab(0)"
          >登录</a
        >
        <b>·</b>
        <a
          href="javascript:;"
          :class="typeView == 1 ? 'active' : ''"
          @click="handleTab(1)"
          >注册</a
        >
      </div>
      <!-- 登录模块 -->
      <div class="right-content" v-show="typeView == 0">
        <div class="input-box">
          <input
            type="text"
            autocomplete="off"
            class="input"
            placeholder="请输入手机号/登录邮箱"
            v-model="formLogin.userName"
          />
          <input
            type="password"
            autocomplete="off"
            class="input"
            maxlength="20"
            @key.enter="login"
            placeholder="请输入登录密码"
            v-model="formLogin.userPwd"
          />
          <Button
            class="loginBtn"
            type="primary"
            :disabled="isDisabled"
            :loading="isLoading"
            @click.stop="login"
            >立即登录</Button
          >
          <div class="option">
            <Checkbox
              class="remember"
              v-model="checked"
              @on-change="checkChange"
            >
              <span class="checked">记住我</span>
            </Checkbox>
            <span class="forget-pwd" @click.stop="forgetPwd">忘记密码?</span>
          </div>
        </div>
      </div>
      <!-- 注册模块 -->
      <div class="right-content" v-show="typeView == 1">
        <div class="input-box">
          <input
            type="text"
            autocomplete="off"
            class="input"
            placeholder="请输入手机号/登录邮箱"
            v-model="formRegister.userName"
          />
          <input
            type="password"
            autocomplete="off"
            class="input"
            maxlength="20"
            @key.enter="register"
            placeholder="请输入密码"
            v-model="formRegister.userPwd"
          />
          <input
            type="password"
            autocomplete="off"
            class="input"
            maxlength="20"
            @key.enter="register"
            placeholder="请再次输入密码"
            v-model="formRegister.userPwd2"
          />
          <Button
            class="loginBtn"
            type="primary"
            :disabled="isRegAble"
            :loading="isLoading"
            @click.stop="register"
            >立即注册</Button
          >
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { login, register } from "../utils/api";
export default {
  name: "login",
  data() {
    return {
      formLogin: {
        userName: "",
        userPwd: "",
      },
      formRegister: {
        userName: "",
        userPwd: "",
        userPwd2: "",
      },
      isLoading: false,
      checked: false,
      typeView: 0, //登录 注册 选择
    };
  },
  computed: {
    //登录按钮状态
    isDisabled() {
      return !(this.formLogin.userName && this.formLogin.userPwd);
    },
    //注册按钮状态
    isRegAble() {
      return !(
        this.formRegister.userName &&
        this.formRegister.userPwd &&
        this.formRegister.userPwd2
      );
    },
  },
  mounted() {
    this.getCookie();
  },
  methods: {
    // 登录/注册tab切换
    handleTab(type) {
      this.typeView = type;
      this.clearInput();
    },
    // 是否勾选记住密码
    checkChange(status) {
      console.log(status);
      this.checked = status;
    },
    //忘记密码
    forgetPwd() {
      this.$Message.info("忘记密码,请联系客服");
    },
    //登录
    login() {
      if (this.isDisabled || this.isLoading) {
        return false;
      }
      if (!this.$Valid.validUserName(this.formLogin.userName)) {
        this.$Message.error("请输入正确的邮箱/手机号");
        return false;
      }
      if (!this.$Valid.validPass(this.formLogin.userPwd)) {
        this.$Message.error("密码应为8到20位字母或数字");
        return false;
      }
      //判断复选框是否被勾选,勾选则调用配置cookie方法
      if (this.checked) {
        this.setCookie(this.formLogin.userName, this.formLogin.userPwd, 7);
      } else {
        this.clearCookie();
      }
      this.isLoading = true;
      let form = {
        username: this.formLogin.userName,
        password: this.formLogin.userPwd,
      };
      console.log(form);
      login(form)
        .then((result) => {
          console.log(result);
          this.isLoading = false;
          if (result.retCode == 0) {
            this.clearInput();
            this.$Message.success("登录成功");
            this.$router.push("/");
          } else {
            this.$Message.error(result.retMsg);
          }
        })
        .catch(() => {
          this.isLoading = false;
          this.$Message.error("登陆失败");
        });
    },
    //注册
    register() {
      if (this.isRegAble || this.isLoading) {
        return false;
      }
      if (!this.$Valid.validUserName(this.formRegister.userName)) {
        this.$Message.error("请输入正确的邮箱/手机号");
        return false;
      } else if (!this.$Valid.validPass(this.formRegister.userPwd)) {
        this.$Message.error("密码应为8到20位字母或数字!");
        return false;
      } else if (!this.$Valid.validPass(this.formRegister.userPwd2)) {
        this.$Message.error("确认密码有误");
        return false;
      } else if (this.formRegister.userPwd2 !== this.formRegister.userPwd) {
        this.$Message.error("两次密码不一致");
        return false;
      }
      this.isLoading = true;
      let data = {
        username: this.formRegister.userName,
        password: this.formRegister.userPwd2,
      };
      console.log(data);
      register(data)
        .then((result) => {
          console.log(result);
          this.isLoading = false;
          if (result.retCode == 0) {
            this.clearInput();
            this.$Message.success("注册成功");
            this.$router.push("/");
          } else {
            this.$Message.error(result.retMsg);
          }
        })
        .catch(() => {
          this.isLoading = false;
          this.$Message.error("注册失败");
        });
    },
    //设置cookie
    setCookie(userName, userPwd, days) {
      //获取cookie设置时间
      let exdate = new Date();
      //保存天数
      exdate.setTime(exdate.getTime() + 24 * 60 * 60 * 1000 * days);
      //字符串拼接cookie
      window.document.cookie = `userName=${userName};path/;expires=${exdate.toUTCString()}`;
      window.document.cookie = `userPwd=${userPwd};path/;expires=${exdate.toUTCString()}`;
    },
    //读取cookie
    getCookie() {
      if (document.cookie.length > 0) {
        let arr = document.cookie.split(";");
        console.log(arr);
        for (let i = 0; i < arr.length; i++) {
          let arrData = arr[i].split("=");
          if (arrData[0] == "userName") {
            this.formLogin.userName = arrData[1];
          } else if (arrData[0] == "userPwd") {
            this.formLogin.userPwd = arrData[1];
          }
        }
      }
    },
    //清除cookie
    clearCookie() {
      this.setCookie("", "", -1);
    },
    //清空输入框
    clearInput() {
      this.formLogin = {
        username: "",
        userPwd: "",
      };
      this.formRegister = {
        username: "",
        userPwd: "",
      };
    },
  },
};
</script>

<style lang="scss" scoped>
.Login {
  background-image: url("../assets/iview_bg.jpg");
  background-position: center;
  background-size: cover;
  position: relative;
  width: 100%;
  height: 100%;

  .Login-header {
    padding-top: 30px;
    padding-left: 40px;
    span {
      font-size: 18px;
      font-weight: bold;
      display: inline-block;
      vertical-align: -4px;
      color: rgba(0, 0, 0, 1);
    }
    img {
      vertical-align: middle;
      display: inline-block;
      margin-right: 15px;
    }
  }

  .Login-box {
    position: absolute;
    left: 64vw;
    top: 50%;
    -webkit-transform: translateY(-50%);
    transform: translateY(-50%);
    box-sizing: border-box;
    text-align: center;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.3);
    border-radius: 2px;
    width: 420px;
    background: #fff;
    padding: 45px 35px;
    .option {
      text-align: left;
      margin-top: 18px;
      .checked {
        padding-left: 5px;
      }
      .forget-pwd,
      .goback {
        float: right;
        font-size: 14px;
        font-weight: 400;
        color: #4981f2;
        line-height: 20px;
        cursor: pointer;
      }
      .protocol {
        color: #4981f2;
        cursor: pointer;
      }
    }

    .Login-text {
      width: 100%;
      text-align: center;
      padding: 0 0 30px;
      font-size: 24px;
      letter-spacing: 1px;
      a {
        padding: 10px;
        color: #969696;
        &.active {
          font-weight: 600;
          color: rgba(73, 129, 242, 1);
          border-bottom: 2px solid rgba(73, 129, 242, 1);
        }
        &:hover {
          border-bottom: 2px solid rgba(73, 129, 242, 1);
        }
      }
      b {
        padding: 10px;
      }
    }
    .title {
      font-weight: 600;
      padding: 0 0 30px;
      font-size: 24px;
      letter-spacing: 1px;
      color: rgba(73, 129, 242, 1);
    }

    .input-box {
      .input {
        &:nth-child(1) {
          /*margin-top: 10px;*/
        }
        &:nth-child(2),
        &:nth-child(3) {
          margin-top: 20px;
        }
      }
    }

    .loginBtn {
      width: 100%;
      height: 45px;
      margin-top: 40px;
      font-size: 15px;
    }

    .input {
      padding: 10px 0px;
      font-size: 15px;
      width: 350px;
      color: #2c2e33;
      outline: none; // 去除选中状态边框
      border: 1px solid #fff;
      border-bottom-color: #e7e7e7;
      background-color: rgba(0, 0, 0, 0); // 透明背景
    }

    input:focus {
      border-bottom-color: #0f52e0;
      outline: none;
    }
    .button {
      line-height: 45px;
      cursor: pointer;
      margin-top: 50px;
      border: none;
      outline: none;
      height: 45px;
      width: 350px;
      background: rgba(216, 216, 216, 1);
      border-radius: 2px;
      color: white;
      font-size: 15px;
    }
  }

  // 滚动条样式
  ::-webkit-scrollbar {
    width: 10px;
  }
  ::-webkit-scrollbar-track {
    -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.3);
    border-radius: 8px;
  }
  ::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
  }
  ::-webkit-scrollbar-thumb:window-inactive {
    background: rgba(0, 0, 0, 0.4);
  }
  //设置更改input 默认颜色
  ::-webkit-input-placeholder {
    /* WebKit browsers */
    color: #bebebe;
    font-size: 16px;
  }
  ::-moz-placeholder {
    /* Mozilla Firefox 19+ */
    color: #bebebe;
    font-size: 16px;
  }
  :-ms-input-placeholder {
    /* Internet Explorer 10+ */
    color: #bebebe;
    font-size: 16px;
  }
  input:-webkit-autofill {
    box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset;
    -webkit-box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset;
    -webkit-text-fill-color: #2c2e33;
  }
  .ivu-checkbox-wrapper {
    margin-right: 0;
  }
}
</style>

1.封装网络请求模块

在src目录下创建http.js

import Vue from 'vue';
import axios from 'axios';
import { apiUrl } from './url'

//创建实例
const http = axios.create({
    baseURL: apiUrl,
    timeout: 5000
});


http.interceptors.request.use(
    config => {
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

http.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        return Promise.reject(error);
    }
);


export default http;

2.创建接口登录、注册、忘记密码

import http from './http';


//登录接口
export function login(data) {
    return http({
        url: "/api/login",
        method: "POST",
        data
    })
}


// 注册接口
export function register(data){
    return http({
        url:"/api/register",
        method: "POST",
        data
    });
}

//修改密码
export function resetPwd(data){
    return http({
        url:"/api/resetPwd",
        method: "POST",
        data
    });
}

3.创建全局函数

export function validEmail(val) {
    return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(val);
}

export function validPhone(val) {
    return /^1[3456789]\d{9}$/.test(val);
}

export function validPass(val) {
    return /^[a-zA-Z\d]{8,20}$/.test(val);
    // return /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$/.test(val);
    // return /^.{6,16}$/.test(val);
}

export function validUserName(name) {
    return validEmail(name) || validPhone(name);
}

export function validCode(val) {
    return /^[0-9]{6}$/.test(val);
}

export function userName(str) {
    const re = /^[\u4E00-\u9FA5A-Za-z0-9]+$/
    return re.test(str);
}

export function validateMainName2(name) {
    const re = /^[a-zA-Z0-9_-]{1,19}$/
    return re.test(name);
}

export function validateNickName(name) {
    const re = /^[a-zA-Z0-9\u4E00-\u9FA5]{2,10}$/
    return re.test(name);
}

export function formatDate(value) {
    if (!value) {
        return '';
    }
    let d = new Date(value);
    let year = d.getFullYear();
    let month = (d.getMonth() + 1) < 10 ? '0' + (d.getMonth() + 1) : (d.getMonth() + 1);
    let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
    return year + '-' + month + '-' + day;
}

export function formatTime(value) {
    if (!value) {
        return '';
    }
    let d = new Date(value);
    let year = d.getFullYear();
    let month = d.getMonth() + 1;
    let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
    let hour = d.getHours() < 10 ? '0' + d.getHours() : '' + d.getHours();
    let minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : '' + d.getMinutes();
    let seconds = d.getSeconds() < 10 ? '0' + d.getSeconds() : '' + d.getSeconds();
    return month + '月' + day + '日' + ' ' + hour + '时';
}

export function formatHour(value) {
    if (!value) {
        return '';
    }
    let d = new Date(value);
    let year = d.getFullYear();
    let month = d.getMonth() + 1;
    let day = d.getDate() < 10 ? '0' + d.getDate() : '' + d.getDate();
    let hour = d.getHours();
    return hour;
}

export function timeFromNow(value) {
    let currentDate = Date.now();
    let timestamp = currentDate - value;
    switch (true) {
        case timestamp > 86400000:
            return `${Math.floor(timestamp / 86400000)}天前`
            break;
        case timestamp > 3600000:
            return `${Math.floor(timestamp / 3600000)}小时前`
            break;
        case timestamp > 60000:
            return `${Math.floor(timestamp / 60000)}分钟前`
            break;
        case timestamp > 1000:
            return `${Math.floor(timestamp / 1000)}秒钟前`
            break;
    }
}


export default {
    validEmail,
    validPhone,
    validUserName,
    validCode,
    validPass,
    userName,
    validateMainName2,
    validateNickName,
    formatDate,
    timeFromNow,
    formatHour
}

在mian.js中引入并全局使用

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Valid from './utils/valid'


import ViewUI from 'view-design'
import 'view-design/dist/styles/iview.css'

Vue.use(ViewUI)
Vue.prototype.$Valid = Valid;
Vue.config.productionTip = false

//路由守卫
router.beforeEach((to, from, next) => {
  document.title = to.meta.title;
  const isLogin = localStorage.isLogin ? true : false;
  if (isLogin) {
    next();
  } else {
    if (to.path === "/login") {
      next();
    } else {
      next("/login");
    }
  }
});

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

到这里,前端的登录注册功能就基本实现了。接下来要实现后端的接口部分了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值