Creating an Online Store on Nuxt.js 2 Walkthrough Part 1


The article is aimed at people who already have an understanding of Vue, which Nuxt is based on, so I will focus only on things specific to Nuxt. But even if you are not familiar with them, the article will give a general idea of ​​what the project with Nuxt looks like.


You can learn useful hacks, plugins, and ways to solve the problems that often arise when creating Nuxt applications.


In this article I want to share how to create a primitive online store:


  • Which will be loaded quickly by the user.
  • Which will fall in love with Google (or any other search engine) in terms of SEO.

To simplify the perception of the process, the creation of the backend api will not be dealt with in this article, since this topic is quite voluminous and draws on a separate article.


Introduction


The advantage of progressive (PWA) frameworks like Nuxt.js is that:


  • You do not need to worry about the return of html with the help of a prerender as in the case of working with SPA for search robots.
  • , , js chunks, css styles api ( webpack 4, nuxt.js)
  • Google Lighthouse , ( 100/100 ).
  • CSS Modules, Babel

:



:


  • ( ).
  • cookie ( ).
  • .
  • , , , html .
  • 2 : 1) ( Babel) 2) Legacy Babel.
  • FOUT, FOIT, FOFT ( , , ).
  • IE 10.
  • svg.
  • SEO .
  • , html .
  • API XHR .

Nuxt


. Node v12.16.1 Yarn v1.22.0.
, package.json yarn install


package.json
{
  "name": "habr",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "analyze": "cross-env NODE_ENV=production  nuxt build --analyze",
    "build": "cross-env NODE_ENV=production nuxt build",
    "buildandstart": "cross-env NODE_ENV=production nuxt build && cross-env NODE_ENV=production nuxt start",
    "dev": "cross-env NODE_ENV=development NUXT_PORT=3000 nuxt",
    "start": "cross-env NODE_ENV=production nuxt start"
  },
  "dependencies": {
    "@nuxtjs/axios": "5.9.5",
    "@nuxtjs/style-resources": "1.0.0",
    "@nuxtjs/svg": "0.1.6",
    "cookie-universal-nuxt": "2.1.1",
    "cross-env": "7.0.0",
    "cssnano": "4.1.10",
    "cssnano-preset-advanced": "4.0.7",
    "imagemin-mozjpeg": "^8.0.0",
    "imagemin-webpack-plugin": "^2.4.2",
    "intersection-observer": "^0.7.0",
    "node-sass": "^4.13.1",
    "normalize.css": "8.0.1",
    "nuxt": "2.11.0",
    "nuxt-trailingslash-module": "1.1.0",
    "nuxt-webfontloader": "^1.1.0",
    "sass-loader": "^8.0.2",
    "vue-js-modal": "1.3.31",
    "vue-lazy-hydration": "^1.0.0-beta.12",
    "vue-lazyload": "1.3.3",
    "vue-svg-loader": "0.11.0",
    "vuelidate": "^0.7.5"
  },
  "devDependencies": {
    "@nuxtjs/eslint-config": "2.0.0",
    "babel-eslint": "8",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "6.10.0",
    "eslint-config-standard": "14.1.0",
    "eslint-friendly-formatter": "4.0.1",
    "eslint-loader": "3.0.3",
    "eslint-plugin-array-func": "3.1.3",
    "eslint-plugin-import": "2.20.1",
    "eslint-plugin-jest": "23.7.0",
    "eslint-plugin-lodash": "6.0.0",
    "eslint-plugin-no-loops": "0.3.0",
    "eslint-plugin-no-use-extend-native": "0.4.1",
    "eslint-plugin-node": "11.0.0",
    "eslint-plugin-prettier": "3.1.2",
    "eslint-plugin-promise": "4.2.1",
    "eslint-plugin-security": "1.4.0",
    "eslint-plugin-standard": "4.0.1",
    "eslint-plugin-vue": "6.1.2",
    "prettier": "1.19.1",
    "prettier-eslint": "9.0.1"
  }
}

devDependencies , .


:


  • @nuxtjs/axios XHR .
  • @nuxtjs/style-resources sass, .
  • cookie-universal-nuxt api cookie
  • cross-env Windows, ( dotenv)
  • cssnano css, .
  • image-webpack-loader JPG, GIF, WEBP, PNG .
  • intersection-observer Intersection Observer API ( ).
  • node-sass Webpack scss.
  • normalize.css .
  • nuxt-trailingslash-module ( SEO).
  • nuxt-webfontloader .
  • vue-js-modal .
  • vue-lazy-hydration Max Potential First Input Delay ( SSR , html , , ).
  • vue-lazyload .
  • vue-svg-loader SVG .
  • vuelidate .


--assets 
  --scss
  --img
  --svg
--components
--pages
  --category
  --checkout
  --product
--layouts
--plugins 
--store 
--middleware (       )
--serverMiddleware (     server only)

  • assets , .
  • components Vue
  • pages , , , Vue Router. .
  • layouts , , , , ( ).
  • plugins . .

layout


layout, .


layouts , , .


default.vue
<template>
    <nuxt/>
</template>

<script>
export default {
  computed: {
    meta () {
      return [
        { charset: 'utf-8' },
        {
          name: 'viewport',
          content: 'width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no'
        },
        { hid: 'description', name: 'description', content: '' }
      ]
    }
  },

  head () {
    const canonical = `https://mysite.com${this.$route.path
      .toLowerCase()
      .replace(/\/$/, '')}`
    return {
      meta: [
        ...this.meta

      ],
      script: [
        // { src: 'https://markknol.imtqy.com/console-log-viewer/console-log-viewer.js' }
      ],
      link: [{ rel: 'canonical', href: canonical }]
    }
  }
}
</script>

template Nuxt layout . .


computed meta(), , - .
Vuex .


head(), Nuxt . , .


, . Nuxt :


<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no">
<meta data-n-head="ssr" data-hid="description" name="description" content="">
<link data-n-head="ssr" rel="canonical" href="https://mysite.com">

:


  • data-n-head="ssr" .


  • data-hid="description" , , hid: 'description'. , Nuxt id, , , id. description layout ( SEO).


  • link: [{ rel: 'canonical', href: canonical }] , Nuxt "", .
    canonical . -.
    , site.com/product/myProduct, site.com/product/myProduct/, site.com/product/MyProduct/ , canonical, .


    <link data-n-head="ssr" rel="canonical" href="https://site.com/product/myproduct">

    , .


  • script. . . .



  • name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1 shrink-to-fit=no" ( Safary iOS).
  • charset="utf-8"


, . pages index.vue


index.vue
<template>
    <div>
<h1> !</h1>
    </div>
</template>

<script>
    export default {

    }
</script>

, Vue , . .


Nuxt


Nuxt . nuxt.config.js


nuxt.config.js
const imageminMozjpeg = require('imagemin-mozjpeg')
const ImageminPlugin = require('imagemin-webpack-plugin').default
const isDev = process.env.NODE_ENV !== 'production'

module.exports = {
  mode: 'universal',
  ...(!isDev && {
    modern: 'client'
  }),

  head: {
    htmlAttrs: {
      lang: 'ru'
    },
    title: 'Nuxt APP',
    meta: [
      { hid: 'description', name: 'description', content: '-' }
    ],
    link: [
      { rel: 'shortcut icon', href: 'favicon.ico' }
    ]
  },
  rootDir: __dirname,
  serverMiddleware: [
  ],
  router: {
    prefetchLinks: false
  },
  loading: { color: '#ddd' },
  css: [
    'normalize.css',
    './assets/scss/global-styles.scss'
  ],
  plugins: [
  ],
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    'nuxt-trailingslash-module',
    'nuxt-webfontloader',
    'cookie-universal-nuxt',
    '@nuxtjs/style-resources'
  ],

  webfontloader: {
    events: false,
    google: {
      families: ['Montserrat:400,500,600:cyrillic&display=swap']
    },
    timeout: 5000
  },
  styleResources: {
    // your settings here
    // scss: ['./assets/scss/global-variables.scss'], // alternative: scss
    less: [],
    stylus: []
  },
  /*
  ** Axios module configuration
  */
  axios: {
    // See https://github.com/nuxt-community/axios-module#options
  },
  render: {
    // http2: {
    //     push: true,
    //     pushAssets: (req, res, publicPath, preloadFiles) => preloadFiles
    //     .map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`)
    //   },
    // compressor: false,
    resourceHints: false,
    etag: false,
    static: {
      etag: false
    }
  },
  /*
  ** Build configuration
  */
  build: {
    optimizeCss: false,
    filenames: {
      app: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',
      chunk: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',
      css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash].css',
      img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]',
      font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[contenthash:7].[ext]',
      video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[contenthash:7].[ext]'
    },
    ...(!isDev && {
      html: {
        minify: {
          collapseBooleanAttributes: true,
          decodeEntities: true,
          minifyCSS: true,
          minifyJS: true,
          processConditionalComments: true,
          removeEmptyAttributes: true,
          removeRedundantAttributes: true,
          trimCustomFragments: true,
          useShortDoctype: true
        }
      }
    }),
    splitChunks: {
      layouts: true,
      pages: true,
      commons: true
    },
    optimization: {
      minimize: !isDev
    },
    ...(!isDev && {
      extractCSS: {
        ignoreOrder: true
      }
    }),
    transpile: ['vue-lazy-hydration', 'intersection-observer'],
    postcss: {
      plugins: {
        ...(!isDev && {
          cssnano: {
            preset: ['advanced', {
              autoprefixer: false,
              cssDeclarationSorter: false,
              zindex: false,
              discardComments: {
                removeAll: true
              }
            }]
          }
        })
      },
      ...(!isDev && {
        preset: {
          browsers: 'cover 99.5%',
          autoprefixer: true
        }
      }),

      order: 'cssnanoLast'
    },
    extend (config, ctx) {
      const ORIGINAL_TEST = '/\\.(png|jpe?g|gif|svg|webp)$/i'
      const vueSvgLoader = [
        {
          loader: 'vue-svg-loader',
          options: {
            svgo: false
          }
        }
      ]
      const imageMinPlugin = new ImageminPlugin({
        pngquant: {
          quality: '5-30',
          speed: 7,
          strip: true
        },
        jpegtran: {
          progressive: true

        },
        gifsicle: {
          interlaced: true
        },
        plugins: [
          imageminMozjpeg({
            quality: 70,
            progressive: true
          })

        ]
      })
      if (!ctx.isDev) config.plugins.push(imageMinPlugin)

      config.module.rules.forEach(rule => {
        if (rule.test.toString() === ORIGINAL_TEST) {
          rule.test = /\.(png|jpe?g|gif|webp)$/i
          rule.use = [
            {
              loader: 'url-loader',
              options: {
                limit: 1000,
                name: ctx.isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]'
              }
            }
          ]
        }
      })
      //  Create the custom SVG rule
      const svgRule = {
        test: /\.svg$/,
        oneOf: [
          {
            resourceQuery: /inline/,
            use: vueSvgLoader
          },
          {
            resourceQuery: /data/,
            loader: 'url-loader'
          },
          {
            resourceQuery: /raw/,
            loader: 'raw-loader'
          },
          {
            loader: 'file-loader' // By default, always use file-loader
          }
        ]
      }

      config.module.rules.push(svgRule) // Actually add the rule
    }
  }
}


  • const imageminMozjpeg = require('imagemin-mozjpeg')
    const ImageminPlugin = require('imagemin-webpack-plugin').default

    2 , Webpack .


  • const isDev = process.env.NODE_ENV !== 'production'

    , .


  • mode: 'universal'

    , SSR ( SPA).


  • ...(!isDev && {
    modern: 'client'
    }),

    ES6 . !isDev , modern: 'client' production. modern: 'client' Nuxt 2 , ES6 Modules , Legacy Babel. html 2 js :


    <script nomodule src="***" defer></script><script type="module" src="***" defer>

    .


  • head: {
    htmlAttrs: {
      lang: 'ru'
    },
    title: 'Nuxt APP',
    meta: [
      { hid: 'description', name: 'description', content: '-' }
    ],
    link: [
      { rel: 'shortcut icon', href: 'favicon.ico' }
    ]
    }

    Head, layout, head . , Title Description favicon.ico ( , ).


  • rootDir: __dirname

    .


  • router: {
    prefetchLinks: false
    }

    Nuxt, UI . . , . - , ( , 2G).


  • loading: { color: '#ddd' }

    , . , . XHR .


  • css: [
    'normalize.css'
    ]

    . normalize, .


  • modules: [
    '@nuxtjs/axios',
    'nuxt-trailingslash-module',
    'nuxt-webfontloader',
    'cookie-universal-nuxt',
    '@nuxtjs/style-resources'
    ]

    Nuxt , Vue, install, . Vue instance, , .


  • webfontloader: {
    events: false,
    google: {
      families: ['Montserrat:400,500,600:cyrillic&display=swap']
    },
    timeout: 5000
    }

    nuxt-webfontloader. . Google Fonts. , html , wf-active. &display=swap , html, js , css . , , events: false.


  • styleResources: {
    scss: ['./assets/scss/global-variables.scss'], // alternative: scss
    less: [],
    stylus: []
    }

    @nuxtjs/style-resources. . SCSS . global-variables.scss, , , .


  • axios: {
    // See https://github.com/nuxt-community/axios-module#options
    },

    axios. .


  • render: {
    // http2: {
    //     push: true,
    //     pushAssets: (req, res, publicPath, preloadFiles) => preloadFiles
    //     .map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`)
    //   },
    // compressor: false,
    resourceHints: false
    // etag: false,
    // static: {
    //  etag: false
    // }
    },

    Nuxt Web Server, http2. localhost, http2 https, .
    compressor Gzip , (html, js, css, ). https://www.npmjs.com/package/compression , production compressor: false Nuxt render, Nginx , , . Nuxt .
    etag, Nginx, etags.
    resourceHints , ( prefetchLinks: false) .


  • optimizeCss: false

    cssNano , .


    filenames: {
      app: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',
      chunk: ({ isDev }) => isDev ? '[name].js' : 'js/[contenthash].js',
      css: ({ isDev }) => isDev ? '[name].css' : 'css/[contenthash].css',
      img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]',
      font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[contenthash:7].[ext]',
      video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[contenthash:7].[ext]'
    },

    Nuxt Webpack . , development , production build contenthash. , production, js . , , - . - , , , .




html , 0 .


  • ...(!isDev && {
      html: {
        minify: {
          collapseBooleanAttributes: true,
          decodeEntities: true,
          minifyCSS: true,
          minifyJS: true,
          processConditionalComments: true,
          removeEmptyAttributes: true,
          removeRedundantAttributes: true,
          trimCustomFragments: true,
          useShortDoctype: true
        }
      }
    })

    Development html, .


  • splitChunks: {
      layouts: true,
      pages: true,
      commons: true
    },

    .


  • optimization: {
      minimize: !isDev
    },

    js development.


  • ...(!isDev && {
      extractCSS: {
        ignoreOrder: true
      }
    }),

    Nuxt html style. , css . css . development inline styles, production .
    ignoreOrder: true , webpack , #4885.


  • transpile: ['vue-lazy-hydration', 'intersection-observer']

    Babel , .


  • postcss: {
      plugins: {
        ...(!isDev && {
          cssnano: {
            preset: ['advanced', {
              autoprefixer: false,
              cssDeclarationSorter: false,
              zindex: false,
              discardComments: {
                removeAll: true
              }
            }]
          }
        })
      },
      ...(!isDev && {
        preset: {
          browsers: 'cover 99.5%',
          autoprefixer: true
        }
      }),
    
      order: 'cssnanoLast'
    }

    Nuxt Postcss . Postcss, . development , production css vendor 99.5% . , .


  • extend (config, ctx) {

    webpack , loaders test.
    Webpack '/\\.(png|jpe?g|gif|svg|webp)$/i' svg url-loader file-loader . svg . .


  • Webpack, , .


    const ORIGINAL_TEST = '/\\.(png|jpe?g|gif|svg|webp)$/i'
    const vueSvgLoader = [
    {
        loader: 'vue-svg-loader',
        options: {
        svgo: false
        }
    }
    ]
    const imageMinPlugin = new ImageminPlugin({
    pngquant: {
    quality: '5-30',
    speed: 7,
    strip: true
    },
    jpegtran: {
    progressive: true
    },
    gifsicle: {
    interlaced: true
    },
    plugins: [
    imageminMozjpeg({
    quality: 70,
    progressive: true
    })]
    })

  • if (!ctx.isDev) config.plugins.push(imageMinPlugin)

    production


  •   config.module.rules.forEach(rule => {
        if (rule.test.toString() === ORIGINAL_TEST) {
          rule.test = /\.(png|jpe?g|gif|webp)$/i
          rule.use = [
            {
              loader: 'url-loader',
              options: {
                limit: 1000,
                name: ctx.isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]'
              }
            }
          ]
        }
      })

  • svg


      const svgRule = {
        test: /\.svg$/,
        oneOf: [
          {
            resourceQuery: /inline/,
            use: vueSvgLoader
          },
          {
            resourceQuery: /data/,
            loader: 'url-loader'
          },
          {
            resourceQuery: /raw/,
            loader: 'raw-loader'
          },
          {
            loader: 'file-loader' // By default, always use file-loader
          }
        ]
      }
      config.module.rules.push(svgRule) 

    svg



nuxt.config.js .



Nuxt development. yarn dev
http://localhost:3000/ , .



production , dev server Ctrl+C yarn buildandstart


, package.json. .



99/100


  1. , , app.js . Google Fonts. . Webfonts Loader, 100/100, .
  2. http2 html, production 0,5-1. ( Nuxt Nginx).
  3. Lighthouse 4x CPU Throttling, , .
  4. Lighthouse . , .
  5. , Nginx

, , HighLoad .




Vuex store


API, , Vuex.


store index.js


index.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))
const categories = [
  {
    cName: '',
    cSlug: 'cats',
    cImage: 'https://source.unsplash.com/300x300/?cat,cats'
  },
  {
    cName: '',
    cSlug: 'dogs',
    cImage: 'https://source.unsplash.com/300x300/?dog,dogs'
  },
  {
    cName: '',
    cSlug: 'wolfs',
    cImage: 'https://source.unsplash.com/300x300/?wolf'
  },
  {
    cName: '',
    cSlug: 'bulls',
    cImage: 'https://source.unsplash.com/300x300/?ox'
  }
]

export const state = () => ({
  categoriesList: []
})
export const mutations = {
  SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
  }
}
export const actions = {
  async getCategoriesList ({ commit }) {
    try {
      await sleep(1000)
      await commit('SET_CATEGORIES_LIST', categories)
    } catch (err) {
      console.log(err)
      throw new Error('  ,  ')
    }
  }
}

Vuex


Vuex
  • const sleep = m => new Promise(r => setTimeout(r, m))

    , Async .


  • export const state = () => ({
    categoriesList: []
    })

    . .


  • export const mutations = {
    SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
    }
    }

    , . . . .


  • export const actions = {
    async getCategoriesList ({ commit }) {
    try {
      await sleep(1000)
      await commit('SET_CATEGORIES_LIST', categories)
    } catch (err) {
      console.log(err)
      throw new Error('  ,  ')
    }
    }
    }

    actions, , , store comit, , , . action Nuxt , axios API.
    actions API. actions, . API, API .




components commons, . CategoriesList.vue :


CategoriesList.vue
<template>
  <div>
    <h2> </h2>
    <div :class="$style.wrapper">
      <div
        v-for="category in categories"
        :key="category.cSlug"
        :class="$style.block"
      >
        <nuxt-link :to="`/category/${category.cSlug}`">
          <p>{{ category.cName }}</p>
          <img :src="category.cImage" />
        </nuxt-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    categories: {
      type: Array,
      default: () => []
    }
  }
}
</script>

<style lang="scss" module>
.wrapper {
  display: flex;
}
.block {
display: flex;
flex-direction: column;
}
</style>

  • <style lang="scss" module>
    .wrapper {
    display: flex;
    }
    .block {
    display: flex;
    flex-direction: column;
    }
    </style>

    CSS Modules, wrapper wrapper_26mY3 , .
    $style .


  • props: {
    categories: {
      type: Array,
      default: () => []
    }
    }

    , dump () . API .


  • <nuxt-link :to="`/category/${category.cSlug}`">

    Nuxt-link <router-link> , , , ( , .
    to cSlug , .


    <a href="/category/cats" class=""><p></p> <img src="https://source.unsplash.com/300x300/?cat,cats"></a>

    dom a href, IObserver. JS , .



index.vue


.


index.vue
<template>
  <div>
    <h1>- ""</h1>
    <CategoriesList :categories="categories" />
  </div>
</template>

<script>
import CategoriesList from '~~/components/common/CategoriesList'
import { mapState } from 'vuex'
export default {
  components: {
    CategoriesList
  },
  async asyncData ({ app, route, params, error, store }) {
    try {
      await store.dispatch('getCategoriesList')
    } catch (err) {
      console.log(err)
      return error({
        statusCode: 404,
        message: '      '
      })
    }
  },
  computed: {
    ...mapState({
      categories: 'categoriesList'
    })
  }
}
</script>

  • import CategoriesList from '~~/components/common/CategoriesList'

    ~~, .


  • import { mapState } from 'vuex'

    .


    async asyncData ({ app, route, params, error, store }) {
    try {
      await store.dispatch('getCategoriesList')
    } catch (err) {
      console.log(err)
      return error({
        statusCode: 404,
        message: '      '
      })
    }
    },

    asyncData , Nuxt . API , , action Vuex. async , Nuxt API .


  • computed: {
    ...mapState({
      categories: 'categoriesList'
    })
    }

    categories categoriesList


  • <CategoriesList :categories="categories" />

    , props categories





nuxt , . .



pages category _CategorySlug.vue :


_CategorySlug.vue
<template>
  <div>
    <h1>{{ category.cName }}</h1>
    <p>{{ category.cDesc }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  async asyncData ({ app, params, route, error }) {
    try {
      await app.store.dispatch('getCurrentCategory', { route })
    } catch (err) {
      console.log(err)
      return error({
        statusCode: 404,
        message: '      '
      })
    }
  },
  computed: {
    ...mapState({
      category: 'currentCategory'
    })
  },
  head () {
    return {
      title: this.category.cTitle,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.category.cMetaDescription
        }
      ]
    }
  }
}
</script>

_, Nuxt , .
http://127.0.0.1:3000/category/cats
route route.params.CategorySlug ( ), cats
index.vue,


  • await app.store.dispatch('getCurrentCategory', { route })

    actions, , route.


  • head () {
    return {
      title: this.category.cTitle,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.category.cMetaDescription
        }
      ]
    }
    }

    Title Meta description, API.



Vuex


index.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))
const categories = [
  {
    cTitle: '',
    cName: '',
    cSlug: 'cats',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?cat,cats'
  },
  {
    cTitle: '',
    cName: '',
    cSlug: 'dogs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?dog,dogs'
  },
  {
    cTitle: '',
    cName: '',
    cSlug: 'wolfs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?wolf'
  },
  {
    cTitle: '',
    cName: '',
    cSlug: 'bulls',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?bull'
  }
]

export const state = () => ({
  categoriesList: [],
  currentCategory: {}
})
export const mutations = {
  SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
  },
  SET_CURRENT_CATEGORY (state, category) {
    state.currentCategory = category
  }
}
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)
    await commit('SET_CURRENT_CATEGORY', category)
  }
}

getCurrentCategory action route state.



.



JSON .

static/mock. Nuxt , static. Axios.


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 addProductsToCategory (products, 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: `https://source.unsplash.com/300x300/?${p.pName}`
      })
    }
  })
  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 = await this.$axios.$get('/mock/products.json')

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

action getCurrentCategory , axios products.json. .


ProductBrief.vue components/category.


ProductBrief.vue
<template>
  <div :class="$style.wrapper">
    <nuxt-link :to="`/product/${product.pSlug}`">
      <p>{{ product.pName }}</p>
      <img :src="product.image" />
    </nuxt-link>
    <p> {{ product.pPrice }}</p>
  </div>
</template>

<script>
export default {
  props: {
    product: {
      type: Object,
      default: () => {}
    }
  }
}
</script>

<style lang="scss" module>
.wrapper {
  display: flex;
  flex-direction: column;
}
</style>

dump , .



_CategorySlug.vue
<template>
  <div>
    <h1>{{ category.cName }}</h1>
    <p>{{ category.cDesc }}</p>
    <div :class="$style.productList">
      <div
        v-for="product in category.products"
        :key="product.id"
      >
        <ProductBrief :product="product" />
      </div>
    </div>
  </div>
</template>

<script>
import ProductBrief from '~~/components/category/ProductBrief'
import { mapState } from 'vuex'
export default {
  components: {
    ProductBrief
  },
  async asyncData ({ app, params, route, error }) {
    try {
      await app.store.dispatch('getCurrentCategory', { route })
    } catch (err) {
      console.log(err)
      return error({
        statusCode: 404,
        message: '      '
      })
    }
  },
  computed: {
    ...mapState({
      category: 'currentCategory'
    })
  },
  head () {
    return {
      title: this.category.cTitle,
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: this.category.cMetaDescription
        }
      ]
    }
  }
}
</script>
<style lang="scss" module>
.productList {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}
</style>

.



, 150 , . .


plugins


vue-lazy-load.js
import Vue from 'vue'
import VueLazyload from 'vue-lazyload'

export default async (context, inject) => {
  Vue.use(VueLazyload, {
    preLoad: 0,
    error: 'https://via.placeholder.com/300',
    // eslint-disable-next-line
    loading: require(`${'~~/assets/svg/download.svg'}`),
    attempt: 3,
    lazyComponent: true,
    observer: true,
    throttleWait: 500
  })
}

placeholder , .
nuxt.config.js


  plugins: [
    { src: '~~/plugins/vue-lazy-load.js' }
  ],

img src


      <img
        v-lazy="product.image"
        :class="$style.image"
      />


.image {
  width: 300px;
  height: 300px;
}



. .




Thanks to everyone who read the article. This is just a practical example of using Nuxt. In a real project, you need to proceed from business logic, data structure, etc. TP, but this project is far from reality.


In the sequel, we will increase the functionality of the site, because so far it is no good.


All Articles