
O artigo é destinado a pessoas que já conhecem o Vue, nas quais o Nuxt se baseia, portanto, focarei apenas em coisas específicas do Nuxt. Mas mesmo que você não esteja familiarizado com eles, o artigo dará uma idéia geral de como é o projeto com o Nuxt.
Você pode aprender hacks, plugins e maneiras úteis de resolver os problemas que costumam surgir ao criar aplicativos Nuxt.
Neste artigo, quero compartilhar como criar uma loja online primitiva:
- Que será carregado rapidamente pelo usuário.
- Que se apaixonará pelo Google (ou qualquer outro mecanismo de pesquisa) em termos de SEO.
Para simplificar a percepção do processo, a criação da API de back-end não será tratada neste artigo, pois esse tópico é bastante volumoso e baseia-se em um artigo separado.
Introdução
A vantagem de estruturas progressivas (PWA) como o Nuxt.js é que:
- Você não precisa se preocupar com o retorno do html com a ajuda de um pré-fornecedor, como no caso de trabalhar com o SPA para robôs de pesquisa.
- , , 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
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;
}

. .
Obrigado a todos que leram o artigo. Este é apenas um exemplo prático de uso do Nuxt. Em um projeto real, você precisa prosseguir da lógica de negócios, estrutura de dados etc. TP, mas este projeto está longe da realidade.
Na sequência, aumentaremos a funcionalidade do site, porque até agora não é bom.