Nuxt.js 2 рд╡реЙрдХрдереНрд░реВ рднрд╛рдЧ 1 рдкрд░ рдПрдХ рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░ рдмрдирд╛рдирд╛


рдпрд╣ рд▓реЗрдЦ рдРрд╕реЗ рд▓реЛрдЧреЛрдВ рдХреЗ рд▓рд┐рдП рд▓рдХреНрд╖рд┐рдд рд╣реИ, рдЬрд┐рдирдХреЗ рдкрд╛рд╕ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА Vue рдХреА рд╕рдордЭ рд╣реИ, рдЬреЛ Nuxt рдкрд░ рдЖрдзрд╛рд░рд┐рдд рд╣реИ, рдЗрд╕рд▓рд┐рдП рдореИрдВ рдХреЗрд╡рд▓ Nuxt рдХреЗ рд▓рд┐рдП рд╡рд┐рд╢рд┐рд╖реНрдЯ рдЪреАрдЬреЛрдВ рдкрд░ рдзреНрдпрд╛рди рдХреЗрдВрджреНрд░рд┐рдд рдХрд░реВрдВрдЧрд╛ред рд▓реЗрдХрд┐рди рдпрд╣рд╛рдВ рддрдХ тАЛтАЛрдХрд┐ рдЕрдЧрд░ рдЖрдк рдЙрдирд╕реЗ рдкрд░рд┐рдЪрд┐рдд рдирд╣реАрдВ рд╣реИрдВ, рддреЛ рд▓реЗрдЦ рдЗрд╕ рдмрд╛рдд рдХрд╛ рдПрдХ рд╕рд╛рдорд╛рдиреНрдп рд╡рд┐рдЪрд╛рд░ рджреЗрдЧрд╛ рдХрд┐ рдиреНрдпреВрдЯреЗрдХреНрд╕ рдХреЗ рд╕рд╛рде рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдХреИрд╕рд╛ рджрд┐рдЦрддрд╛ рд╣реИред


рдЖрдк рдЙрдкрдпреЛрдЧреА рд╣реИрдХреНрд╕, рдкреНрд▓рдЧрдЗрдиреНрд╕ рдФрд░ рдЙрди рд╕рдорд╕реНрдпрд╛рдУрдВ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреЗ рддрд░реАрдХреЗ рд╕реАрдЦ рд╕рдХрддреЗ рд╣реИрдВ рдЬреЛ рдЕрдХреНрд╕рд░ Nuxt рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рддреЗ рд╕рдордп рдЙрддреНрдкрдиреНрди рд╣реЛрддреА рд╣реИрдВред


рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ рдореИрдВ рдПрдХ рдЖрджрд┐рдо рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░ рдмрдирд╛рдиреЗ рдХрд╛ рддрд░реАрдХрд╛ рд╕рд╛рдЭрд╛ рдХрд░рдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВ:


  • рдЬрд┐рд╕реЗ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рджреНрд╡рд╛рд░рд╛ рдЬрд▓реНрджреА рд╕реЗ рд▓реЛрдб рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред
  • рдЬреЛ SEO рдХреЗ рдорд╛рдорд▓реЗ рдореЗрдВ Google (рдпрд╛ рдХрд┐рд╕реА рдЕрдиреНрдп рд╕рд░реНрдЪ рдЗрдВрдЬрди) рдХреЗ рдкреНрдпрд╛рд░ рдореЗрдВ рдкрдбрд╝ рдЬрд╛рдПрдЧрд╛ред

рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдХреА рдзрд╛рд░рдгрд╛ рдХреЛ рд╕рд░рд▓ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдмреИрдХрдПрдВрдб рдПрдкрд┐ рдХреЗ рдирд┐рд░реНрдорд╛рдг рд╕реЗ рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ рдирд┐рдкрдЯрд╛ рдирд╣реАрдВ рдЬрд╛рдПрдЧрд╛, рдХреНрдпреЛрдВрдХрд┐ рдпрд╣ рд╡рд┐рд╖рдп рдХрд╛рдлреА рдмрдбрд╝рд╛ рд╣реИ рдФрд░ рдПрдХ рдЕрд▓рдЧ рд▓реЗрдЦ рдкрд░ рдЖрддрд╛ рд╣реИред


рдкрд░рд┐рдЪрдп


Nuxt.js рдЬреИрд╕реЗ рдкреНрд░рдЧрддрд┐рд╢реАрд▓ (PWA) рдЪреМрдЦрдЯреЗ рдХрд╛ рд▓рд╛рдн рдпрд╣ рд╣реИ рдХрд┐:


  • рдЖрдкрдХреЛ рдПрдХ рдкреВрд░реНрд╡рдЬреЗрдВрдбрд░ рдХреА рдорджрдж рд╕реЗ html рдХреА рд╡рд╛рдкрд╕реА рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЪрд┐рдВрддрд╛ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ рдХреНрдпреЛрдВрдХрд┐ рдЦреЛрдЬ рд░реЛрдмреЛрдЯ рдХреЗ рд▓рд┐рдП SPA рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рдорд╛рдорд▓реЗ рдореЗрдВред
  • , , 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. . . .



  • рдирд╛рдо = "рд╡реНрдпреВрдкреЛрд░реНрдЯ" рд╕рд╛рдордЧреНрд░реА = "рдЪреМрдбрд╝рд╛рдИ = рдбрд┐рд╡рд╛рдЗрд╕-рдЪреМрдбрд╝рд╛рдИ, рдкреНрд░рд╛рд░рдВрднрд┐рдХ-рд╕реНрдХреЗрд▓ = 1, рдЕрдзрд┐рдХрддрдо-рд╕реНрдХреЗрд▓ = 1 рд╣рдЯрдирд╛-рдлрд┐рдЯ рд╣реЛрдирд╛ = рдХреЛрдИ" рдирд┐рд╖реНрдХреНрд░рд┐рдп рди рдХрд░реЗрдВ рдореЛрдмрд╛рдЗрд▓ рдЙрдкрдХрд░рдгреЛрдВ рдкрд░ (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;
}



. .




рд▓реЗрдЦ рдкрдврд╝рдиреЗ рд╡рд╛рд▓реЗ рд╕рднреА рдХреЛ рдзрдиреНрдпрд╡рд╛рджред рдпрд╣ Nuxt рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХрд╛ рд╕рд┐рд░реНрдл рдПрдХ рд╡реНрдпрд╛рд╡рд╣рд╛рд░рд┐рдХ рдЙрджрд╛рд╣рд░рдг рд╣реИред рдПрдХ рд╡рд╛рд╕реНрддрд╡рд┐рдХ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдореЗрдВ, рдЖрдкрдХреЛ рд╡реНрдпрд╛рд╡рд╕рд╛рдпрд┐рдХ рддрд░реНрдХ, рдбреЗрдЯрд╛ рд╕рдВрд░рдЪрдирд╛, рдЖрджрд┐ рд╕реЗ рдЖрдЧреЗ рдмрдврд╝рдирд╛ рд╣реЛрдЧрд╛ред рдЯреАрдкреА, рд▓реЗрдХрд┐рди рдпрд╣ рдкрд░рд┐рдпреЛрдЬрдирд╛ рд╡рд╛рд╕реНрддрд╡рд┐рдХрддрд╛ рд╕реЗ рдмрд╣реБрдд рджреВрд░ рд╣реИред


рдЕрдЧрд▓реА рдХрдбрд╝реА рдореЗрдВ, рд╣рдо рд╕рд╛рдЗрдЯ рдХреА рдХрд╛рд░реНрдпрдХреНрд╖рдорддрд╛ рдмрдврд╝рд╛рдПрдВрдЧреЗ, рдХреНрдпреЛрдВрдХрд┐ рдЕрднреА рддрдХ рдпрд╣ рдЕрдЪреНрдЫрд╛ рдирд╣реАрдВ рд╣реИред


All Articles