在Nuxt.js上创建在线商店2演练第2部分


第一部分在这里


我们继续发展我们的在线商店。这部分将是:


  • 在静态地址正常加载图片
  • 客户生成的面包屑
  • 产品页面
  • 一顶帽子
  • 工作按钮购买,选项卡(和会话)之间的商品同步

Unsplash . ( ). .


""


.


const products = require('../static/mock/products.json')

const got = require('got')
const QS = require('querystring')
const API_KEY = ''

const fs = require('fs')
const { promisify } = require('util')

const writeFileAsync = promisify(fs.writeFile)

async function fetchApiImg (searchQuery) {
  try {
    const query = QS.stringify({ key: API_KEY, q: searchQuery, per_page: '3', image_type: 'photo' })
    const resPr = got(`https://pixabay.com/api/?${query}`)
    const json = await resPr.json()
    if (json.hits && json.hits.length > 0 && json.hits[0].largeImageURL && json.hits[0].webformatURL) {
      return {
        imgXL: json.hits[0].largeImageURL,
        imgL: json.hits[0].webformatURL
      }
    } else {
      throw 'no image'
    }
  } catch (error) {
    return {
      imgXL: null,
      imgL: null
    }
  }
}
async function getImagesUrls () {
  let imagesUrlWithId =
    products.map(product => {
      const productName = product.pName.split(' ')[0]
      const urls = fetchApiImg(productName)
      return { id: product.id, urls }
    })
  const imagesUrls = await Promise.all(imagesUrlWithId.map(iu => iu.urls))
  imagesUrlWithId = imagesUrlWithId.map((ob, index) => {
    return {
      id: ob.id,
      urls: imagesUrls[index]
    }
  }
  )
  return imagesUrlWithId
}
async function main () {
  try {
    const imagesUrls = await getImagesUrls()
    await writeFileAsync('./static/mock/products-images.json', JSON.stringify(imagesUrls), { flag: 'w+' })
  } catch (error) {
    console.log(error)
  }
}
main()

API_KEY ( 1 ).
, , 1 , 500 ( ddos).


:


  await Promise.all(
    products.map(async product => {
      const productName = product.pName.split(' ')[0]
      const imgUrl = await fetchApiImg(productName)
      imagesUrl.push({ id: product.id, url: imgUrl })
    })
  )

, url ( ), imagesUrl () Promise. await Promise.all .


product.pName.split(' ')[0]
imagesUrl id url .


    const query = QS.stringify({ key: API_KEY, q: searchQuery, per_page: '3', image_type: 'photo' })
    const resPr = got(`https://pixabay.com/api/?${query}`)
    const json = await resPr.json()
    if (json.hits && json.hits.length > 0 && json.hits[0].largeImageURL && json.hits[0].webformatURL) {
      return {
        imgXL: json.hits[0].largeImageURL,
        imgL: json.hits[0].webformatURL
      }
    } else {
      throw 'no image'
    }

QS.stringify querystring ( ) ( , ?)
got(https://pixabay.com/api/?${query})
await resPr.json() , json


Store


, .


store/index.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))
const categories = [
  {
    id: 'cats',
    cTitle: '',
    cName: '',
    cSlug: 'cats',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?cat,cats',
    products: []
  },
  {
    id: 'dogs',
    cTitle: '',
    cName: '',
    cSlug: 'dogs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?dog,dogs',
    products: []
  },
  {
    id: 'wolfs',
    cTitle: '',
    cName: '',
    cSlug: 'wolfs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?wolf',
    products: []
  },
  {
    id: 'bulls',
    cTitle: '',
    cName: '',
    cSlug: 'bulls',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?bull',
    products: []
  }
]
function addProductsToCategory (products, productsImages, category) {
  const categoryInner = { ...category, products: [] }
  products.map(p => {
    if (p.category_id === category.id) {
      categoryInner.products.push({
        id: p.id,
        pName: p.pName,
        pSlug: p.pSlug,
        pPrice: p.pPrice,
        image: productsImages.find(img => img.id === p.id).urls
      })
    }
  })
  return categoryInner
}
export const state = () => ({
  categoriesList: [],
  currentCategory: {},
  currentProduct: {}
})
export const mutations = {
  SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
  },
  SET_CURRENT_CATEGORY (state, category) {
    state.currentCategory = category
  },
  SET_CURRENT_PRODUCT (state, product) {
    state.currentProduct = product
  }
}
export const actions = {
  async getCategoriesList ({ commit }) {
    try {
      await sleep(1000)
      await commit('SET_CATEGORIES_LIST', categories)
    } catch (err) {
      console.log(err)
      throw new Error('  ,  ')
    }
  },
  async getCurrentCategory ({ commit }, { route }) {
    await sleep(1000)
    const category = categories.find((cat) => cat.cSlug === route.params.CategorySlug)

    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]
    )

    await commit('SET_CURRENT_CATEGORY', addProductsToCategory(products, productsImages, category))
  }
}

:


    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
         this.$axios.$get('/mock/products-images.json')
      ]
    )

api. 20 .


Vue ( ).


- :




Header.vue
<template>
  <div :class="$style.wrapper">
    <div :class="$style.header">
      <n-link :class="$style.logo" to="/">
        <p>
          
        </p>
      </n-link>
    </div>
  </div>
</template>

<script>
export default {

}
</script>

<style lang="scss" module>
.wrapper {
    background-color: $basic-bg-color;
    height: 70px;
}
.header {
  @include globalWrapper;
  display: flex;
}
.logo {
  font-size: 2.2em;
  font-weight: 700;
  opacity: 1;
   color: #000;
   text-decoration: none;
}
</style>

default.vue


default.vue
<Header />

<nuxt />





mainWrapper .
, max-width . global-variables.scss.


@mixin globalWrapper {
  max-width: 1280px;
  margin: 0 auto;
  padding:  0 20px;
}
$basic-bg-color: #fcc000;

. Header


.header {
  @include globalWrapper;
  display: flex;
}

.


:



:




_ProductSlug.vue


_ProductSlug.vue
<template>
  <div :class="$style.page">
    <div :class="$style.topBlock">
      <div :class="$style.topLeftBlock">
        <a :href="product.images.imgXL" target="_blank">
          <img
            v-lazy="product.images.imgL"
            :class="$style.image"
          />
        </a>
      </div>
      <div :class="$style.topRightBlock">
        <h1>{{ product.pName }}</h1>
        <p>: {{ product.pPrice }}</p>
      </div>
    </div>
    <h2></h2>
    <p>{{ product.pDesc }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  async asyncData ({ app, params, route, error }) {
    try {
      await app.store.dispatch('getCurrentProduct', { route })
    } catch (err) {
      console.log(err)
      return error({
        statusCode: 404,
        message: '      '
      })
    }
  },
  computed: {
    ...mapState({
      product: 'currentProduct'
    })
  },
  head () {
    return {
      title: this.product.pTitle,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.product.pMetaDescription
        }
      ]
    }
  }
}
</script>
<style lang="scss" module>
.page {
  @include globalWrapper;
}
.image {
  width: 400px;
  height: auto;
}
.topBlock {
  padding-top: 2em;
  display: flex;
  .topLeftBlock {
    display: flex;
  }
  .topRightBlock {
    padding-left: 2em;
    display: flex;
    flex-direction: column;
  }
}
</style>

,


        <a :href="product.images.imgXL" target="_blank">
          <img
            v-lazy="product.images.imgL"
            :class="$style.image"
          />
        </a>

imgL , imgL .





. nuxt meta , route


{                                                                                                                                                                                              
  name: 'category-CategorySlug',
  meta: [
    {}
  ],
  path: '/category/dogs',
  hash: '',
  query: {},
  params: {
    CategorySlug: 'dogs'
  },
  fullPath: '/category/dogs',
  matched: [
    {
      path: '/category/:CategorySlug?',
      regex: /^\/category(?:\/((?:[^\/]+?)))?(?:\/(?=$))?$/i,
      components: [Object],
      instances: {},
      name: 'category-CategorySlug',
      parent: undefined,
      matchAs: undefined,
      redirect: undefined,
      beforeEnter: undefined,
      meta: {},
      props: {}
    }
  ]
}

.


, , , :


Vuex :


index.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))
const categories = [
  {
    id: 'cats',
    cTitle: '',
    cName: '',
    cSlug: 'cats',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?cat,cats',
    products: []
  },
  {
    id: 'dogs',
    cTitle: '',
    cName: '',
    cSlug: 'dogs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?dog,dogs',
    products: []
  },
  {
    id: 'wolfs',
    cTitle: '',
    cName: '',
    cSlug: 'wolfs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?wolf',
    products: []
  },
  {
    id: 'bulls',
    cTitle: '',
    cName: '',
    cSlug: 'bulls',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?bull',
    products: []
  }
]
function getProduct (products, productsImages, productSlug) {
  const innerProduct = products.find(p => p.pSlug === productSlug)
  if (!innerProduct) return null
  return {
    ...innerProduct,
    images: productsImages.find(img => img.id === innerProduct.id).urls,
    category: categories.find(cat => cat.id === innerProduct.category_id)
  }
}
function addProductsToCategory (products, productsImages, category) {
  const categoryInner = { ...category, products: [] }
  products.map(p => {
    if (p.category_id === category.id) {
      categoryInner.products.push({
        id: p.id,
        pName: p.pName,
        pSlug: p.pSlug,
        pPrice: p.pPrice,
        image: productsImages.find(img => img.id === p.id).urls
      })
    }
  })
  return categoryInner
}
function getBreadcrumbs (pageType, route, data) {
  const crumbs = []
  crumbs.push({
    title: '',
    url: '/'
  })
  switch (pageType) {
    case 'category':
      crumbs.push({
        title: data.cName,
        url: `/category/${data.cSlug}`
      })
      break
    case 'product':
      crumbs.push({
        title: data.category.cName,
        url: `/category/${data.category.cSlug}`
      })
      crumbs.push({
        title: data.pName,
        url: `/product/${data.pSlug}`
      })

      break

    default:
      break
  }
  return crumbs
}
export const state = () => ({
  categoriesList: [],
  currentCategory: {},
  currentProduct: {},
  bredcrumbs: []
})
export const mutations = {
  SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
  },
  SET_CURRENT_CATEGORY (state, category) {
    state.currentCategory = category
  },
  SET_CURRENT_PRODUCT (state, product) {
    state.currentProduct = product
  },
  SET_BREADCRUMBS (state, crumbs) {
    state.bredcrumbs = crumbs
  },
  RESET_BREADCRUMBS (state) {
    state.bredcrumbs = []
  }
}
export const actions = {
  async setBreadcrumbs ({ commit }, data) {
    await commit('SET_BREADCRUMBS', data)
  },
  async getCategoriesList ({ commit }) {
    try {
      await sleep(300)
      await commit('SET_CATEGORIES_LIST', categories)
    } catch (err) {
      console.log(err)
      throw new Error('  ,  ')
    }
  },
  async getCurrentCategory ({ commit, dispatch }, { route }) {
    await sleep(300)
    const category = categories.find((cat) => cat.cSlug === route.params.CategorySlug)

    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]
    )
    const crubms = getBreadcrumbs('category', route, category)
    await dispatch('setBreadcrumbs', crubms)

    await commit('SET_CURRENT_CATEGORY', addProductsToCategory(products, productsImages, category))
  },
  async getCurrentProduct ({ commit, dispatch }, { route }) {
    await sleep(300)
    const productSlug = route.params.ProductSlug
    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]

    )
    const product = getProduct(products, productsImages, productSlug)
    const crubms = getBreadcrumbs('product', route, product)
    await dispatch('setBreadcrumbs', crubms)
    await commit('SET_CURRENT_PRODUCT', product)
  }

}

.


function getBreadcrumbs (pageType, route, data) {
  const crumbs = []
  crumbs.push({
    title: '',
    url: '/'
  })
  switch (pageType) {
    case 'category':
      crumbs.push({
        title: data.cName,
        url: `/category/${data.cSlug}`
      })
      break
    case 'product':
      crumbs.push({
        title: data.category.cName,
        url: `/category/${data.category.cSlug}`
      })
      crumbs.push({
        title: data.pName,
        url: `/product/${data.pSlug}`
      })

      break

    default:
      break
  }
  return crumbs
}

: 1 route, data ( - pageType .


.


( 2- .)


api


const crubms = getBreadcrumbs('product', route, product)

, , . .


store




, Breadcrumbs.vue


Breadcrumbs.vue
<template>
  <div v-if="bredcrumbs && bredcrumbs.length > 0" :class="$style.breadcrumbs">
    <ul>
      <li v-for="cr in bredcrumbs" :key="cr.url">
        <n-link :to="cr.url">
          {{ cr.title }}
        </n-link>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState({
      bredcrumbs: 'bredcrumbs'
    })
  }
}
</script>

<style lang="scss" module>
/* Style the list */
.breadcrumbs {
  background-color: #eee;
  padding: 10px 16px;
  ul {
    list-style: none;

    @include globalWrapper;
    li {
      display: inline;
      font-size: 18px;
      + ::before {
        padding: 8px;
        color: black;
        content: '/\00a0';
      }
      a {
        color: #0275d8;
        text-decoration: none;

        &:hover {
          color: #01447e;
          text-decoration: underline;
        }
      }
    }
  }
}
</style>

layout, :




. . , .



middlware


export default async function ({ store, route }) {
  if (route.matched.length > 0) {
    await Promise.all(route.matched.map(async ({ name }) => {
      if (name === 'index') {
        await store.commit('RESET_BREADCRUMBS')
      }
    }))
  }
}

nuxt.config.js


  router: {
    middleware: ['resetBreacrumbs'],
    prefetchLinks: false
  },

, — .


, .



api , localStorage.


, Vuex watcher . localStorage, , , , . , .


nuxt-vuex-localstorage


nuxt-vuex-localstorage nuxt.config.js


    ['nuxt-vuex-localstorage', {
      ...(isDev && {
        mode: 'debug'
      }),
      localStorage: ['cart'] //  If not entered, “localStorage” is the default value
    }]

localStorage ( , "" ), mode: 'debug'. Vuex cart.


Vuex


cart.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))

export const state = () => ({
  products: [],
  version: '0.0.1'

})
export const mutations = {
  ADD_PRODUCT (state, product) {
    // if cart doesn't have product add it
    if (!state.products.find(p => product.id === p.id)) {
      state.products = [...state.products, product]
    }
  },
  SET_PRODUCT (state, { productId, data }) {
    state.products = [...state.products.filter(prod => prod.id !== productId), data]
  },
  REMOVE_PRODUCT (state, productId) {
    state.products = Array.from(state.products.filter(prod => prod.id !== productId))
  }

}
export const actions = {
  async addProduct ({ commit }, data) {
    await sleep(300)
    await commit('ADD_PRODUCT', data)
  },
  async removeProduct ({ commit }, productId) {
    await sleep(300)
    await commit('REMOVE_PRODUCT', productId)
  }

}

version: '0.0.1' storage .


, .



BuyButton.vue
<template>
  <div v-if="product">
    <button
      v-if="!isProductAdded"
      :class="$style.buy"
      @click.prevent="buyClickHandler"
    >
      
    </button>
    <a
      v-else
      :class="$style.added"
      href="#"
      @click.prevent="addedClickHandler"
    >
        
    </a>
  </div>
</template>

<script>
import { mapActions, mapState } from 'vuex'

export default {
  props: {
    product: {
      type: Object,
      required: true
    }
  },
  computed: {
    ...mapState({
      products: state => state.cart.products
    }),
    isProductAdded () {
      return this.products.find(p => p.id === this.product.id)
    }
  },
  methods: {
    ...mapActions({
      addProduct: 'cart/addProduct',
      removeProduct: 'cart/removeProduct'
    }),
    buyClickHandler () {
      this.addProduct(this.product)
    },
    addedClickHandler () {
      this.removeProduct(this.product.id)
    }
  }
}
</script>

<style lang="scss" module>
.buy {
  background-color: $basic-bg-color; /* Green */
  border: none;
  color: #000;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;

  &:hover {
    cursor: pointer;
  }
}
.added {
  text-decoration: none;
  border-bottom: 2px dotted;
}
</style>

.


  • mapActions cart/addProduct
  • mapState , , state state => state.cart.products
  • isProductAdded
  • , , , .

.





, ( ).



  • : Github .
  • : .
  • 1
  • 2
  • 3

Nuxt.
, , .


, .



, , 6 . .


, , , .


!


All Articles