preface

Recently, I studied the course of Vue familybarrel +SSR+Koa2 full stack Development of Meituan.com. My main lecture was aimed at Vue SSR+Koa2 full stack technology, and finally realized meituan.com project.

  • Front-end: nuxt.js /vue-router/ Vuex/ element-UI
  • The backend: Node. Js/Koa2 / Koa – the router/Nodemailer/Passport
  • HTTP communication: Axios
  • Data support: Mongoose/Redis/ Amap Web service API interface

Source link: github.com/zhanglichun…

Meituan homepage

City positioning

Lbs.amap.com/)

  1. On autonavi’s official website, apply for the “Web service API” Key and get the API interface
  2. Vuex global state management is used to store city in State, because meituan’s entire service (such as take-out) is carried out around the user’s city, so that all components can obtain city data.
const state = () => ({
  position: {},
})

const mutations = {
  setPosition(state, position) {
    state.position = position
  },
  setCity(state, city) {
    state.position.city = city
  },
  setProvince(state, province) {
    state.position.province
  },
}

const actions = {
  setPosition: ({commit}, position) => {
    commit('setPosition', position)
  },
  setCity: ({commit}, city) => {
    commit('setPosition', city)
  },
  setProvince: ({commit}, province) => {
    commit('setPosition', province)
  },
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}
Copy the code

2. Because the data stored in the Store is stored in the running memory, when the page is refreshed, the data stored in the Vuex instance Store will be lost (i.e. the page will reload the Vue instance and the data in the Store will be reassigned).

The Fetch hook provided by Nuxt and nuxtServerInit (both running on the server side) come into play and help us quickly manipulate the Store before the page renders (component loads)

So that no matter how to jump to the page, state’s city data will not be lost

Reference article: NuxT-NuxTServerInit & Store handling & Context before page rendering

import Vue from 'vue'
import Vuex from 'vuex'

import geo from './models/geo'

Vue.use(Vuex)

const store = () => 
  new Vuex.Store({
    modules: {
      geo
    },
    actions: {
      async nuxtServerInit({commit}, {req, app}) {
        const {status, data:{province, city}} = await app.$axios.get('https://restapi.amap.com/v3/ip?key=b598c12de310236d9d40d3e28ea94d03')
        commit('geo/setPosition', status === 200 ? {province, city} : {province: ' ', city: ' '}}}})export default store
Copy the code
  • Obtain the current city data for each component
{{$store.state.geo.position.city}}
Copy the code

Search search

  • Gets Autonavi’s search POI interface
  • @INPUT listens for events. When the input focus and the input value change, the input function will be triggered, making an Ajax request to the server to autonavi’s search POI interface, and obtaining the data to render the page through v-IF.
<div class="wrapper">
    <input v-model="search" placeholder="Search for businesses or places" @focus="focus" @blur="blur" @input="input"/>
    <button class="el-icon-search"></button>
</div>
<dl class="searchList" v-if="isSearchList">
    <dd v-for="(item, i) in searchList" :key="i">{{ item.name }}</dd>
</dl>
export default {
  data() {
    return {
      search: ' ',
      isFocus: false,
      searchList: []
    }
  },
  computed: {
    isSearchList() {
      return this.isFocus && this.search
    }
  },
  methods: {
    focus() {
      this.isFocus = true
    },
    blur() {
      this.isFocus = false
    },
    input: _.debounce(async function () {
      const { data: { pois } } = await this.$axios.get(`https://restapi.amap.com/v3/place/text?keywords=${this.search}&city=${this.$store.state.geo.position.city}&offset=7&page=1&key=a776091c1bac68f3e8cda80b8c57627c&extensions=base`)
      this.searchList = pois
    })
  },
}
Copy the code

Product list

  • Gets Autonavi’s search POI interface
  • After mounting the DOM, in the Mounted lifecycle function, request the Autonavi search POI interface, according to the keywords=” gourmet “.
  • When the mouse passes over a DD element of DL, the over function is triggered to obtain the keywords attribute of DD element, according to which the search POI interface of Autonavi is requested.
<template>
  <div class="m-container">
    <div class="scenes-container">
      <dl @mouseover="over">
        <dt class="dt"> </dt> <! -- <dd keywords="Food | film | | spa hotel" kind="all""> </dd> --> <dd keywords="Food"</dd> <dd keywords="SPA"Word-wrap: break-word! Important; "> <dd keywords="Film"Word-wrap: break-word! Important; "> </dd> <dd keywords="Hotel"> Quality travel </dd> </dl> <div class="detial">
        <nuxt-link to="item.url" v-for="(item, i) in list" :key="item.name">
          <img :src='item.photos[0].url' alt="Meituan">
          <ul>
            <li class="title">{{ item.name }}</li>
            <li class="other">{{ item.adname }}&nbsp; &nbsp; &nbsp; {{ item.address }}</li> <li class="price"> < span > selections {{item. Biz_ext. Cost. Length? Item. Biz_ext. The cost:'暂无' }}</span>
            </li>
          </ul>
        </nuxt-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      kind: 'all',
      keywords: ' ',
      list: []
    }
  },
  methods: {
    async over(e) {
      const current = e.target.tagName.toLowerCase()
      if (current === 'dd') {
        this.keywords = e.target.getAttribute('keywords')
        const {status, data: {pois}} = await this.$axios.get(`https://restapi.amap.com/v3/place/text?keywords=${this.keywords}&city=${this.$store.state.geo.position.city}&offset=10&page=1&key=b598c12de310236d9d40d3e28ea94d03&extensions=all`)
        if (status === 200) {
          const r = pois.filter(item => item.photos.length)
          this.list= r.slice(0, 6)
        } else {
          this.list = []
        }
      }
    }
  },
  async mounted() {
    const {status, data: { pois }} = await this.$axios.get (` food & city = https://restapi.amap.com/v3/place/text?keywords=${this.$store.state.geo.position.city}&offset=100&page=1&key=b598c12de310236d9d40d3e28ea94d03&extensions=all`)
    if (status === 200) {
      const r = pois.filter((item) => item.biz_ext.cost.length && item.photos.length)
      this.list = r.slice(0, 6)
    } else {
      this.list = []
    }
  }
}
</script>
Copy the code

registered

/** * ----- Send email to customer to obtain verification code interface ----- */ router.post("/verify", async (ctx) => {
  letusername = ctx.request.body.username; Const saveExpire = await store. hget(' nodemail:${username}`, "expire")
  if (saveExpire && new Date().getTime() - saveExpire < 0) {
    ctx.body = {
      code: -1,
      msg: "Validation requests are too frequent, 1 in 1 minute"
    }
    return false} // Use Nodemail to email the user to obtain the verification codelet transporter = nodeMailer.createTransport({
    host: Email.smtp.host,
    port: 587,
    secure: false,
    auth: {
      user: Email.smtp.user,
      pass: Email.smtp.pass
    }
  })
  let ko = {
    code: Email.smtp.code(),
    expire: Email.smtp.expire(),
    email: ctx.request.body.email,
    user: ctx.request.body.username
  }
  letMailOptions = {from: 'authentication mail <${Email.smtp.user}>`,
    to: ko.email,
    subject: "Meituan Registration Code", HTML: 'You are registered in Meituan, your invitation code is${ko.code}`
  }
  await transporter.sendMail(mailOptions, (err, info) => {
    if (err) {
      return console.log(err);
    } else {
      Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
    }
  })
  ctx.body = {
    code: 0,
    msg: "The captcha has been sent, there may be a delay, valid for 1 minute."}}) /** * ----- register interface ----- */ router.post("/signup", async (ctx) => { const { username, password, email, code } = ctx.request.body; // Check whether the verification code is correct. Const saveCode = await store. hget(' nodemail:${username}`, "code");
  const saveExpire = await Store.hget(`nodemail:${username}`, "expire");
  if (code === saveCode) {
    if (new Date().getTime() - saveExpire > 0) {
      ctx.body = {
        code: -1,
        msg: "Verification code has expired. Please obtain it again."
      }
      return false; }}else {
    ctx.body = {
      code: -1,
      msg2: "Please enter the correct verification code."
    }
    return false} // Query the mongoose database to see if there is a user name. Yes, user name registered, does not exist, write databaselet user = await User.find({
    username
  })
  console.log(user)
  if (user.length) {
    ctx.body = {
      code: -1,
      msg1: "User name has been registered"
    }
    return false
  }
  let nuser = await User.create({
    username,
    password,
    email
  })
  if (nuser) {
    ctx.body = {
      code: 0,
      msg: "Registration successful",}}else {
    ctx.body = {
      code: -1,
      msg: "Registration failed"}}})Copy the code

3. Use the Element-UI form in the front end. When clicking send verification code, it will request the interface that sends email to the customer to obtain the verification code and make relevant logical judgment. When clicking register, the interface will be registered and relevant logical judgment will be made. Once the registration is successful, it will be written into the Mongoose database.

     sendMsg() {
        const self = this
        letNamePass, emailPass // Verify the user name and password on the client form, whether to fill in, whether the format is correct this.$refs['ruleForm'].validateField('username', (valid) => {
          namePass = valid
        })
        if (namePass) {
          return false
        }
        this.$refs['ruleForm'].validateField('email', (valid) => {
          emailPass = valid
        })
        self.statusMsg = ' '
        if(! namePass && ! emailPass) { this.$axios.post('/users/verify', {
            username: encodeURIComponent(self.ruleForm.username),
            email: self.ruleForm.email
          }).then(({ status, data}) => {
            if(status===200 && data && data.code===0) {
              letCount = 60 self.statusMsg = 'Verification code sent, remaining${count--}Second ` self. Timerid =setInterval(() => {self.statusMsg = 'Verification code sent, remaining${count--}Second `if (count === 0) {
                  clearInterval(self.timerid)
                  self.statusMsg = 'Please obtain the captcha code again'}}, 1000); }else {
              self.statusMsg = data.msg
            }
          })
        }
      }
      register() {
        let self = this
        this.$refs["ruleForm"].validate((valid) => {
          if (valid) {
            this.$axios.post("/users/signup", {
              username: window.encodeURIComponent(this.ruleForm.username),
              password: cryptoJs.MD5(this.ruleForm.pwd).toString(),
              email: this.ruleForm.email,
              code: this.ruleForm.code
            })
              .then(({ status, data }) => {
                if (status === 200) {
                  if (data && data.code === 0) {
                    location.href = "/login"
                  } else {
                    self.statusMsg = data.msg
                    self.error1 = data.msg1
                    self.error2 = data.msg2
                  }
                }
                else{self.error = 'Server error, error code:${status}'}})}})}}Copy the code

The login

Koa2 uses passport permission authentication middleware

const passport = require('koa-passport')
const LocalStrategy = require('passport-local')
const UserModel = require('.. /.. /dbs/models/users.js'// Define the local login policy and the serialization and deserialization operations passport. Use (new LocalStrategy(async)function(username, password, done) {
  let where= { username }; // Check whether the user exists in the Mongoose databaselet result = await UserModel.findOne(where)
  if(result ! = null) {if (result.password === password) {
      return done(null, result)
    } else {
      return done(null, false.'Password error')}}else {
    return done(null, false.'User does not exist'}})) // Session serializeUser(function(user, done) {
  done(null, user)}) // deserializeUser(function(user, done) {
  done(null, user)
})

module.exports =  passport
Copy the code

2. Apply passport middleware

app.use(passport.initialize()) 
app.use(passport.session())
Copy the code

3. Set the login interface on the background

/** * ----- ----- */ router.post('/signin', async (ctx, next) => {
  let{username, password} = ctx.request.body // there is no username but a passwordif(! username && password ! = ="d41d8cd98f00b204e9800998ecf8427e") {
    ctx.body = {
      code: -1,
      msg: 'Please enter a user name'
    }
    return false} // A user name exists, but no password existsif (username && password === "d41d8cd98f00b204e9800998ecf8427e") {
    ctx.body = {
      code: -1,
      msg: 'Please enter your password'
    }
    return false} // No user name or password existsif(! username && password ==="d41d8cd98f00b204e9800998ecf8427e") {
    ctx.body = {
      code: -1,
      msg: 'Please enter username and password'
    }
    return false} // Perform local login authenticationreturn Passport.authenticate("local".function (err, user, info, status) {
    if (err) {
      ctx.body = {
        code: -1,
        msg: err
      }
    } else {
      if (user) {
        ctx.body = {
          code: 0,
          msg: "Login successful",
          user
        }
        return ctx.login(user)
      } else {
        ctx.body = {
          code: 1,
          msg: info
        }
      }
    }
  })(ctx, next)
})
Copy the code

4. When using the Element-UI form on the client, the login interface will be requested and relevant logical judgment and local login verification will be carried out during login and registration. Once the login is successful, it will be written into the Redis database and jump to the home page.

login() {
    this.$axios.post('/users/signin', {
        username: window.encodeURIComponent(this.username),
        password: cryptoJs.MD5(this.password).toString()
    }).then(({ status, data }) => {
        if (status === 200) {
            if (data && data.code === 0) {
               location.href = '/'}}else {
                this.error = data.msg
            }
        } else{this.error = 'Server error, status code${status}`}})}Copy the code

Switch the city

Write only the switching city of the previous one

To associate a province with a city, you must select a province to select the city under the province

  1. Obtain the administrative region query of Autonavi Web service API interface and return to the next two administrative levels (administrative levels include: country, province/municipality and city)

  2. Select element-UI components (select selector)

<span> select v-model= </span> <el-select V -model="pvalue" placeholder="Province">
    <el-option v-for="item in province" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <! --city. Length is empty, select city box will be disabled --> <el-select V-model ="cvalue" placeholder="City" :disabled=! "" city.length" @visible-change="select" ref="currentCity"> 
    <el-option v-for="item in city" :key="item.value" :label="item.label" :value="item.value">
    </el-option>
</el-select>
Copy the code
  1. After the DOM is mounted, query the AUtonavi administrative region API in the Mounted life cycle function. Get all the provinces according to the map and v-for render to the first drop – down box
export default {
  data() {
    return {
      pvalue: ' ',
      cvalue: ' ', 
      search: ' 'Public: [], / / all data province: [], / / all provinces city: [], / / according to the province city}}, / / 1. Get all data for all country/city/province asyncmounted() {
    const {status, data: { districts:[{ districts }]} } = await this.$axios.get('https://restapi.amap.com/v3/config/district?subdistrict=2&key=b598c12de310236d9d40d3e28ea94d03')
    ifPublic = "this.province" = "districts. Map" (item => {return {
          value: item.adcode,
          label: item.name
        }
      })
    }
  }
}
</script>
Copy the code
  1. Monitor the change of pValue, click the first drop-down box to select the province, you can obtain the relevant city according to the province
exportDefault {watch: {// Monitor the pvalue change, according to the province to obtain the city pvalue:function (newPvalue) {
      this.city = this.public.filter(item => item.adcode===newPvalue)[0].districts
      this.city = this.city.map(item => {
        return {
          value: item.name,
          label: item.name
        }
      })
    }
  }
}
Copy the code
  1. @visible-change listens for the appearance of a second drop-down box, triggering the select function. Choose the city, and the value of the second drop-down box is not empty, which will trigger actions, submit commint to mutations, and change the state of the city.
import { mapMutations } from 'vuex'
exportdefault { methods: { ... mapMutations({setPosition: 'geo/setPosition'
    }),
    async select () {
      const isSelect = this.$refs.currentCity.value
      if (isSelect) {
        this.$store.commit('geo/setCity', isSelect)
        location.href = '/'}}}}Copy the code
  1. The following problems occur: Before the update, the city has been changed, but after the update, the city is changed back to the city obtained based on the user IP address.

The reason:

Each time a page is refreshed, the vuex data is lost, the vue instance is reloaded, and the data in the Store is reassigned. In the previous nuxtServerInit function, store was quickly manipulated prior to page rendering (component loading) based on the city obtained by the user'S IP address. So even if the store's city is changed before the page is refreshed, it will still change back to the city obtained according to the user's IP address after the page is refreshed.Copy the code

Solution:

SessionStorage = sessionStorage = sessionStorage = sessionStorage = sessionStorage So state data is saved to sessionStorage when the page is refreshed (triggering beforeUnload event) and state information in sessionStorage is read when the page is loaded.Copy the code

But it’s too much trouble to write this on every page. So I put it in the default.vue file in the layouts folder.

export default {
    mounted() {// Read the status information in sessionStorage during page loadingif (window.sessionStorage.getItem("store") ) {
          this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem("store"))))
      } 

      //在页面刷新时将vuex里的信息保存到sessionStorage里
      window.addEventListener("beforeunload",()=>{
          window.sessionStorage.setItem("store",JSON.stringify(this.$store.state))
      })
    }
 }
Copy the code

Reference article: A solution to vuex’s state data loss after a page refresh for a vUE single page application