
рдпрд╣ рд▓реЗрдЦ рдРрд╕реЗ рд▓реЛрдЧреЛрдВ рдХреЗ рд▓рд┐рдП рд▓рдХреНрд╖рд┐рдд рд╣реИ, рдЬрд┐рдирдХреЗ рдкрд╛рд╕ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА 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: [
],
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.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
Vuexconst 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 рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХрд╛ рд╕рд┐рд░реНрдл рдПрдХ рд╡реНрдпрд╛рд╡рд╣рд╛рд░рд┐рдХ рдЙрджрд╛рд╣рд░рдг рд╣реИред рдПрдХ рд╡рд╛рд╕реНрддрд╡рд┐рдХ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдореЗрдВ, рдЖрдкрдХреЛ рд╡реНрдпрд╛рд╡рд╕рд╛рдпрд┐рдХ рддрд░реНрдХ, рдбреЗрдЯрд╛ рд╕рдВрд░рдЪрдирд╛, рдЖрджрд┐ рд╕реЗ рдЖрдЧреЗ рдмрдврд╝рдирд╛ рд╣реЛрдЧрд╛ред рдЯреАрдкреА, рд▓реЗрдХрд┐рди рдпрд╣ рдкрд░рд┐рдпреЛрдЬрдирд╛ рд╡рд╛рд╕реНрддрд╡рд┐рдХрддрд╛ рд╕реЗ рдмрд╣реБрдд рджреВрд░ рд╣реИред
рдЕрдЧрд▓реА рдХрдбрд╝реА рдореЗрдВ, рд╣рдо рд╕рд╛рдЗрдЯ рдХреА рдХрд╛рд░реНрдпрдХреНрд╖рдорддрд╛ рдмрдврд╝рд╛рдПрдВрдЧреЗ, рдХреНрдпреЛрдВрдХрд┐ рдЕрднреА рддрдХ рдпрд╣ рдЕрдЪреНрдЫрд╛ рдирд╣реАрдВ рд╣реИред