Vue SPA single-page apps are notoriously SEO unfriendly, but there are solutions. Server-side rendering (SSR) is a common one. SSR is good for SEO (Search Engine Optimization), and time-to-content (or first screen rendering time) has a lot of room for Optimization.

Nuxt.js is a lightweight application framework based on Vue. It can be used to create server-side rendering (SSR) applications, and can also act as a static site engine to generate static site applications, with elegant code structure layering and hot loading features.

Project address: Ming Me’s blog

Initialize the project

Run the create – nuxt – app

Initialize the project with the scaffolding tool create-NuxT-app provided by Nuxt:

$NPX create-nuxt-app < project name >Copy the code

/ / or

$YARN create nuxt-appCopy the code

Project configuration

When the project is created, you will be given some configuration choices, which you can choose according to your needs.

Project running

After running, it will install all dependencies, and the next step is to start the project:

$ cd <project-name>
$ yarn dev
Copy the code

In a browser, openhttp://localhost:3000

The directory structure

.├ ─ assets // For organizing uncompiled Static Resources ├─ Components // For organizing application Voe.js Component ├─ layouts // For organizing application Layout Component ├─ middleware // For storing application middleware ├─ node_modules ├─ Pages for Organizing application Route & View ├─ Plugins ├─ Static // For Storing application ├─ Store // State Management ├─ Nuxt.config.js // Config file ├─ package.json ├─ Jsconfig.json ├─ Stylelint.config.js ├─ readme.md ├─ yarnCopy the code

Project development

After the project starts, we can move on to the development phase.

Create the page

Create page file in Pages:

└── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─Copy the code

Nuxt.js presets various configurations needed to develop server-side rendered applications using vue.js. So there is no need to install vue-Router, it will automatically generate the route configuration of vue-Router module based on the Pages directory structure. It is recommended that the

tag be used for routing between pages, which is the same as the

tag. The directory structure created above will generate the corresponding routing configuration table:

router: {
  routes: [{name: 'article'.path: '/article'.component: 'pages/article/index.vue'
    },
    {
      name: "article-category"
      path: "/article/:category".component: 'pages/article/_category/index.vue'}, {name: "article-detail-articleId"
      path: "/article/detail/:articleId".component: 'pages/article/detail/_articleId.vue'}}]Copy the code

Component part

Components are divided into base, Framework, and Page directories:

Components / ├ ─ ─ base basic component ├ ─ ─ framework layout related components └ ─ ─ page/components under various pages ├ ─ ─ home └ ─ ─...Copy the code

Note here that when developing VUE SPA applications we sometimes put the page components under Pages. I put all the components under pages under Components, because the nuxt.js framework reads all the.vue files in the Pages directory and automatically generates the corresponding routing configuration.

Storage of resources

There are two directories for storing resources: static and assets

Static: A static file used to hold an application that does not call Webpack for build and compile processing. When the server starts, files in this directory are mapped to the root/of the application. For example: /static/banner.png maps to /banner.png

Assets: Used to organize uncompiled static resources such as LESS, SASS, or JS.

The alias

The alias directory
~ or @ srcDir
~ ~ or @ @ rootDir

For ease of reference, nuxt provides two aliases. If you need to import assets or static directories, use ~/assets/your_image. PNG and ~/static/your_image.png.

Global style

Here I use LESS preprocessor language, install:

$ yarn add less less-loader -D
Copy the code

Create.less in assets/ CSS/and import it with a file:

// assets/css/index.less

@import './normalize.less';
@import './reset.less';
@import './variables.less';
@import './common.less'
Copy the code

Introduced in nuxt.config.js

export default{...css: ['~/assets/css/index.less'],... }Copy the code

LESS global variable

When using pre-processing languages, we will definitely use variables to manage colors, font sizes, and so on.

Start by defining the variable file variable.less

/* ===== Theme color configuration ===== */
@colorPrimary: #6bc30d;
@colorAssist: #2db7f5;
@colorSuccess: #67c23a;
@colorWarning: #e6a23c;
@colorError: #f56c6c;
@colorInfo: # 909399;
Copy the code

Installation:

$ yarn add @nuxtjs/style-resources -D
Copy the code

Add configuration to nuxt.config.js:

export default{...modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios'.'@nuxtjs/style-resources',].styleResources: {
    // your settings here
    // sass: [],
    // scss: [],
    // stylus: [],
    less: ['~/assets/css/variables.less'],},... }Copy the code

Layout layouts

My blog is organized in the following ways:

Here I created three layout components:

Layouts / ├ ─ ─ admin. Vue / / above the fourth ├ ─ ─ the default. The vue / / above the first and third contains only nav and footer └ ─ ─ the user. The vue / / above the secondCopy the code

Admin. vue: layout of the background management module user.vue: layout of the personal center module default.vue: default layout

In default.vue, for example, I put the navigation and footer into a component AppLayout:

<! -- layouts/default.vue -->

<template>
  <app-layout>
    <nuxt />
  </app-layout>
</template>

<script>
import AppLayout from '@/components/framework/AppLayout/AppLayout'

export default {
  name: 'AppLayoutDefault'.components: {
    AppLayout
  }
}
</script>
Copy the code

Then use:

<! -- pages/index.vue -->

<template>
  <! -- Your template -->
</template>
<script>
  export default {
    layout: 'default'
    // Specify the layout. If not, use the default: layouts/default.vue
    // I don't have to specify it.
  }
</script>
Copy the code


is not the same as
. Nuxt-link takes a vue-Router route, that is, the page is a single page and the browser does not redirect. The
tag goes window.location.href, and every time the a tag is clicked, the page is rendered on the server side.

Global filter

In the plugins/ directory, create filters.js. Let’s say we want to format the time:

Day.js: a lightweight JavaScript library that handles times and dates

$ yarn add dayjs
Copy the code
import Vue from 'vue'
import dayjs from 'dayjs'
// Time formatting
export function dateFormatFilter(date, fmt{
  if(! date) {return ' '
  } else {
    return dayjs(date).format(fmt)
  }
}
const filters = {
  dateFormatFilter
}
Object.keys(filters).forEach((key) = > {
  Vue.filter(key, filters[key])
})
export default filters
Copy the code

Then, in nuxt.config.js,

export default{...plugins: ['~/plugins/filters.js']... }Copy the code

Custom instruction

In the plugins/directive/focus directory, add index.js:

import Vue from 'vue';
const focus = Vue.directive('focus', {
  inserted(el){ el.focus(); }});export default focus;
Copy the code

As with global filters, custom directives need to be configured in nuxt.config.js:

export default{...plugins: [
    '~/plugins/filters.js', {src: '~/plugins/directive/focus/index.js'.ssr: false},],... }Copy the code

The head configuration of SEO

Set the header label for the current page by using the head method.


<template>
  <h1>{{ title }}</h1></template>

<script>
  export default{...head() {
      return {
        title: 'My blog'.meta: [{hid: 'description'.name: 'description'.content: 'My custom description'}]}}}</script>
Copy the code

Note: It is recommended that the HID key be used to assign a unique identifier to the meta tag in the child component to avoid duplication of the meta tag in the parent component.

If you have a large number of pages, each page needs to write a head object, which can be a bit tedious. Using nuxt’s plugin mechanism, this can be encapsulated as a function and injected into every page:

// plugins/head.js
import Vue from 'vue'

Vue.mixin({
  methods: {
    $seo(title, content) {
      return {
        title,
        meta: [{
          hid: 'description'.name: 'description',
          content
        }]
      }
    }
  }
})
Copy the code

Add configuration to nuxt.config.js:

export default{...plugins: [
    '~/plugins/filters.js', {src: '~/plugins/directive/focus/index.js'.ssr: false },
    '~/plugins/head.js'],... }Copy the code

Use:

head() {
    return this.$seo(this.detail.title, this.detail.summary)
}
Copy the code

Axios requests data

Nuxt.config.js is already configured for the request data, which is already selected when initializing the project and does not need to be installed separately.

export default{...modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios'. ] . }Copy the code

$get = this.$axios.$get = this.$axios. But typically we wrap axios around some data or error messages. Create axios.js and api-repositories.js in the plugins directory. Here are some of my simple configurations:

// plugins/axios.js
import qs from 'qs'

export default function(ctx) {
  const { $axios, store, app } = ctx
  // $axios.defaults.timeout = 0;
  $axios.transformRequest = [
    (data, header) = > {
      if (header['Content-Type'] && header['Content-Type'].includes('json')) {
        return JSON.stringify(data)
      }
      return qs.stringify(data, { arrayFormat: 'repeat' })
    }
  ]

  $axios.onRequest((config) = > {
    const token = store.getters.token
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    // If it is a GET request, the parameters are serialized
    if (config.method === 'get') {
      config.paramsSerializer = function(params) {
        return qs.stringify(params, { arrayFormat: 'repeat' }) // Params is an array type such as arr=[1,2], then arr=1&arr=2}}return config
  })

  $axios.onRequestError((error) = > {
    console.log('onRequestError', error)
  })

  $axios.onResponse((res) = > {
    // ["data", "status", "statusText", "headers", "config", "request"]
    // Res.data is returned if the code returned by the back end is normal
    if (res && res.data) {
      if (res.headers['content-type'= = ='text/html') {
        return res
      }
      if (res.data.code === 'success') {
        return res
      } else {
        return Promise.reject(res.data)
      }
    }
  })

  $axios.onResponseError((error) = > {
    console.log('onResponseError', error)
  })

  $axios.onError((error) = > {
    console.log('onError', error)

    if (error && error.message.indexOf('401') > 1) {
      app.$toast.error('Login expired, please log in again! ')
      sessionStorage.clear()
      store.dispatch('changeUserInfo'.null)
      store.dispatch('changeToken'.' ')}else {
      app.$toast.show(error.message)
    }
  })
}
Copy the code
// plugins/api-repositories.js
export default ({ $axios }, inject) => {
  const repositories = {
    GetCategory: (params, options) = > $axios.get('/categories', params, options),
    PostCategory: (params, options) = > $axios.post('/categories', params, options),
    PutCategory: (params, options) = > $axios.put(`/categories/${params.categoryId}`, params, options),
    DeleteCategory: (params, options) = > $axios.delete(`/categories/${params.categoryId}`, params, options)
    ...
  }

  inject('myApi', repositories)
}
Copy the code

Then add the configuration to nuxt.config.js:

export default{...plugins: [{...src: '~/plugins/axios.js'.ssr: true },
    { src: '~/plugins/api-repositories.js'.ssr: true},]./* ** Axios module configuration ** See https://axios.nuxtjs.org/options */
  axios: {
    baseURL: 'http://localhost:5000/'}},Copy the code

This can be used directly in the page:

this.$myApi.GetCategory()
Copy the code

The proxy agent

Using proxy to solve cross-domain problems:

$ yarn add @nuxtjs/proxy
Copy the code

To add a configuration to nuxt.config.js, here is my configuration:

export default{...modules: [...'@nuxtjs/proxy'. ] .axios: {
    proxy: true.headers: {
      'Access-Control-Allow-Origin': The '*'.'X-Requested-With': 'XMLHttpRequest'.'Content-Type': 'application/json; charset=UTF-8'
    },
    prefix: '/api'.credentials: true
  },
  /* ** Configure the proxy */
  proxy: {
    '/api': {
      target: process.env.NODE_ENV === 'development' ? 'http://localhost:5000/' : 'http://localhost:5000/'.changeOrigin: true.pathRewrite: {
        '^/api': ' '}},'/douban/': {
      target: 'http://api.douban.com/v2'.changeOrigin: true.pathRewrite: {
        '^/douban': ' '}},... }},Copy the code

In single-page development, packaged publishing also requires an Nginx agent to implement cross-domain implementation. In NUxT, after packaged publishing is online, the request is initiated on the server side. There is no cross-domain problem, so there is no need to do an additional Nginx agent.

asyncData

This method is one of Nuxt’s big selling points, and the asyncData method is called before each load of a component (limited to page components). It can be called before the server or route updates, and that’s where the server rendering power lies.

Note: Since the asyncData method is called before the component is initialized, there is no way to refer to the component instance object through this within the method.

Also, when asyncData is executed on the server, there are no Document and Window objects.

The first parameter of asyncData is set as the context object of the current page. The asyncData method can be used to get data. Nuxt.js will return the data returned by the data fusion component data method of asyncData to the current component.

export default {
  asyncData (ctx) {
    ctx.app / / root instance
    ctx.route // Route instance
    ctx.params  // Route parameters
    ctx.query  // The argument after the route question mark
    ctx.error   // Error handling}}Copy the code

Server render:

export default {
  data () {
    return { categoryList: []}; },async asyncData({ app }) {
    const res = await app.$myApi.GetCategory();
    return {
      categoryList: res.result.list }; }},Copy the code

AsyncData rendering error

When using asyncData, the page may not render due to server error or API error. In this case, we need to do something about it. Nuxt provides the context.error method for error handling, which is called in asyncData to jump to the error page.

export default {
    async asyncData({ app, error}) {
    app.$myApi.GetCategory()
      .then(res= > {
        return { categoryList: res.result.list }
      })
      .catch(e= > {
        error({ statusCode: 500.message: 'Server error ~'}})})},Copy the code

The default error page is displayed when an exception occurs. You can customize the error page by using /layout/error.vue.

Context. Error must have arguments like {statusCode: 500, message: ‘the server opened a small error ~’}, and statusCode must be an HTTP statusCode

For convenience, create ctx-inject. Js in the plugins directory:

// plugins/ctx-inject.js
export default (ctx, inject) => {
  ctx.$errorHandler = (err) = > {
    try {
      const res = err.data
      if (res) {
        // the nuxt error page can only recognize the HTTP statusCode, so the statusCode is transmitted 500, indicating that the server is abnormal.
        ctx.error({ statusCode: 500.message: res.resultInfo })
      } else {
        ctx.error({ statusCode: 500.message: 'Server error ~'}}})catch {
      ctx.error({ statusCode: 500.message: 'Server error ~'})}}}Copy the code

Then add the configuration in nuxt.config.js:

export default{...plugins: [...'~/plugins/ctx-inject.js'. ] . }Copy the code

Use:

export default {
  data() {
    return { categoryList: []}},async asyncData(ctx) {
    const { app } = ctx
    // Use try catch to catch all exceptions
    try {
      const res = await app.$myApi.GetCategory()
      return {
        categoryList: res.result.list,
      }
    } catch (err) {
      ctx.$errorHandler(err)
    }
  },
}
Copy the code

fetch

The FETCH method is used to populate the application’s store data before rendering the page, similar to the asyncData method except that it does not set the component’s data. It is called before each load of the component (on the server or before switching to the destination route). Like asyncData, the first parameter is the context object of the page, and you cannot use this internally to get the component instance.

<template>.</template>

<script>
  export default {
    async fetch({ app, store, params }) {
      let res = await app.$myApi.GetCategory()
      store.commit('setCategory', res.result.list)
    }
  }
</script>
Copy the code

store

To use state management in NUxT, you simply create files in the store/ directory.

Store / ├ ─ ─ actions. Js ├ ─ ─ getters. Js ├ ─ ─ index. The js ├ ─ ─ mutations. Js └ ─ ─ state. JsCopy the code
// store/actions.js
const actions = {
  changeToken({ commit }, token) {
    commit('setToken', token)
  },
  ...
}
export default actions



// store/getters.js
export const token = (state) = > state.token
export const userInfo = (state) = > state.userInfo
...




// store/mutations.js
const mutations = {
  setToken(state, token) {
    state.token = token
  },
  ...
}
export default mutations



// store/state.js
const state = () = > ({
  token: ' '.userInfo: null. })export default state




// store/index.js
import state from './state'
import * as getters from './getters'
import actions from './actions'
import mutations from './mutations'

export default {
  state,
  getters,
  actions,
  mutations
}
Copy the code

Regardless of which mode you use, the value of your state should always be function to avoid returning a reference type, which can cause multiple instances to interact.

Build the deployment

Once developed, it is ready to be packaged and deployed, generally tested locally:

$ yarn build
$ yarn start
Copy the code

The cloud server then installs the Node environment and PM2.

To add the Pm2 configuration, in the server/ directory, create the pm2.config.json file:

{
  "apps": [{"name": "my-blog"."script": "./server/index.js"."instances": 0."watch": false."exec_mode": "cluster_mode"}}]Copy the code

Then, configure the command scripts in package.json:

{
  "scripts": {..."pm2": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json",}}Copy the code

Upload.nuxt, static, package.json, nuxt.config.js, yarn.lock or package.lock in our project to the server. Go to the uploaded server directory and install dependencies:

$ yarn install
Copy the code

Then, run:

$ npm run pm2
Copy the code

After setting the server to open port 3000, you can access it through the port. Adding a port number after it is not always appropriate, you also need to use the nginx proxy to default port 80(HTTP) or 433(HTTPS).

Record a small problem: there is no problem with port 3000 and the project starts normally, but it cannot be accessed through http://60.***.***.110:3000. In nuxt.config.js add:

{... server: { port:3000,
    host: '0.0. 0. 0'}}Copy the code

Restart the project.