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