使用vue+node搭建图片上传下载的web端简单服务(一)

1,541 阅读3分钟

前言

  • 通过搭建一个简单项目,练习vue全家桶,elementui,node,koa,axios等。
  • 项目为上传下载图片,可公共展示,个人注册、登录、设置图片分类等功能
  • 本项目按照本人搭建与写入顺序进行,文件会发生多次拆分。所有内容都抱起来过。
  • 第一次写文章,内容可能比较杂乱,实在抱歉。 以前端为主,后台部分实在没有任何经验。

前端:github.com/bingfeng1/p… 后台:github.com/bingfeng1/p…

目前所完成的项目目录:

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

  1. 使用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"
  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搭建

  1. 在放置前端项目的外层文件夹中,创建后台项目(code-workspace是vscode的工作区,不用管这个)
  2. 进入photoNodeServer文件夹,输入
npm init -y
  1. 等创建出package.json后,初始化本地仓库
git init
  1. 安装可能需要使用的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用
  1. 根目录下创建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,走马灯,图片列表等。

  1. 先提取首页头部一整行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>
  1. 将上面四个都写入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。所以会存在两种写法,我自己都是使用#

  1. 开始写入走马灯,我这里直接封装了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后台

  1. 首先,在写的时候官网上发现,kcors已经被替换为@koa/cors(官网意思好像是过时了)
  2. 增加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的搭建

  1. 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>

  1. 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之后基本那都是这么做的。之后会将整个项目一一展现。

如有不足之处,请麻烦指出,谢谢!