
本文针对的是已经了解Nux所基于的Vue的人员,因此,我将只关注Nuxt特有的内容。但是即使您不熟悉它们,本文也会给出使用Nuxt的项目的大致概念。
您可以学习有用的技巧,插件以及解决创建Nuxt应用程序时经常出现的问题的方法。
在本文中,我想分享如何创建原始的在线商店:
- 它将由用户快速加载。
- 在搜索引擎优化方面,这将爱上Google(或任何其他搜索引擎)。
为了简化对流程的了解,本文将不介绍后端api的创建,因为该主题非常繁琐,并且来自另一篇文章。
介绍
像Nuxt.js这样的渐进(PWA)框架的优点是:
- 您无需担心在预渲染的帮助下返回html,就像使用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: [
        
      ],
      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.jsconst 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: [
    
    '@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: {
    
    
    less: [],
    stylus: []
  },
  
  axios: {
    
  },
  render: {
    
    
    
    
    
    
    resourceHints: false,
    etag: false,
    static: {
      etag: false
    }
  },
  
  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]'
              }
            }
          ]
        }
      })
      
      const svgRule = {
        test: /\.svg$/,
        oneOf: [
          {
            resourceQuery: /inline/,
            use: vueSvgLoader
          },
          {
            resourceQuery: /data/,
            loader: 'url-loader'
          },
          {
            resourceQuery: /raw/,
            loader: 'raw-loader'
          },
          {
            loader: 'file-loader' 
          }
        ]
      }
      config.module.rules.push(svgRule) 
    }
  }
}
 - 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'], 
less: [],
stylus: []
}
 
 - @nuxtjs/style-resources. . SCSS . global-variables.scss, , , . 
 
- axios: {
},
 
 - axios. . 
 
- render: {
resourceHints: 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' 
      }
    ]
  }
  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
- , , app.js . Google Fonts. . Webfonts Loader, 100/100, .
- http2 html, production 0,5-1. ( Nuxt Nginx).
- Lighthouse 4x CPU Throttling, , .
- Lighthouse . , .
- , Nginx
, , HighLoad .

Vuex store
API, , Vuex.
store index.js
index.js
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
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
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.jsimport Vue from 'vue'
import VueLazyload from 'vue-lazyload'
export default async (context, inject) => {
  Vue.use(VueLazyload, {
    preLoad: 0,
    error: 'https://via.placeholder.com/300',
    
    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;
}

. .
感谢阅读本文的所有人。这只是使用Nuxt的实际示例。在真实的项目中,您需要从业务逻辑,数据结构等开始。TP,但是这个项目远非现实。
在后续版本中,我们将增加站点的功能,因为到目前为止这还不好。