VUE project based on photo album uploading

Project document structure planning idea

  • A good project must be planning the project file structure, the project structure is clear, the front and back end communication is more convenient, and the efficiency of project development is improved.
  • In the project of realizing album upload, we carried out the following structure planning for the back-end file:

  • With such file planning for the back-end project, look at server.js(Main entry file) code:
const Koa = require("koa"); // Const serve = require("koa-static"); Const koaBody = require("koa-body"); const koaBody = require("koa-body"); // require("koa-router") const Router = require("koa-router"); Exports =async CTX =>{//todo.. } // const upload = require("./lib/upload"); Exports =async CTX =>{//todo.. Const login = require("./lib/login"); const login = require("./lib/login"); Const getPhotos = require("./lib/getPhotos"); SELECT * FROM photos WHERE uId=? AND id=? const getPhoto = require("./lib/getPhoto"); // require("./lib/db") const db = require("./lib/db"); Const koaJwt = require("koa-jwt"); Const {SECRET} = require("./lib/config"); // Initialize the database db.initdb (); const app = new Koa(); App.use (koaBody({multipart: true})); Use (serve(__dirname + "/static")); Middleware app.use(koaJwt({secret: secret}).unless({path: [/^\/login/]})); const router = new Router(); Router.post ("/upload", upload); router.post("/upload", upload); router.post("/login", login); router.get("/getPhotos", getPhotos); router.get("/getPhoto", getPhoto); app.use(router.routes()); // The port of the back-end server app.listen(8081);Copy the code
  • We can find the main entry file (server.js) code intent is very concise, thanks to our project file structure planning ideas.

The idea of splitting tasks

  • The completion of a project must be accompanied by the idea of task splitting. Doing a project is like putting together a puzzle. As long as you find each small piece of the puzzle, you can complete the whole puzzle. With this in mind, we should be in a state of taking small steps in project implementation.
  • In the project of realizing album uploading, we used vUE framework, and we need to complete the login function as shown below:

  • After getting the functional requirements, we should not immediately start writing the business code. It is better to draw a simple logical code flow chart with the idea of task splitting, and then write the business code. Here I write the flow chart as follows:

Function 1: Authentication when a user enters an account password

Summary: Solving cross-domain problems encountered in the request process, encapsulating a myAxios request function based on AXIOS, using AXIos request interceptor, data loss caused by reassignment of data in store after page refresh when using Vuex,

  • Lesson 1: Solve cross-domain problems encountered in the request process!

  • In the project to implement photo album uploads we used the AXIos third-party library to send requests to the back end such as:axios.post('localhost:8081/login',{username,password})
  • We are inlocalhost:8080/loginbackendlocalhost:8081/loginA POST request was sent, which was rejected by a ruthless cross-domain problem thousands of miles away.
  • We are inScaffolding official websiteSolutions can be found invue.config.jsConfiguration in filedevServer.proxy, the configuration of the project is as follows:
module.exports = { devServer: { proxy: { "/api": { target: "http://localhost:8081", pathRewrite: { "^/api": ""}}}}};Copy the code
  • After the configurationdevServer.proxyThen our request should be modified accordingly to:axios.post('localhost:8081/api/login',{username,password})At this point, we have successfully solved the cross-domain problem.

  • Lesson 2: Encapsulate a myAxios request function based on AXIos
  • We will find that each subsequent request will need to be added/apiThese characters, the code is not very elegant. We want to wrap a myAxios request function that automatically adds a URL prefix to each request/apiWe just need to focus on where to send the request.
  • inAxios websiteCan be found in the Request Config configuration optionsbaseURLTo solve the problem we mentioned above, you can refer to the following code:
// The following code is written in the http.js file. /http"; import axios from "axios"; const myAxios = axios.create({ baseURL: "/api" }); export default myAxios;Copy the code
  • After wrapping the myAxios request function and exporting it, import it in the required fileHTTP js fileYou can use this function to send the request. The code is as follows:
import http from ".. /http"; Http. post("/login", {username, password})Copy the code
  • We need to build on the code above and export a back end that can pass in user account and password parameters/loginSend request function, code reference as follows:
// Export function apiLogin({username, // call async/await result // call async/await result // call async/await result // call async/await result http .post("/login", { username, password }) ); }Copy the code
  • Note: Restart the server every time you modify configuration files such as vue.config.js.

  • Lesson 3: Data loss caused by reassignment in store after page refresh when using Vuex
  • Let’s reproduce the scene of this problem step by step. The scene flow chart is as follows:
  • Scenario Flowchart 1:

  • Scenario Flow Chart 2:

  • In the project to implement photo album uploads, we implemented the login page component (views/Login.vue) for click event monitoring, when the login button is clickedloginMethod, in theloginIn the method wedispatchtostoreIn theactionsIn theloginMethod and carries the account password data information filled in by the user. The code reference is as follows:
methods: {
 login() {
     this.$store
      .dispatch("login", {
      username: this.username,
      password: this.password
      })
 }
}
Copy the code
  • instoreIn theactionsIn theloginMethod is called based on received data informationapiLoginMethod back end/loginSend the request and thencommittostoreIn themutationsIn theloginMethod with token data information returned by the back end (User login failed The token has no value). The code reference is as follows:
import Vue from "vue"; import Vuex from "vuex"; import { apiLogin } from ".. /api"; Vue.use(Vuex); export default new Vuex.Store({ state: { token: "", }, mutations: { }, actions: { async login({ commit }, payload) { const { username, password } = payload; const res = await apiLogin({ username, password }); const token = res.data.data.token; commit("login", { token }); }}});Copy the code
  • instoreIn themutationsIn theloginThe method is stored in the token data receivedstateToken in. The code reference is as follows:
import Vue from "vue"; import Vuex from "vuex"; import { apiLogin } from ".. /api"; Vue.use(Vuex); export default new Vuex.Store({ state: { token: "", }, mutations: { login(state, payload) { state.token = payload.token; // console.log(state.token,'token'); }, }, actions: { async login({ commit }, payload) { const { username, password } = payload; const res = await apiLogin({ username, password }); const token = res.data.data.token; commit("login", { token }); }}});Copy the code
  • After that we need to redirect the page tohttp://localhost:8080/photoPage, code reference is as follows:
Methods: {Login () {this.$store.dispatch (" Login ", {username: this.username, password: This.password}).then(() => {// jump to the photo page console.log(' I want to jump to '); this.$router.push({ name: "Photo" }); }); }}Copy the code
  • Jump tohttp://localhost:8080/photoThe preparation process is as follows:

  • We are inPhoto.vueWhen requirement 1 is implemented inThat is, each request is sent with token data informationWhen the page is refreshed, the vue instance will be reloaded. Therefore, the data in the Store will be reassigned. Therefore, the data will be lost, which is represented as toKE data loss.
  • If we cannot send a request with token data information, even though the back-end generates tokens to the logged in users and sends them to the front-end, the front-end loses token data during the request, so the logged in users cannot access the page and obtain relevant data. The code reference is as follows:

/ / use axios request interceptor / / in token of the presence of data add authentication header information myAxios. Interceptors. Request. Use (config = > {const token = store. State. The token. if (token) { config.headers.authorization = "Bearer " + token; } return config; });Copy the code
  • Solution: Using localStorage to persist token data, the flow chart and reference code are as follows:

import Vue from "vue"; import Vuex from "vuex"; import { apiLogin } from ".. /api"; Vue.use(Vuex); export default new Vuex.Store({ state: { token: localStorage.getItem("token") || "", }, mutations: { login(state, payload) { state.token = payload.token; localStorage.setItem("token", state.token); } }, actions: { async login({ commit }, payload) { const { username, password } = payload; const res = await apiLogin({ username, password }); const token = res.data.data.token; commit("login", { token }); }}});Copy the code

Functional implementation of the Photo component page

General knowledge :Vuex global state management, list rendering, AXIos request processing (response, request intercession), component communication, task splitting idea, component splitting idea, FileList, Filereader.readasDataURL (), FormData, calculation properties, Refactoring code, asynchronous request, full Office front route guard, lazy route loading, programmatic navigation, declarative navigation, decouple components and routes using props

  • Features a: Sent to the backend after successful login'/getPhotos'Request, render the Photo initialization page based on the requested data, as shown in the GIF:
  • Lesson one: Learn the idea of task splitting, Vuex global state management, list rendering, AXIos request processing

  • Once again, based on the idea of task splitting, we draw a simple flow chart to realize function 1:

  • The core code for feature 1 implementation in Photo.vue is referenced below:
Export default {// At what point does the component request the /getPhotos interface on the back end? async created() { this.updatePhotos(); }, methods: { async updatePhotos() { this.$store.dispatch("updatePhotos"); }}};Copy the code
  • store/index.js(Vuex status management file) for the implementation of function one core code reference is as follows:
import Vue from "vue"; import Vuex from "vuex"; Import {apiGetPhotos} from "... "import {apiGetPhotos} from"... /api"; Export function apiGetPhotos() {myAxios return http.get("/getPhotos"); myAxios return http.get("/getPhotos"); } */ Vue.use(Vuex); export default new Vuex.Store({ state: { photos: [] }, mutations: { updatePhotos(state, payload) { state.photos = payload.photos; } }, actions: { async updatePhotos({ commit }) { const res = await apiGetPhotos(); commit("updatePhotos", { photos: res.data.data.photos }); }}});Copy the code
  • We have successfully assigned photos data as shared state to Vuex to manage. Now we can initialize and render the view of the Photo page component based on photos data. The core implementation code is as follows:
<template> <! <template v-for="photo in photos"> <div class="photoItem" :key="photo.id"> <img: SRC =" photo-.imgurl "/> <span> {{ photo.name }} </span> </div> </template> </template> export default { async created() { this.updatePhotos(); }, computed: { photos() { return this.$store.state.photos; } }, methods: { async updatePhotos() { this.$store.dispatch("updatePhotos"); }}};Copy the code

  • Function 2: The upload photo pop-up box is displayed after clicking the upload button. The effect picture is as follows:
  • Lesson 2: Subdivide components according to component ideas and functional requirements, use sync to solve parent-child component communication (for VUE2.x)

  • In the realization of the Photo page component, we can use the idea of component to subdivide the whole page into various component pages. We can subdivide the component based on the functional requirements as the entry point, refer to the basic idea of Vue official website component:

  • We split the popbox page as a page component (The UploadPhotoView.vue page component) before we clickUpload photosButton displaybouncedPage, a simple flow chart to achieve the requirement is as follows:

  • Functions in twoThe parent component Photo. VueThe implementation code of the core is as follows:
// Here is the core implementation code in the parent component photo.vue: <button class=" mybTN "@click="showUploadPhotoView = true"> <UploadPhotoView :visible.sync="showUploadPhotoView" ></UploadPhotoView> </template> <script> import UploadPhotoView from ".. /components/UploadPhotoView"; export default { components: { UploadPhotoView }, data() { return { showUploadPhotoView: false }; }}; </script>Copy the code
  • Functions in twoSubcomponents UploadPhotoView. VueThe implementation code of the core is as follows:
Uploadphotoview.vue () uploadPhotoview.vue () <template> <div class="masking" V-if ="visible"> <span class="close" @click="close">╳</span> <! </div> </template> <script> export default {props: ["visible"], close() { this.$emit("update:visible", false); }}; </script>Copy the code

  • Function three: implemented in sub-componentsUploadPhotoView.vueClick on theTo upload picturesThe uploaded preview image is displayed (Hide the background box of uploaded image while displaying preview image (mutually exclusive)), the effect picture is as follows:
  • Lesson 3: FileList(Please refer to the official website for details), FileReader. ReadAsDataURL (),Please refer to the official website for details), calculate the use of attributes

  • Let’s first solve the mutually exclusive problem of displaying the uploaded picture background box and the preview picture to be uploaded. The core implementation code is as follows:

<template> <! <div class="showContainer" v-show="showAddContainer"> <span> <span> <input class="imgFile" type="file" name="" multiple="multiple" @input="addWantShowPhotos" /> <! </div> <! <div class="loadContainer" v-show="showWaitUploadContainer"> <! --> <template v-for="(item, index) in wantUploadPhotos"> <UploadPhotoItem :item="item" :key="index"></UploadPhotoItem> </template> <! --> </div> </template> <script> export default {data() {return {wantUploadPhotos: []}; }, computed: { showAddContainer() { return this.wantUploadPhotos.length === 0; }, showWaitUploadContainer() { return this.wantUploadPhotos.length > 0; } }, methods: {addWantShowPhotos(e) {// A FileList object usually comes from the files property of an HTML <input> element this.wantUploadPhotos.push(... Array.from(e.target.files)); }}}; </script>Copy the code
  • Let’s solve the problem of how to click upload picture and select the relevant picture. After opening the page, the corresponding effect of uploaded picture preview will be displayed (Remember that we have not uploaded the image to the back end at this point), the core implementation code is as follows:
  • Note: The filereader.readasDataURL () method reads the specified Blob or File object (Here we've got the File object via e.target.files), when the read operation is complete, readyState will become DONE and the Loadend event will be triggered, and the Result property will contain onedata:URLFormat string (base64 encoded) to represent the contents of the file being read, using this assignment toimg.srcJust solve the effect of showing the image preview.
Uploadphotoitem. vue <template> <div class=" UploadPhotoItem "> <img :src="imgSrc" /> <span class="pictureName"> {{ item.name }} </span> </div> </template> <script> export default { ImgSrc: ["item"], data() {return {imgSrc: ""}; // Props: ["item"], data() {return {imgSrc: ""}; }, mounted() { const fileReader = new FileReader(); // Async filereader.onload = () => {this.imgsrc = filereader.result; }; fileReader.readAsDataURL(this.item); }}; </script>Copy the code

  • Function four: implemented in sub-componentsUploadPhotoView.vueClick on theTo uploadAfter the button, upload the selected image to the back-end server directory. After the upload, the front-end will pull the back-end image data rendering page again, and the effect picture is as follows:
  • Lesson 4: Learn FormDataPlease refer to the official website for details, father-son communication (Please refer to the official website for details), refactoring code (Express the intent of the code and don't write duplicate code), the use of asynchronous requests

  • Let’s tackle the sub-component firstUploadPhotoView.vueClick on theTo uploadUpload the selected picture to the backend server directory after the button. The core implementation code is as follows:
<template> <span class="uploadBtn" @click="uploadFile" > Start upload </span> </template> <script> // Request interface for uploading image files to the backend server import  { apiUpload } from ".. /api"; /* export function apiUpload(file) { const formData = new FormData(); Formdata.append ("img", file); // Const {img} = ctx.request.files formdata.append ("img", file); Then (res=>res) return http.post("/upload", formData) } */ export default { data() { return { wantUploadPhotos: [] }; }, computed: { showAddContainer() { return this.wantUploadPhotos.length === 0; }, showWaitUploadContainer() { return this.wantUploadPhotos.length > 0; } }, methods: {{async uploadFile () for (const item of enclosing wantUploadPhotos) {/ / upload is asynchronous / / have to have the result to continue operating await next apiUpload (item); } / / code refactoring / / here is divided into three steps intention makes the code more apparent enclosing uploadPhotosCompleted (); }, uploadPhotosCompleted() { this.reset(); }, reset() {this.wantUploadPhotos = []; }, AddWantShowPhotos (e) {// A FileList object usually comes from the files property of an HTML <input> element this.wantUploadPhotos.push(... Array.from(e.target.files)); }}}; </script>Copy the code
  • Let’s solve the problem of the front end pulling the back end image data rendering page again after the upload is successfulUploadPhotoView.vueThe request to pull the backend image data to render the page is already wrapped in the parent componentPhoto.vueWe can use parent-child communication to notify the parent component to trigger the request to pull data rendering page again after the child component successfully uploads the picture to the back end. The core implementation code is as follows:
Export default {data() {return {wantUploadPhotos: []}; export default {data() {return {wantUploadPhotos: []}; }, computed: { showAddContainer() { return this.wantUploadPhotos.length === 0; }, showWaitUploadContainer() { return this.wantUploadPhotos.length > 0; }}, methods: {async uploadFile() {for (const item of this.wantuploadphotos) {await apiUpload(item); } this.uploadPhotosCompleted(); $emit(" uploadPhotosCompleted "); $emit(" uploadPhotosCompleted "); this.reset(); }, reset() { this.wantUploadPhotos = []; }, close() { this.$emit("update:visible", false); }, addWantShowPhotos(e) { this.wantUploadPhotos.push(... Array.from(e.target.files)); }}}; < / script > -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / here is a Photo. In the vue core implementation code: <template> <UploadPhotoView :visible.sync="showUploadPhotoView" @upload-completed="handleUploadCompleted" ></UploadPhotoView> </template> <script> import UploadPhotoView from ".. /components/UploadPhotoView"; Export default {components: {UploadPhotoView}, async created() {// Initialize render page sent request await this.updatephotos (); }, methods: {async handleUploadCompleted() {// The child tells the parent to pull data from the back end again this.updatephotos (); }, async updatePhotos() {this.$store. Dispatch ("updatePhotos"); }}}; </script>Copy the code

  • Function 5: Set the requirement for refreshing the token in LocalStorage after it expires and jumping to the login page (An example is to manually delete the token), the effect picture is as follows:
  • Tip 5: Learn how to use the global front-guard and AXIOS response blocker.
  • Note: The global pre-route guard executes before the AXIOS response interceptor

Import axios from "axios"; import axios from "axios"; import axios from "axios"; import store from "./store"; import router from "./router"; const myAxios = axios.create({ baseURL: "/api" }); / / response to intercept myAxios interceptors. Response. Use (res = > res, err = > {/ / 401 403... Switch (err.response.status) {// Return failed status code can be specified with the back end // Return status code if there is no request page with token case 401: // alert(" please go to login page "); router.replace({ name: "Login" }); break; }}); export default myAxios; -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / here is using the global front routing guard on each page of a route to guard / / needs in validation and no access token of routing when the page is directed to the login page // Prevent the user from typing photo directly into the URL bar to waste an AXIos response interceptor import Vue from "Vue "; import VueRouter from "vue-router"; import Login from ".. /views/Login.vue"; import Photo from ".. /views/Photo.vue"; import store from ".. /store"; Vue.use(VueRouter); const routes = [ { path: "/login", name: "Login", component: Login, meta: { isAuth: false } }, { path: "/photo", name: "Photo", component: Photo, meta: { isAuth: true } } ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes }); router.beforeEach((to, from, Next) => {// login -> no need to check // photo -> need to check if (to.meta.isauth) {// Need to check if there is no token if (store.state.token) {next(); } else { next({ name: "Login" }); } } else { next(); }}); export default router;Copy the code

  • Function 6: The user will jump to the picture details page when clicking the picture and click in the details pagebackButton back/photoPage, the renderings are as follows:
  • Lesson 6: Learn how to configure lazy routing (Please refer to the official website for details) and use of programmatic and declarative navigation (Please refer to the official website for details) and decoupled components and routes using props (Please refer to the official website for details)

Vue.use(VueRouter); Const routes = [// dynamic loading // optimization {path: "/detail/:id", name: "detail ", props: true, // 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: "detail" */ "../views/Detail.vue") } ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes }); export default router; -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / here are Phtot. The core of the vue implementation code < template > < the router - link: key = "photo. Id" : to = {" " name: 'Detail', params: { id: . Photo id}} "tag =" div "> < / template > -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / below is the Detail. The core of the vue implementation code < template > <div> <div> <img :src="photoInfo.imgUrl" alt="" /> <p> {{ photoInfo.name }} </p> </div> <div> <button @click="back">back</button> </div> </div> </template> <script> export default { props: ["id"], methods: { back() { this.$router.back(); } }, computed: {photoInfo() {// Without a value, Return this.$store.state.photos. Find (item => item.id === this.id); return this.$store.state.photos. }}}; </script>Copy the code