前言
- 通过搭建一个简单项目,练习vue全家桶,elementui,node,koa,axios等。
- 项目为上传下载图片,可公共展示,个人注册、登录、设置图片分类等功能
- 本项目按照本人搭建与写入顺序进行,文件会发生多次拆分。所有内容都抱起来过。
- 第一次写文章,内容可能比较杂乱,实在抱歉。 以前端为主,后台部分实在没有任何经验。
目前所完成的项目目录:
photoitem //前后端项目都放在这个目录中
|--photoNodeServer //后端项目
|--|--dealFile
|--|--|--file.js //处理硬盘上文件(暂时只有删除)
|--|--mysql
|--|--|--sqlData.js //封装了mysql的语句处理转为promise输出
|--|--node_modules
|--|--route //koa的路由
|--|--|--allRouter.js //总路由,返回这个给主程序
|--|--|--image.js //处理图片(不需要身份认证)
|--|--|--login.js //用户注册与登录
|--|--|--user.js //需要身份认证的对图片操作路由
|--|--token
|--|--|--secret.json //token密钥(应该使用随机数列,我这里先用我自己的名字了)
|--|--|--token.js //使用koa-jwt的方式加密或者解密token
|--|--updates //会自动创建的上传图片文件夹
|--|--.eslintrc.json //语法检测规则
|--|--.gitignore
|--|--config.json //配置项,目前仅有数据库信息配置(需要自己创建)
|--|--index.js //程序入口
|--|--package-lock.json
|--|--package.json
|--|--REAMED.md
|--|--table.sql //建立数据库表
|--photovue //前端项目
|--|--node_modules
|--|--public //自动生成
|--|--src
|--|--|--assets //静态资源
|--|--|--|--css
|--|--|--|--|--common.scss
|--|--|--|--image
|--|--|--|--|--1.jpg
|--|--|--|--imgload //图片处理时使用
|--|--|--|--|--err.png
|--|--|--|--|--loading.gif
|--|--|--|--js
|--|--|--|--|--common.js
|--|--|--|--logo.png
|--|--|--components //零碎组件都放在这里
|--|--|--|--common //公共组件
|--|--|--|--|--Download.vue //下载功能
|--|--|--|--|--ImgList.vue //图片墙功能
|--|--|--|--index //首页相关组件
|--|--|--|--|--Carousel.vue //走马灯组件
|--|--|--|--|--Header.vue //首页第一行内容
|--|--|--|--|--Logo.vue //首页LOGO
|--|--|--|--|--Search.vue //搜索功能
|--|--|--|--|--ToLogin.vue //登录按钮
|--|--|--|--|--Use.vue //登录成功后个人信息
|--|--|--|--upload //上传页面组件
|--|--|--|--|--ImgBotton.vue //图片相关按钮
|--|--|--|--|--Statistics.vue //个人信息板块
|--|--|--|--|--ToUpload.vue //上传界面
|--|--|--|--|--Tree.vue //图片类型
|--|--|--views //由组件拼装后的页面
|--|--|--|--Index.vue //首页
|--|--|--|--Login.vue //登录页
|--|--|--|--SignUp //注册页
|--|--|--|--Upload //上传页
|--|--|--App.vue //入口页
|--|--|--main.js //vue程序入口
|--|--|--router.js //vue-router配置页
|--|--|--store.js //vuex数据管理
|--|--.gitignore
|--|--babel.config.js
|--|--package-lock.json
|--|--package.json
|--|--README.md
项目搭建
前端vue-cli
- 使用vue-cli(3)搭建项目,我使用vue ui的方式(引入vue-router和vuex时,自动修改整个项目)
vue ui
然后在图形化界面中,安装以下内容 2. 初始安装(我是用的版本):
- "axios": "^0.19.0",
- "element-ui": "^2.11.1",
- "node-sass": "^4.12.0",
- "sass-loader": "^7.1.0",
- "vue-router": "^3.0.3",
- "vuex": "^3.0.1"
- 在main.js中加入引用(如图,注意axios部分)
import axios from 'axios'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
// 特别注意这里,axios绑定在Vue的属性中
axios.defaults.baseURL = 'http://localhost:3000';
Vue.prototype.$http = axios;

|-public |--|--favicon.ico |--|--index.html |--src |--|--App.vue |--|--HelloWorld.vue |--|--logo.png |--|--main.js // 这个就是整个程序的入口,一开始的全局引入基本都在这里配置 |--.gitignore |--babel.config.js |--package-lock.json |--package.json |--README.md
后端koa搭建
- 在放置前端项目的外层文件夹中,创建后台项目(code-workspace是vscode的工作区,不用管这个)

- 进入photoNodeServer文件夹,输入
npm init -y
- 等创建出package.json后,初始化本地仓库
git init
- 安装可能需要使用的npm包
- "kcors": "^2.2.2", //跨域使用(切换为@koa/cors)
- "koa": "^2.7.0",
- "koa-bodyparser": "^4.2.1", //解析body,但实际操作时,感觉基本没用到这个
- "koa-handle-error": "0.0.5", //处理错误,应该需要打日志记录,邮件发送什么的,因为不注重后端,也没管这个
- "koa-logger": "^3.2.1", //路由的日志
- "koa2-router": "^1.1.2", //koa2的路由处理,在npm上找到一个更通用的,以后看情况换吧
- "mysql": "^2.17.1" //连接mysql用
- 根目录下创建index.js入口文件
const Koa = require('koa')
const handleError = require("koa-handle-error")
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const cors = require('@koa/cors')
const app = new Koa();
// 错误处理
const onError = err => {
console.error(err)
}
app.use(logger())
.use(handleError(onError))
.use(bodyParser())
.use(cors())
//启动后,可以看到控制台输出console.log。我是用vscode启动的
.listen(3000, '0.0.0.0', () => {
console.log('成功启动服务')
})
目录形式 |--.gitignore |--index.js |--package-lock.json |--package.json
至此项目前后端搭建完成
开始第一步
前端(基本都是src目录中的事情了)
- 先删去vue的默认初始页
我们先制作主页和登录页
- 首先是路由修改router.js
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/Index.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: '/',
component: Index
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "login" */ './views/Login.vue')
}
]
})

- 修改App.vue的路由,整页都变为一个路由展示
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<style>
body{
margin: 0px;
padding: 0px;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
}
</style>

首先我们设想首页,有logo,有搜索框,有登录按钮,这些并为一行。第二行增加一个走马灯。第三行展示所有人自己上传的图片。
在vue中,一切以组件为基础(我的理解是增加复用性),所以,我这边会在首页提取logo,search,login,走马灯,图片列表等。
- 先提取首页头部一整行Header.vue
<template>
<el-row>
<el-col :span="6">
<slot name="logo"></slot>
</el-col>
<el-col :span="12">
<slot></slot>
</el-col>
<el-col :span="6">
<slot name="login"></slot>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
.el-row {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 119, 255, 0.658);
}
</style>
el-开头的都为element ui组件,slot为vue的插槽。这里有三个插槽,分别为之后的logo,search,login预留的位置,这三块其他页面可能会使用到,先将这些抽出Logo.vue
<template>
<div id="logo">
<i class="el-icon-platform-eleme"></i>
</div>
</template>
<style>
#logo{
font-size: 3rem;
}
</style>
search功能未完成,这里应该在改变或者点击搜索按钮的时候,将input内容抛出。同时可以加入节流功能(等有空也要学一下节流) Search.vue
<template>
<div id="search">
<el-input placeholder="请输入内容" v-model="input" class="input-with-select">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</div>
</template>
<script>
export default {
data() {
return {
input: ""
};
}
};
</script>
<style lang="scss">
#search input.el-input__inner {
background-color: rgba(255, 0, 0, 0.103);
border: none;
}
#search .el-input-group__append {
border: none;
}
#search .el-button {
background-color: rgba(0, 255, 34, 0.507);
line-height: normal;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>
这里先用了sessionStorage存储用户信息,再后面会改为jwt的方式。 ToLogin.vue
<template>
<el-button-group>
<el-button type="text">
<!-- 如果已经登录,那么直接显示用户名 -->
<span v-if="isLogin">用户123</span>
<router-link v-else to="/login">登录</router-link>
</el-button>
<!-- <el-button type="primary">注册</el-button> -->
</el-button-group>
</template>
<script>
export default {
data() {
return {
isLogin: false
};
},
mounted() {
this.isLogin = this.checkLogin();
},
methods: {
//判断用户是否已经登录
checkLogin() {
let userInfo = JSON.parse(window.sessionStorage.getItem("userInfo"));
return userInfo ? userInfo.sessionId : false;
}
}
};
</script>
<style lang="scss" scoped>
.el-button {
color: white;
text-decoration: underline;
}
</style>
- 将上面四个都写入views中的Index.vue进行整合
<template>
<div class="index">
<!-- 这里是主页的路由显示 -->
<el-container>
<el-header>
<!-- 头部内容 -->
<MyHeader>
<!-- LOGO -->
<template #logo>
<Logo></Logo>
</template>
<!-- 搜索功能 -->
<Search></Search>
<!-- 登录按钮 -->
<template #login>
<ToLogin></ToLogin>
</template>
</MyHeader>
</el-header>
<el-main>
<!-- 这里应该会再增加一层body的,暂时先这样 -->
<el-row>
<el-col :span="24">
<!-- 走马灯 -->
<Carousel></Carousel>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<!-- 尝试使用懒加载,但失败了,明天再试一下 -->
<div class="demo-image__lazy" style="height:300px">
<!-- <el-image v-for="url in urls" :key="url" :src="url" lazy></el-image> -->
</div>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
</template>
<script>
// @ is an alias to /src
import MyHeader from "@/components/index/Header.vue";
import Logo from "@/components/index/Logo.vue";
import Search from "@/components/index/Search.vue";
import ToLogin from "@/components/index/ToLogin.vue";
import Carousel from "@/components/index/Carousel.vue";
export default {
name: "index",
components: {
MyHeader,
Logo,
Search,
ToLogin,
Carousel
},
// data() {
// return {
// urls: [
// "https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg",
// "https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg",
// "https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg",
// "https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg",
// "https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg",
// "https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg",
// "https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg"
// ]
// };
// }
};
</script>
<style lang="scss" scoped>
.el-header,
.el-main {
padding: 0px;
}
</style>
上面除了头部,还放了走马灯,和图片墙。其中走马灯,将于后端一起(非数据库,只是读取前端的文件,这个做法不可取,从数据库获取数据部分在图片墙中可以体现) 上面注释说懒加载失败,其实是成功的,后面的修改就是使用了element中的懒加载,第二篇会讲到v-lazyload换了个方式,但这个还是有一定问题
vue2.6开始,slot可以使用#代替。类似:和@。但是element ui(2.11) 还是使用slot和slot-scope。所以会存在两种写法,我自己都是使用#
- 开始写入走马灯,我这里直接封装了element-ui的Carousel.vue
<template>
<div class="block">
<!-- 走马灯,拎出来的效果 -->
<el-carousel ref="carousel">
<el-carousel-item v-for="(item,index) in imgList" :key="index">
<el-image style="width: 100%; height: 100%" :src="item" fit="cover"></el-image>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script>
export default {
data() {
return {
imgList: []
};
},
mounted() {
this.getImgList();
},
methods: {
// 通过后台,拿到图片列表
getImgList() {
this.$http.get("/image/carousel").then(res => {
let result = res.data;
// 直接使用地址不成功
result = result.map(v => require(`@/assets/image/${v}`));
this.imgList = result;
// this.$nextTick(this.computedImgHeight)
});
},
// 计算走马灯的高度(图片都是16:9的)
computedImgHeight() {
let carouselDom = this.$refs.carousel;
let width = carouselDom.$el.clientWidth;
let height = (width / 16) * 9;
if (height > 500) {
height = 500;
}
carouselDom.$el.style.height = height + "px";
for (let i in carouselDom.$children) {
carouselDom.$children[i].$el.style.height = height + "px";
}
}
}
};
</script>
this.$http就是上面axios绑定在vue中的方法,与axios使用方式一致。$nextTick因为vue的处理都是异步的,使用$nextTick可以等数据在页面中显示后,在继续进行下一步操作
下面是index对应的koa后台
- 首先,在写的时候官网上发现,kcors已经被替换为@koa/cors(官网意思好像是过时了)
- 增加allRouter.js,image.js(最上面的目录有写,之后不再说了) allRouter.js
// 所有路由管理
const Router = require('koa2-router');
const router = new Router();
const Login = require('./login')
const Image = require('./image')
//Login是登录使用的,下面有写
router.use('/',Login)
router.use('/image',Image)
module.exports = router;
3.这个图片目前是读取photoVue中的文件,这个不可取(再次声明),按照之后的图片墙的方式修改 image.js
// 获取图片相关的路由
const Router = require('koa2-router');
const router = new Router();
const fs = require('fs')
// 走马灯图片路由
router.get('/carousel', async ctx => {
let filePath = 'F:/learn/photoItem/photovue/src/assets/image';
let fileList = await getImgPath(filePath)
ctx.body = fileList
})
module.exports = router;
// 获取图片地址(这个以后从数据库获取)
const getImgPath = (filePath)=>{
return new Promise((resolve,reject)=>{
fs.readdir(filePath, async (err, files) => {
if (err) reject(new Error('读取文件失败' + err))
resolve(files)
})
})
}
在index.js的入口文件中,添加两行
const route = require('./route/allRouter')
app
.use(route)
以下是完整代码
const Koa = require('koa')
const handleError = require("koa-handle-error")
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const cors = require('@koa/cors')
const app = new Koa();
const route = require('./route/allRouter')
const onError = err => {
console.error(err)
}
app.use(logger())
.use(handleError(onError))
.use(bodyParser())
.use(cors())
.use(route)
.listen(3000, '0.0.0.0', () => {
console.log('成功启动服务')
})
以上整个首页完成,之后是login.vue的搭建
- el-form包含了验证,v-model双向绑定等功能,之后直接使用axios进行交互即可 login.vue
<template>
<!-- 登录界面 -->
<article>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>用户登录</span>
</div>
<el-form :model="form" status-icon :rules="rules" ref="form" label-width="50px">
<el-form-item label="账号" prop="account">
<el-input v-model="form.account"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.pass" :show-password="true" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('form')">提交</el-button>
<el-button @click="resetForm('form')">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</article>
</template>
<script>
export default {
data() {
// 验证条件判断
var checkAccount = (rule, value, callback) => {
if (!value) {
return callback(new Error("账号不能为空"));
} else {
callback();
}
};
var validatePass = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else {
if (this.ruleForm.checkPass !== "") {
this.$refs.ruleForm.validateField("checkPass");
}
callback();
}
};
return {
form: {
pass: "",
account: ""
},
rules: {
pass: [{ validator: validatePass, trigger: "blur" }],
account: [{ validator: checkAccount, trigger: "blur" }]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
// 数据库中验证,暂时不用数据库
this.$http
.post("/login", this.form)
.then(res => {
const result = res.data;
if (result.sessionId) {
// 放在session中
window.sessionStorage.setItem('userInfo',JSON.stringify(result))
this.$router.push('/')
}else{
this.$message.error("账号或者密码错误")
}
})
.catch(err => {
throw new Error("验证用户登录信息出错,错误原因:" + err);
});
} else {
// console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
};
</script>
<style lang="scss" scoped>
article {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.el-input {
width: 300px;
}
</style>
- koa后端写法 login.js
// 登录等用户信息路由
const Router = require('koa2-router');
const router = new Router();
// 检测登录信息
router.post('/login', async ctx => {
return ctx.body = {
sessionId : true
}
})
module.exports = router;
登录页完成
koa到vue之后基本那都是这么做的。之后会将整个项目一一展现。
如有不足之处,请麻烦指出,谢谢!