🏃 Woman: light up your Vue technology stack. Here comes the practice notes of Wanzi Nuxt.js~

preface

As a vue developer, if you don't have this framework, your vue technology stack hasn't been lit yet.

What is Nuxt.js

Nuxt.js official introduction:

Nuxt.js is a general application framework based on Vue.js.
Through the abstract organization of client / server infrastructure, Nuxt.js focuses on the UI rendering of applications.
Our goal is to create a flexible application framework based on which you can initialize the infrastructure code of a new project or use Nuxt.js in an existing Node.js project.
Nuxt.js presets various configurations required for developing server-side rendering applications using Vue.js.


If you are familiar with the use of Vue.js, you can start Nuxt.js soon. The development experience is not very different from Vue.js, which is equivalent to extending some configurations for Vue.js. Of course, if you have a foundation for Node.js, that's great.

What problems does Nuxt.js solve

Now, most of Vue.js are used in single page applications. With the development of technology, single page applications are not enough to meet the needs. And some disadvantages also become a common problem of single page application. Single page application will load all files when visiting. The first screen access needs to wait for a period of time, which is often called white screen. The other is the known SEO optimization problem.

Nuxt.js is just to solve these problems. If your website is a project that favors the community and needs search engine to provide traffic, then it is just right.

My first Nuxt.js project

In my spare time, I also use Nuxt.js to imitate the gold digger web site:

nuxt-juejin-project is a learning project that uses Nuxt.js to copy nuxt. It mainly uses nuxt + koa + vuex + Axios + element UI. All data of the project is synchronized with nuggets, because the interface is forwarded through koa as the middle layer. The main page data is rendered by the server.

In a few days after the completion of the project, I will sort out the notes and add some common technical points. Finally, I have this article, hoping to help the learning partners.

There are some screenshots in the project introduction. If jio is OK, please come to star 😜 ~

Project address: https://github.com/ChanWahFung/nuxt-juejin-project

Basic application and configuration

Please refer to the guidelines on the official website for the construction of the project. It's hard to run a project without you. I won't go into details here.

🏃 Running https://www.nuxtjs.cn/guide/installation

For the configuration of the project, I choose:

  • Server: Koa
  • UI framework: Element UI
  • Test framework: None
  • Nuxt mode: Universal
  • Use integrated Axios
  • Using EsLint

context

context is an extra object provided from Nuxt, which is used in "asyncData", "plugins", "middlewars", "modules" and "store/nuxtServerInit" and other special Nuxt life cycle areas.

Therefore, to use Nuxt.js, we must be familiar with the available properties of the object.

context official document description stamp here https://www.nuxtjs.cn/api/context

Here are some important and commonly used attributes in practical application:

app

App is the most important property in context. Just like this in Vue, global methods and properties will be attached to it. Because of the particularity of server-side rendering, many of the life cycles provided by Nuxt are run on the server-side, that is to say, they will precede the creation of Vue instances. Therefore, in these lifecycles, we cannot get methods and properties on the instance through this. Using app can make up for this. Generally, we will inject the global method into this and app at the same time, use app to access this method in the service life cycle, and use this in the client to ensure the sharing of methods.

for instance:

Assuming that $axios has been injected at the same time, generally, the main data is requested to be rendered by the server in advance through asyncData (request is initiated in this life cycle, and the acquired data is handed over to the server to be spliced into html for return), while the secondary data is requested by the client's mounted.

export default {
  async asyncData({ app }) {
    // List data
    let list = await app.$axios.getIndexList({
      pageNum: 1,
      pageSize: 20
    }).then(res => res.s === 1 ? res.d : [])
    return {
      list
    }
  },
  data() {
    return {
      list: [],
      categories: []
    }
  },
  async mounted() {
    // classification
    let res = await this.$axios.getCategories()
    if (res.s  === 1) {
      this.categories = res.d
    }
  }
}

store

Store is an instance of Vuex.Store. At runtime, Nuxt.js will try to find the store directory under the application root directory. If the directory exists, it will add the module file to the build configuration.

So we just need to create the module js file in the root store, and then we can use it.

/store/index.js :

export const state = () => ({
  list: []
})

export const mutations = {
  updateList(state, payload){
    state.list = payload
  }
}

And Nuxt.js will help us inject the store at the same time. Finally, we can use this in the component:

export default {
  async asyncData({ app, store }) {
    let list = await app.$axios.getIndexList({
      pageNum: 1,
      pageSize: 20
    }).then(res => res.s === 1 ? res.d : [])
    // Server use
    store.commit('updateList', list)
    return {
      list
    }
  },
  methods: {
    updateList(list) {
      // You can use the client, of course, you can also use the auxiliary function maprotations to complete
      this.$store.commit('updateList', list)
    }
  }
}

In order to understand the process of store injection, I went through the source code of. nuxt/index.js (. The nuxt directory is generated automatically by Nuxt.js when the build is running), and I probably knew the process. First, in. nuxt/store.js, we do a series of processing to the store module file, and expose the createStore method. Then in. nuxt/index.js, the createApp method will inject:

import { createStore } from './store.js'

async function createApp (ssrContext) {
  const store = createStore(ssrContext)
  // ...
  // here we inject the router and store to all child components,
  // making them available everywhere as `this.$router` and `this.$store`.
  // Inject into this
  const app = {
    store
    // ...
  }
  // ...
  // Set context to app.context
  // Inject into context
  await setContext(app, {
    store
    // ...
  })
  // ...
  return {
    store,
    app,
    router
  }
}

In addition, I also found that Nuxt.js will mount a plugin for Nuxt.js through the inject method (plugin is the main way to mount the global method, which will be discussed later, and can be ignored first). That is to say, in the store, we can access the global method through this:

export const mutations = {
  updateList(state, payload){
    console.log(this.$axios)
    state.list = payload
  }
}

params,query

Params and query are aliases of route.params and route.query respectively. They are all objects with routing parameters and are easy to use. There's nothing to say about this. It's over.

export default {
  async asyncData({ app, params }) {
    let list = await app.$axios.getIndexList({
      id: params.id,
      pageNum: 1,
      pageSize: 20
    }).then(res => res.s === 1 ? res.d : [])
    return {
      list
    }
  }
}

redirect

This method redirects user requests to another route, which is usually used in permission authentication. Usage: redirect(params). Params parameters include status (status code, 302 by default), path (routing path), query (parameter). Status and query are optional. Of course, if you just redirect the route, you can pass in the path string, just like redirect('/index').

for instance:

Suppose we now have a routing middleware to verify the login identity. The logic is that if the identity does not expire, nothing will be done. If the identity expires, it will be redirected to the login page.

export default function ({ redirect }) {
  // ...
  if (!token) {
    redirect({
      path: '/login',
      query: {
        isExpires: 1
      }
    })
  }
}

error

This method jumps to the error page. Usage: error(params), params parameter should contain statusCode and message fields. In the actual scene, there are always some unreasonable operations, so the page cannot show the really desired effect, so it is necessary to use this method for error prompt.

for instance:

The request data of the tag details page depends on query.name. When query.name does not exist, the request cannot return the available data. At this time, jump to the error page

export default {
  async asyncData({ app, query, error }) {
    const tagInfo = await app.$api.getTagDetail({
      tagName: encodeURIComponent(query.name)
    }).then(res => {
      if (res.s === 1) {
        return res.d
      } else {
        error({
          statusCode: 404,
          message: 'Label does not exist'
        })
        return
      }
    })
    return {
      tagInfo
    }
  }
}

Nuxt common page lifecycle

asyncData

You may want to retrieve and render data on the server side. Nuxt.js adds the asyncData method to enable you to retrieve data asynchronously before rendering components.

asyncData is the most commonly used and important life cycle, and it is also the key point of server-side rendering. This life cycle is limited to page component calls. The first parameter is context. It is called before the initialization of components, and runs in the server environment. So in the asyncData life cycle, we can't refer to the current Vue instance through this, and there are no window objects and document objects, which we need to pay attention to.

In general, asyncData will pre request the main page data, and the acquired data will be spliced into html by the server and returned to the front end for rendering, so as to improve the loading speed of the first screen and seo optimization.

See the figure below. In the Google debugging tool, you can't see the main data interface to initiate the request, only the returned html document, which proves that the data is rendered in the server.

Finally, the data acquired by the interface needs to be returned:

export default {
  async asyncData({ app }) {
    let list = await app.$axios.getIndexList({
      pageNum: 1,
      pageSize: 20
    }).then(res => res.s === 1 ? res.d : [])
    // Return data
    return {
      list
    }
  },
  data() {
    return {
      list: []
    }
  }
}

It is worth mentioning that asyncData is only executed on the first screen, and other times it is equivalent to created or mounted rendering pages on the client side.

What do you mean? for instance:

Now there are two pages, the first page and the details page, both of which have asyncData set. When entering the home page, asyncData runs on the server. After rendering, click the article to enter the details page. At this time, the asyncData of the details page will not run on the server, but the client initiates a request to obtain data rendering, because the details page is not the first screen. When we refresh the details page, the asyncData of the details page will run on the server. So, don't go into this mistake (ah, isn't it the server-side rendering, how can it still make a request?).

fetch

The fetch method is used to fill the application's state tree data before rendering the page. Similar to the asyncData method, it does not set the component's data.

Looking at the official instructions, you can see that the lifecycle is used to fill the Vuex status tree. Similarly to asyncData, it is called before the component initialization, and the first parameter is context.

In order for the acquisition process to be asynchronous, you need to return a promise, Nuxt.js We will wait for the promise to finish before rendering the component.

export default {
  fetch ({ store, params }) {
    return axios.get('http://my-api/stars')
    .then((res) => {
      store.commit('setStars', res.data)
    })
  }
}

You can also use async or await to simplify the code as follows:

export default {
  async fetch ({ store, params }) {
    let { data } = await axios.get('http://my-api/stars')
    store.commit('setStars', data)
  }
}

But this is not to say that we can only fill the state tree in the fetch, and it can also be done in asyncData.

validate

Nuxt.js You can configure a verification method in the page component corresponding to the dynamic route to verify the validity of the dynamic route parameters.

When verifying the validity of routing parameters, it can help us. The first parameter is context. Different from the above, we can access the methods on the instance this.methods.xxx .

Print this as follows:

The life cycle can return a Boolean. If it is true, it will enter the route. If it is false, it will stop rendering the current page and display the error page:

export default {
  validate({ params, query }) {
    return this.methods.validateParam(params.type)
  },
  methods: {
    validateParam(type){
      let typeWhiteList = ['backend', 'frontend', 'android']
      return typeWhiteList.includes(type)
    }
  }
}

Or return a Promise:

export default {
  validate({ params, query, store }) {
    return new Promise((resolve) => setTimeout(() => resolve()))
  }
}

You can also throw expected or unexpected errors during validation function execution:

export default {
  async validate ({ params, store }) {
    // Trigger internal server 500 error with custom message
    throw new Error('Under Construction!')
  }
}

watchQuery

Listen for parameter string changes and execute component methods when they are changed (asyncData, fetch, validate, layout,...)

watchQuery can set Boolean or Array (default: []). Use the watchQuery property to listen for parameter string changes. If the defined string changes, all component methods (asyncData, fetch, validate, layout,...) will be called. To improve performance, it is disabled by default.

In the search page of nuxt-juejin-project project, I also use this configuration:

<template>
  <div class="search-container">
    <div class="list__header">
      <ul class="list__types">
        <li v-for="item in types" :key="item.title" @click="search({type: item.type})">{{ item.title }}</li>
      </ul>
      <ul class="list__periods">
        <li v-for="item in periods" :key="item.title" @click="search({period: item.period})">{{ item.title }}</li>
      </ul>
    </div>
  </div>
</template>
export default {
  async asyncData({ app, query }) {
    let res = await app.$api.searchList({
      after: 0,
      first: 20,
      type: query.type ? query.type.toUpperCase() : 'ALL',
      keyword: query.keyword,
      period: query.period ? query.period.toUpperCase() : 'ALL'
    }).then(res => res.s == 1 ? res.d : {})
    return {
      pageInfo: res.pageInfo || {},
      searchList: res.edges || []
    }
  },
  watchQuery: ['keyword', 'type', 'period'],
  methods: {
    search(item) {
      // Update route parameters, trigger watchQuery, execute asyncData to retrieve data
      this.$router.push({
        name: 'search',
        query: {
          type: item.type || this.type,
          keyword: this.keyword,
          period: item.period || this.period
        }
      })
    }
  }
}

The advantage of using watchQuery is that when we use the browser Back button or forward button, the page data will refresh because the parameter string has changed.

head

Nuxt.js The header tag and html attribute of the application are updated with Vue meta.

Use the head method to set the header label of the current page. In this method, you can get the data of the component through this. In addition to good-looking, the correct set of meta tags, but also conducive to the search engine search page, seo optimization. Generally, description and keyword are set.

title:

meta:

export default {
  head () {
    return {
      title: this.articInfo.title,
      meta: [
        { hid: 'description', name: 'description', content: this.articInfo.content }
      ]
    }
  }
}

In order to avoid the phenomenon that the meta tag in the child component cannot correctly cover the same tag in the parent component, it is recommended to use the hid key to assign a unique identification number to the meta tag.

At nuxt.config.js We can also set the global head:

module.exports = {
  head: {
    title: 'Nuggets',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover' },
      { name: 'referrer', content: 'never'},
      { hid: 'keywords', name: 'keywords', content: 'Nuggets,rare earth,Vue.js,Wechat applet,Kotlin,RxJava,React Native,Wireshark,agile development ,Bootstrap,OKHttp,regular expression ,WebGL,Webpack,Docker,MVVM'},
      { hid: 'description', name: 'description', content: 'Nuggets is a community that helps developers grow and is for developers Hacker News,For designers Designer News,And for product managers Medium. The technical articles of nuggets are compiled by technology bulls and geeks gathered on rare earth to select the best dry goods for you, including: Android,iOS,Front end, back end, etc. Users can find the headlines of the technology world here every day. At the same time, there are also boiling points, translation plans, offline activities, columns, etc. Even if you are GitHub,StackOverflow,Open source users in China, we believe you can also get something here.'}
    ],
  }
}

supplement

Here is the order of these lifecycles, which may help us in some cases.

validate  =>  asyncData  =>  fetch  =>  head

Configure boot port

Both of the following can be configured with boot ports, but I prefer the first one in nuxt.config.js Configuration, which is more in line with normal logic.

The first

nuxt.config.js :

module.exports = {
  server: {
    port: 8000,
    host: '127.0.0.1'
  }
}

The second

package.json :

"config": {
  "nuxt": {
    "port": "8000",
    "host": "127.0.0.1"
  }
},

Load external resources

Global configuration

nuxt.config.js :

module.exports = {
  head: {
    link: [
      { rel: 'stylesheet', href: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/atom-one-light.min.css' },
    ],
    script: [
      { src: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/highlight.min.js' }
    ]
  }
}

Component configuration

Components can be configured in head, which can accept object or function. The official example uses the object type, and the function type also takes effect.

export default {
  head () {
    return {
      link: [
        { rel: 'stylesheet', href: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/atom-one-light.min.css' },
      ],
      script: [
        { src: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/highlight.min.js' }
      ]
    }
  }
}

environment variable

nuxt.config.js Provide env options to configure environment variables. But I have tried to create the. Env file management environment variable in the root directory before and found it is invalid.

Create environment variables

nuxt.config.js :

module.exports = {
  env: {
    baseUrl: process.env.NODE_ENV === 'production' ? 'http://test.com' : 'http://127.0.0.1:8000'
  },
}

In the above configuration, we create a baseUrl environment variable, which can be set by process.env.NODE_ENV judges the environment to match the corresponding address

Using environment variables

We can use the baseUrl variable in two ways:

  1. Through process.env.baseUrl
  2. Through context.env.baseUrl

For example, we can use it to configure a custom instance of axios.

/plugins/axios.js:

export default function (context) {
	$axios.defaults.baseURL = process.env.baseUrl
	// Or$ axios.defaults.baseURL = context.env.baseUrl
	$axios.defaults.timeout = 30000
	$axios.interceptors.request.use(config => {
		return config
	})
	$axios.interceptors.response.use(response => {
		return response.data
	})
}

plugins

As the main way of global injection, plugins must be mastered in some ways. Sometimes you want to use some function or property value in the whole application. At this time, you need to inject them into Vue instance (client), context (server-side) or even store(Vuex).

plugin function parameters

plugin generally exposes a function, which receives two parameters, context and inject

Context: context object, which stores many useful properties. For example, the common app attribute contains the root instance of Vue of all plug-ins. For example: when using axios, you can get $axios directly through context.app.$axios.

Inject: this method can inject plugin into context, Vue instance and Vuex at the same time.

For example:

export default function (context, inject) {}

Inject Vue instance

definition

plugins/vue-inject.js :

import Vue from 'vue'

Vue.prototype.$myInjectedFunction = string => console.log('This is an example', string)

use

nuxt.config.js :

export default {
  plugins: ['~/plugins/vue-inject.js']
}

This allows the function to be used in all Vue components

export default {
  mounted() {
      this.$myInjectedFunction('test')
  }
}

Inject context

context injection is similar to other vue applications.

definition

plugins/ctx-inject.js :

export default ({ app }) => {
  app.myInjectedFunction = string => console.log('Okay, another function', string)
}

use

nuxt.config.js :

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

Now, as long as you get context, you can use this function (for example, in asyncData and fetch)

export default {
  asyncData(context) {
    context.app.myInjectedFunction('ctx!')
  }
}

Simultaneous injection

If you need to inject in context, Vue instance or even Vuex at the same time, you can use the inject method, which is the second parameter of the plugin export function. By default, $is prefixed to the method name.

definition

plugins/combined-inject.js :

export default ({ app }, inject) => {
  inject('myInjectedFunction', string => console.log('That was easy!', string))
}

use

nuxt.config.js :

export default {
  plugins: ['~/plugins/combined-inject.js']
}

Now you can call the myInjectedFunction method in context, this in the Vue instance, or this in the actions / transitions method of Vuex

export default {
  mounted() {
    this.$myInjectedFunction('works in mounted')
  },
  asyncData(context) {
    context.app.$myInjectedFunction('works with context')
  }
}

store/index.js :

export const state = () => ({
  someValue: ''
})

export const mutations = {
  changeSomeValue(state, newValue) {
    this.$myInjectedFunction('accessible in mutations')
    state.someValue = newValue
  }
}

export const actions = {
  setSomeValueToWhatever({ commit }) {
    this.$myInjectedFunction('accessible in actions')
    const newValue = 'whatever'
    commit('changeSomeValue', newValue)
  }
}

plugin calls each other

When plugin depends on other plugin calls, we can access context to obtain it, provided that plugin needs to use context injection.

For example: now there is a plugin for request request, and another plugin needs to call request

plugins/request.js :

export default ({ app: { $axios } }, inject) => {
  inject('request', {
    get (url, params) {
      return $axios({
        method: 'get',
        url,
        params
      })
    }
  })
}

plugins/api.js :

export default ({ app: { $request } }, inject) => {
  inject('api', {
    getIndexList(params) {
      return $request.get('/list/indexList', params)
    }
  })
}

It is worth mentioning that you should pay attention to the order when injecting plugin. As for the above example, the injection order of request should be before the api

module.exports = {
  plugins: [
    './plugins/axios.js',
    './plugins/request.js',
    './plugins/api.js',
  ]
}

Routing configuration

In Nuxt.js, the route is automatically generated based on the file structure without configuration. The automatically generated routing configuration can be viewed in. nuxt/router.js.

Dynamic routing

This is how dynamic routing is configured in Vue

const router = new VueRouter({
  routes: [
    {
      path: '/users/:id',
      name: 'user',
      component: User
    }
  ]
})

In Nuxt.js, you need to create a Vue file or directory with the corresponding underscore as the prefix

Take the following directory for example:

pages/
--| users/
-----| _id.vue
--| index.vue

The automatically generated route configuration is as follows:

router:{
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    }
  ]
}

Nested Route

Take the following directory as an example, we need the vue file of the first level page and the folder with the same name as the file (for storing the sub pages)

pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue

The automatically generated route configuration is as follows:

router: {
  routes: [
    {
      path: '/users',
      component: 'pages/users.vue',
      children: [
        {
          path: '',
          component: 'pages/users/index.vue',
          name: 'users'
        },
        {
          path: ':id',
          component: 'pages/users/_id.vue',
          name: 'users-id'
        }
      ]
    }
  ]
}

Then use nuxt child to display the sub page in the first level page, just like using router view

<template>
  <div>
    <nuxt-child></nuxt-child>
  </div>
</template>

Custom configuration

In addition to generating routes based on the file structure, you can also modify the router options in the nuxt.config.js file to customize them. These configurations will be added to the route configuration of Nuxt.js.

The following example is the configuration of adding redirection to a route:

module.exports = {
  router: {
    extendRoutes (routes, resolve) {
      routes.push({
        path: '/',
        redirect: {
          name: 'timeline-title'
        }
      })
    }
  }
}

axios

install

Nuxt has integrated @ nuxtjs/axios for us. If you choose axios when creating a project, this step can be ignored.

npm i @nuxtjs/axios --save

nuxt.config.js :

module.exports = {
  modules: [
    '@nuxtjs/axios'
  ],
}

SSR uses Axios

The server side obtains and renders the data. asyncData method can obtain the data asynchronously before rendering the component, and return the acquired data to the current component.

export default {
  async asyncData(context) {
    let data = await context.app.$axios.get("/test")
    return {
      list: data
    };
  },
  data() {
    return {
      list: []
    }
  }
}

Non SSR uses Axios

In this way, as we usually do, access this to call

export default {
  data() {
    return {
      list: []
    }
  },
  async created() {
    let data = await this.$axios.get("/test")
    this.list = data
  },
}

Custom configuration Axios

Most of the time, we need to do custom configuration (baseUrl, interceptor) for axios, which can be introduced by configuring plugins.

definition

/plugins/axios.js :

export default function({ app: { $axios } }) {
  $axios.defaults.baseURL = 'http://127.0.0.1:8000/'
  $axios.interceptors.request.use(config => {
    return config
  })
  $axios.interceptors.response.use(response => {
    if (/^[4|5]/.test(response.status)) {
      return Promise.reject(response.statusText)
    }
    return response.data
  })
}

use

nuxt.config.js :

module.exports = {
  plugins: [
    './plugins/axios.js'
  ],
}

When finished, use it the same way as above.

css preprocessor

Take scss as an example

install

npm i node-sass sass-loader scss-loader --save--dev

use

No configuration required, directly used in the template

<style lang="scss" scoped>
.box{
    color: $theme;
}
</style>

Global style

When writing layout styles, there will be many common styles. At this time, we can extract these styles and only need to add a class name when using them.

definition

global.scss :

.shadow{
  box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
}
.ellipsis{
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
.main{
  width: 960px;
  margin: 0 auto;
  margin-top: 20px;
}

use

nuxt.config.js :

module.exports = {
  css: [
    '~/assets/scss/global.scss'
  ],
}

global variable

Inject variables and mixin s into the page and do not import them every time. You can use @ nuxtjs / style resources to implement them.

install

npm i @nuxtjs/style-resources --save--dev

definition

/assets/scss/variable.scss:

$theme: #007fff;
$success: #6cbd45;
$success-2: #74ca46;

use

nuxt.config.js :

module.exports = {
  modules: [
    '@nuxtjs/style-resources'
  ],
  styleResources: {
    scss: [
      './assets/scss/variable.scss'
    ]
  },
}

Element UI custom theme

definition

/assets/scss/element-variables.scss :

/* Change theme color variable */
/* $theme Define and use in the scss file above */
$--color-primary: $theme;

/* Change icon font path variable, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';

/* Component styles introduced on demand */
@import "~element-ui/packages/theme-chalk/src/select";
@import "~element-ui/packages/theme-chalk/src/option";
@import "~element-ui/packages/theme-chalk/src/input";
@import "~element-ui/packages/theme-chalk/src/button";
@import "~element-ui/packages/theme-chalk/src/notification";
@import "~element-ui/packages/theme-chalk/src/message";

use

nuxt.config.js :

module.exports = {
  modules: [
    '@nuxtjs/style-resources'
  ],
  styleResources: {
    scss: [
      /*
      * Note the order of use here, because variables defined in variable.scss are used in element-variables.scss
      * If the order is reversed, it will cause the variable to fail to find an error when starting the compilation
      */
      '~/assets/scss/variable.scss',
      '~/assets/scss/element-variables.scss'
    ]
  },
}

Another way to use it is plugin

import Vue from 'vue'
import myComponentsInstall from '~/components/myComponentsInstall'
import eleComponentsInstall from '~/components/eleComponentsInstall'
import '~/assets/scss/element-variables.scss' // elementUI custom theme color

Vue.use(myComponentsInstall)
Vue.use(eleComponentsInstall)

Front end technology point

asyncData request parallelism

You should be able to feel the importance of asyncData here. For this kind of life cycle that is often used, some detailed modifications are particularly important. Generally, there is not only one request initiated in asyncData, but also many:

export default {
  async asyncData({ app }) {
    // Article list
    let indexData = await app.$api.getIndexList({
      first: 20,
      order: 'POPULAR',
      category: 1
    }).then(res => res.s == 1 ? res.d : {})
    // Recommended author
    let recommendAuthors = await app.$api.getRecommendAuthor({ 
      limit: 5
    }).then(res => res.s == 1 ? res.d : [])
    // Recommended Brochure
    let recommendBooks = await app.$api.getRecommendBook().then(res => res.s === 1 ? res.d.data : [])
    return {
      indexData,
      recommendAuthors,
      recommendBooks
    }
  }
}

The above operation seems to be OK, but there is actually a detail that can be optimized. Now, we all know that async/await will desynchronize asynchronous tasks. Before the last asynchronous task is finished, the next asynchronous task is waiting. In this way, you need to wait for three asynchronous tasks, assuming that these requests take one second, that is, the page will wait at least three seconds before the content appears. We wanted to use server-side rendering to optimize the first screen, but now we have to wait for the request and slow down the page rendering, which is not worth the loss.

The best solution is to send multiple requests at the same time. Maybe smart partners have thought of promise.all. Yes, using promise. All to send these requests in parallel can solve the above problems. Promise.all takes a promise array as a parameter and returns a result array when all promises are successful. The final time will be based on the longest promise, so the original three second time can be reduced to one second. It should be noted that if one of the requests fails, the value of the first rejected failure status will be returned, resulting in no data acquisition. When the project encapsulates the basic request, I have done the catch error processing, so make sure that the request will not be rejected.

export default {
  asyncData() {
    // Array deconstruction to get the data of the corresponding request
    let [indexData, recommendAuthors, recommendBooks] = await Promise.all([
      // Article list
      app.$api.getIndexList({
        first: 20,
        order: 'POPULAR',
        category: 1
      }).then(res => res.s == 1 ? res.d : {}),
      // Recommended author
      app.$api.getRecommendAuthor({ 
        limit: 5
      }).then(res => res.s == 1 ? res.d : []),
      // Recommended Brochure
      app.$api.getRecommendBook().then(res => res.s === 1 ? res.d.data : []),
    ])
    return {
      indexData,
      recommendAuthors,
      recommendBooks
    }
  }
}

Setting and storage of token

The essential function of an application is token verification. Usually we store the returned verification information after login, and then request to bring token for back-end verification status. In projects separated from the front and back, they are usually stored in local storage. But Nuxt.js is different. Due to the characteristics of server-side rendering, some requests are initiated at the server side, so we can't get localStorage or sessionStorage.

At this point, cookies come in handy. Cookies can not only be operated by the client, but also be sent back to the server when requested. It is very troublesome to use the native operation cooike. With the help of cookie universal nuxt module (this module only helps us to inject and mainly relies on cookie universal), we can use cookies more easily. Whether on the server or the client, cookie universal nuxt provides us with a consistent api, which helps us to adapt the corresponding methods internally.

install

Install cookie universal nuxt

npm run cookie-universal-nuxt --save

nuxt.config.js :

module.exports = {
  modules: [
    'cookie-universal-nuxt'
  ],
}

Basic use

Similarly, cookie universal nuxt will be injected at the same time to access $cookies for use.

Server:

// obtain
app.$cookies.get('name')
// set up
app.$cookies.set('name', 'value')
// delete
app.$cookies.remove('name')

client:

// obtain
this.$cookies.get('name')
// set up
this.$cookies.set('name', 'value')
// delete
this.$cookies.remove('name')

More use method stamps here https://www.npmjs.com/package/cookie-universal-nuxt

Practical application process

Like Nuggets' login, our authentication information will be stored for a long time after login, instead of being logged in every time we use it. However, the cookie life cycle only exists in the browser, and it will be destroyed when the browser is closed, so we need to set a longer expiration time for it.

In the project, I encapsulate the setting identity information as a tool method, which will be called after the login is successful:

/utils/utils.js :

setAuthInfo(ctx, res) {
  let $cookies, $store
  // client
  if (process.client) {
    $cookies = ctx.$cookies
    $store = ctx.$store
  }
  // Server
  if (process.server) {
    $cookies = ctx.app.$cookies
    $store = ctx.store
  }
  if ($cookies && $store) {
    // Expiration time new Date(Date.now() + 8.64e7 * 365 * 10)
    const expires = $store.state.auth.cookieMaxExpires
    // Set cookie s
    $cookies.set('userId', res.userId, { expires })
    $cookies.set('clientId', res.clientId, { expires })
    $cookies.set('token', res.token, { expires })
    $cookies.set('userInfo', res.user, { expires })
    // Set up vuex
    $store.commit('auth/UPDATE_USERINFO', res.user)
    $store.commit('auth/UPDATE_CLIENTID', res.clientId)
    $store.commit('auth/UPDATE_TOKEN', res.token)
    $store.commit('auth/UPDATE_USERID', res.userId)
  }
}

After that, we need to modify axios to bring verification information when requesting:

/plugins/axios.js :

export default function ({ app: { $axios, $cookies } }) {
	$axios.defaults.baseURL = process.env.baseUrl
	$axios.defaults.timeout = 30000
	$axios.interceptors.request.use(config => {
    // Verification information on the head
		config.headers['X-Token'] = $cookies.get('token') || ''
		config.headers['X-Device-Id'] = $cookies.get('clientId') || ''
		config.headers['X-Uid'] = $cookies.get('userId') || ''
		return config
	})
	$axios.interceptors.response.use(response => {
		if (/^[4|5]/.test(response.status)) {
			return Promise.reject(response.statusText)
		}
		return response.data
	})
}

Authority verification Middleware

The above mentioned identity information will be set for a long time. Next, of course, you need to verify whether the identity expires. Here I will use the routing middleware to complete the verification function. The middleware runs before a page or a group of pages are rendered, just like the routing guard. Each middleware should be placed in the middleware directory, and the name of the file name will become the middleware name. Middleware can be executed asynchronously, only need to return Promise.

definition

/middleware/auth.js:

export default function (context) {
  const { app, store } = context
  const cookiesToken = app.$cookies.get('token')
  if (cookiesToken) {
    // Verify whether the login status expires every time the route jumps
    app.$api.isAuth().then(res => {
      if (res.s === 1) {
        if (res.d.isExpired) {   // Expired remove login verification information
          app.$utils.removeAuthInfo(context)
        } else {                 // Not expired reset storage
          const stateToken = store.state.auth.token
          if (cookiesToken && stateToken === '') {
            store.commit('auth/UPDATE_USERINFO', app.$cookies.get('userInfo'))
            store.commit('auth/UPDATE_USERID', app.$cookies.get('userId'))
            store.commit('auth/UPDATE_CLIENTID', app.$cookies.get('clientId'))
            store.commit('auth/UPDATE_TOKEN', app.$cookies.get('token'))
          }
        }
      }
    })
  }
}

The above processing in if (cookiestoken & & statetoken = = '') is due to the fact that some pages will open new tabs, resulting in the loss of information in vuex. Here we need to judge and reset the status tree.

use

nuxt.config.js :

module.exports = {
  router: {
    middleware: ['auth']
  },
}

This kind of middleware is injected into every page in the whole world. If you want the middleware to run on only one page, you can configure the middleware option of the page:

export default {
  middleware: 'auth'
}

Routing middleware document stamp here https://www.nuxtjs.cn/guide/routing#%E4%B8%AD%E9%97%B4%E4%BB%B6

Component registration management

For the simplest example, create Vue global.js in the plugins folder to manage the components or methods needed by the global

import Vue from 'vue'
import utils from '~/utils'
import myComponent from '~/components/myComponent.vue'

Vue.prototype.$utils = utils

Vue.use(myComponent)

nuxt.config.js :

module.exports = {
  plugins: [
    '~/plugins/vue-global.js'
  ],
}

Custom components

For some custom global shared components, my approach is to put them into the / components/common folder for unified management. In this way, you can use require.context to automate the incoming components. This method is provided by webpack, which can read all files in the folder. If you don't know this method, it's really strong for you to understand and use it. It can greatly improve your programming efficiency.

definition

/components/myComponentsInstall.js :

export default {
  install(Vue) {
    const components = require.context('~/components/common', false, /\.vue$/)
    // components.keys() get the array of file names
    components.keys().map(path => {
      // Get component file name
      const fileName = path.replace(/(.*\/)*([^.]+).*/ig, "$2")
      // components(path).default gets the content exposed by ES6 specification, and components(path) gets the content exposed by Common.js specification
      Vue.component(fileName, components(path).default || components(path))
    })
  } 
}

use

/plugins/vue-global.js :

import Vue from 'vue'
import myComponentsInstall from '~/components/myComponentsInstall'

Vue.use(myComponentsInstall)

After the above operation, the component has been registered globally. We just need to use the short horizontal line. And every new component doesn't need to be introduced again. It's really once and for all. Also in other practical applications, if the api file is divided into modules by function, this method can also be used to automatically introduce the interface file.

Third party component library (element UI)

All in

/plugins/vue-global.js :

import Vue from 'vue'
import elementUI from 'element-ui'

Vue.use(elementUI)

nuxt.config.js :

module.exports = {
  css: [
    'element-ui/lib/theme-chalk/index.css'
  ]
}

On demand introduction

With the help of Babel plugin component, we can only introduce the required components to reduce the project volume.

npm install babel-plugin-component -D

nuxt.config.js :

module.exports = {
  build: {
    plugins: [
      [
        "component",
        {
          "libraryName": "element-ui",
          "styleLibraryName": "theme-chalk"
        }
      ]
    ],
  }
}

Next, we will introduce some components we need, and also create a component of eleComponentsInstall.js to manage elementUI:

/components/eleComponentsInstall.js :

import { Input, Button, Select, Option, Notification, Message } from 'element-ui'

export default {
  install(Vue) {
    Vue.use(Input)
    Vue.use(Select)
    Vue.use(Option)
    Vue.use(Button)
    Vue.prototype.$message = Message
    Vue.prototype.$notify  = Notification
  }
}

/plugins/vue-global.js:

import Vue from 'vue'
import eleComponentsInstall from '~/components/eleComponentsInstall'

Vue.use(eleComponentsInstall)

Page layout switch

When we build web applications, the layout of most pages will be consistent. However, in some requirements, it may be necessary to change another layout mode. At this time, the page layout configuration option can help us to complete. Each layout file should be placed in the layouts directory. The name of the file name will become the layout name. The default layout is default. The following example changes the background color of the page layout. In fact, according to the understanding of using Vue, it feels like switching App.vue.

definition

/layouts/default.vue :

<template>
  <div style="background-color: #f4f4f4;min-height: 100vh;">
    <top-bar></top-bar>
    <main class="main">
      <nuxt />
    </main>
    <back-top></back-top>
  </div>
</template>

/layouts/default-white.vue :

<template>
  <div style="background-color: #ffffff;min-height: 100vh;">
    <top-bar></top-bar>
    <main class="main">
      <nuxt />
    </main>
    <back-top></back-top>
  </div>
</template>

use

Page component file:

export default {
  layout: 'default-white',
  // or
  layout(context) {
    return 'default-white'
  }
}

Custom error page

The customized error page needs to be placed in the layouts directory with the file name error. Although this file is placed in the layouts directory, it should be treated as a page. This layout file does not need to contain < nuxt / > tags. You can think of this layout file as a component for displaying application errors (404500, etc.).

definition

<template>
  <div class="error-page">
    <div class="error">
      <div class="where-is-panfish">
        <img class="elem bg" src="https://b-gold-cdn.xitu.io/v3/static/img/bg.1f516b3.png">
        <img class="elem panfish" src="https://b-gold-cdn.xitu.io/v3/static/img/panfish.9be67f5.png">
        <img class="elem sea" src="https://b-gold-cdn.xitu.io/v3/static/img/sea.892cf5d.png">
        <img class="elem spray" src="https://b-gold-cdn.xitu.io/v3/static/img/spray.bc638d2.png">
      </div>
      <div class="title">{{statusCode}} - {{ message }}</div>
      <nuxt-link class="error-link" to="/">Back to home</nuxt-link>
    </div>
  </div>
</template>
export default {
  props: {
    error: {
      type: Object,
      default: null
    }
  },
  computed: {
    statusCode () {
      return (this.error && this.error.statusCode) || 500
    },
    message () {
      return this.error.message || 'Error'
    }
  },
  head () {
    return {
      title: `${this.statusCode === 404 ? 'Page not found' : 'Error rendering page'} - Nuggets`,
      meta: [
        {
          name: 'viewport',
          content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no'
        }
      ]
    }
  }
}

error object

props in the error page accepts an error object that contains at least two properties, statusCode and message.

In addition to these two attributes, we can also pass on other attributes. Here we will talk about the error method mentioned above:

export default {
  async asyncData({ app, query, error }) {
    const tagInfo = await app.$api.getTagDetail({
      tagName: encodeURIComponent(query.name)
    }).then(res => {
      if (res.s === 1) {
        return res.d
      } else {
        // In this way, we add the query attribute to the error object
        error({
          statusCode: 404,
          message: 'Label does not exist',
          query 
        })
        return
      }
    })
    return {
      tagInfo
    }
  }
}

There is also the validate lifecycle of the page:

export default {
  async validate ({ params, store }) {
    throw new Error('Incorrect page parameters')
  }
}

The statusCode passed here is 500, and message is the content of new Error. If you want to transfer the object in the past, the message will be converted to the string [object Object]. You can use JSON.stringify to transfer it in the past, and the error page will be processed and parsed.

export default {
  async validate ({ params, store }) {
    throw new Error(JSON.stringify({ 
      message: 'validate error',
      params
    }))
  }
}

Encapsulate bottom touch event

Every page in the project will have a touch bottom event, so I will separate this logic into mixin s, and the required pages can be used.

/mixins/reachBottom.js :

export default {
  data() {
    return {
      _scrollingElement: null,
      _isReachBottom: false,  // Prevent repeated triggering when entering the execution area
      reachBottomDistance: 80 // How far from the bottom trigger
    }
  },
  mounted() {
    this._scrollingElement = document.scrollingElement
    window.addEventListener('scroll', this._windowScrollHandler)
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this._windowScrollHandler)
  },
  methods: {
    _windowScrollHandler() {
      let scrollHeight = this._scrollingElement.scrollHeight
      let currentHeight = this._scrollingElement.scrollTop + this._scrollingElement.clientHeight + this.reachBottomDistance
      if (currentHeight < scrollHeight && this._isReachBottom) {
        this._isReachBottom = false
      }
      if (this._isReachBottom) {
        return
      }
      // Bottom touch event trigger
      if (currentHeight >= scrollHeight) {
        this._isReachBottom = true
        typeof this.reachBottom === 'function' && this.reachBottom()
      }
    }
  },
}

The core of the implementation is, of course, the trigger time: scrollTop + clientHeight > = scrollheight (the total height of the page, including the scrolling area). But this requires a full touch of the bottom to trigger the event, so on this basis, I add reach bottomdistance to control the distance of the trigger event. Finally, the trigger event calls the page methods' reach button method.

Command window assembly

What is an imperative component? The message component of element UI is a good example. When we need pop-up prompt, we only need to call this.message() instead of switching components through v-if. The advantage of this is that it does not need to introduce components. It is convenient to use and where to adjust.

In nuxt-juejin-project, I also encapsulate two common pop-up components: login pop-up and preview pop-up. The technical point is to mount the components manually. There's not much code to implement, just a few lines.

definition

/components/common/picturesModal/picturesModal.vue :

export default {
  data() {
    return {
      url: '',  // Current picture link
      urls: ''  // Image link array
    }
  },
  methods: {
    show(cb) {
      this.cb = cb
      return new Promise((resolve, reject) => {
        document.body.style.overflow = 'hidden'
        this.resolve = resolve
        this.reject = reject
      })
    },
    // Destroy pop ups
    hideModal() {
      typeof this.cb === 'function' && this.cb()
      document.body.removeChild(this.$el)
      document.body.style.overflow = ''
      // Destroy component instance
      this.$destroy()
    },
    // Close pop up window
    cancel() {
      this.reject()
      this.hideModal()
    },
  }
}

/components/common/picturesModal/index.js

import Vue from 'vue'
import picturesModal from './picturesModal'

let componentInstance = null

// Construction subclass
let ModalConstructor = Vue.extend(picturesModal)

function createModal(options) {
  // Instantiate component
  componentInstance = new ModalConstructor()
  // Merge options
  Object.assign(componentInstance, options)
  // $mount can pass in a selector string to mount to the selector
  // If you don't pass in a selector, it will be rendered as an element outside the document. You can imagine that document.createElement() generates a dom in memory
  // $el gets dom elements
  document.body.appendChild(componentInstance.$mount().$el)
}

function caller (options) {
  // There is only one pop-up window in a single global case
  if (!componentInstance) {
    createModal(options)
    // Call the callback passed in the show method in the component when the component is destroyed
    return componentInstance.show(() => { componentInstance = null })
  }
}

export default {
  install(Vue) {
    // Register the pop-up method. The method returns promise then to close the pop-up window if the login succeeds
    Vue.prototype.$picturesModal = caller
  }
}

use

/plugins/vue-global.js:

import picturesModal from '~/components/common/picturesModal'

Vue.use(picturesModal)

The object passed in here is the options parameter received by createModal above, and finally the data overwritten to the component is merged.

this.$picturesModal({
  url: 'b.jpg'
  urls: ['a.jpg', 'b.jpg', 'c.jpg']
})

Technical point of middle layer

The work flow of the middle tier is to send requests to the middle tier at the front end, and the middle tier sends requests to the back end to get data. The advantage of this is that in the front-end to back-end interaction, we get control of the agent. With this right, we can do more. For example:

  • Agent: in the development environment, we can use agents to solve the most common cross domain problems; in the online environment, we can use agents to forward requests to multiple servers.
  • Cache: the cache is actually closer to the front-end requirements. The user's actions trigger the update of data. The node middle layer can directly handle part of the cache requirements.
  • Log: compared with other server-side languages, the log record of node middle layer can locate problems more conveniently and quickly.
  • Monitoring: good at high concurrency request processing, monitoring is also a suitable option.
  • Data processing: return required data, data field alias, data aggregation.

The existence of the middle tier also makes the separation of front and back-end responsibilities more thorough. The back-end only needs to manage data and write interfaces, and what data needs to be handled by the middle tier.

The middle layer of nuxt-justice-project project uses the koa framework. The http request method of the middle layer is simply encapsulated based on the request library. The code is implemented in / server/request/index.js. Because it needs to be used later, I will mention it here.

Request forwarding

Install related Middleware

npm i koa-router koa-bodyparser --save

Koa Router: router middleware, which can quickly define routes and manage routes

KOA bodyparser: parameter parsing middleware, which supports parsing json and form types, and is commonly used to parse POST requests

The usage of related middleware is searched on npm, and how to use it is explained here

Routing design

As for the so-called "no rules, no circle", I refer to teacher Ruan Yifeng's RESTful API design guide.

Routing directory

I will store the routing file in the / server/routes directory. According to the specification, I need a folder that specifies the api version number. The final routing file is stored in / server/routes/v1.

Routing path

In the RESTful architecture, each web address represents a kind of resource, so there can be no verbs in the web address, only nouns, and the nouns used often correspond to the table names of the database. Generally speaking, the tables in the database are collection s of the same kind of records, so the nouns in the API should also use the plural.

For example:

  • Article related interface file is named articles
  • Label related interface file is named tag
  • The boiling point related interface file is named pins

Route type

Specific types of routing operation resources, represented by HTTP verbs

  • GET (SELECT): takes resources (one or more items) from the server.
  • POST (CREATE): CREATE a new resource on the server.
  • PUT (UPDATE): UPDATE the resource on the server (the client provides the full resource after the change).
  • DELETE: deletes resources from the server.

Routing logic

Here is an example of the user column list interface

/server/router/articles.js

const Router = require('koa-router')
const router = new Router()
const request = require('../../request')
const { toObject } = require('../../../utils')

/**
 * Get user column
 * @param {string} targetUid - User id
 * @param {string} before - The last one is createdAt, which is passed in the next page
 * @param {number} limit - Number of items
 * @param {string} order - rankIndex: Hot, createdAt: latest
 */
router.get('/userPost', async (ctx, next) => {
  // Header information
  const headers = ctx.headers
  const options = {
    url: 'https://timeline-merger-ms.juejin.im/v1/get_entry_by_self',
    method: "GET",
    params: {
      src: "web",
      uid: headers['x-uid'],
      device_id: headers['x-device-id'],
      token: headers['x-token'],
      targetUid: ctx.query.targetUid,
      type: ctx.query.type || 'post',
      limit: ctx.query.limit || 20,
      before: ctx.query.before,
      order: ctx.query.order || 'createdAt'
    }
  };
  // Initiate a request
  let { body } = await request(options)
  // The data obtained after the request is json, which needs to be converted to object for operation
  body = toObject(body)
  ctx.body = {
    s: body.s,
    d: body.d.entrylist || []
  }
})

module.exports = router

Register route

/server/index.js is the file that Nuxt.js generates for us. Our middleware usage and route registration need to be written in this file. The following application ignores some of the code and only shows the main logic.

/server/index.js :

const Koa = require('koa')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
const router = new Router()

// Using middleware
function useMiddleware(){
  app.use(bodyParser())
}

// Register route
function useRouter(){
  let module = require('./routes/articles')
  router.use('/v1/articles', module.routes())
  app.use(router.routes()).use(router.allowedMethods())
}

function start () {
  useMiddleware()
  useRouter()
  app.listen(8000, '127.0.0.1')
}

start()

The call address of the last interface is: http://127.0.0.1:8000/v1/articles/userPost

Automatic route registration

Yes, it's coming again. Automation is fragrance. Once and for all, can it not be fragrant.

const fs = require('fs')
const Koa = require('koa')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
const router = new Router()

// Register route
function useRouter(path){
  path = path || __dirname + '/routes'
  // Get all file names under routes directory, urls is the array of file names
  let urls = fs.readdirSync(path)
  urls.forEach((element) => {
    const elementPath = path + '/' + element
    const stat = fs.lstatSync(elementPath);
    // Folder or not
    const isDir = stat.isDirectory();
    // Folder recursion registration route
    if (isDir) {
      useRouter(elementPath)
    } else {
      let module = require(elementPath)
      let routeRrefix = path.split('/routes')[1] || ''
      //File name in routes as route name
      router.use(routeRrefix + '/' + element.replace('.js', ''), module.routes())
    }
  })
  //Use routing
  app.use(router.routes()).use(router.allowedMethods())
}

function start () {
  useMiddleware()
  useRouter()
  app.listen(8000, '127.0.0.1')
}

start()

The above code takes routes as the main directory of the route, looks down for JS file to register the route, and finally takes JS file path as the route name. For example, if there is a search interface / search in / server/routes/v1/articles.js, the call address of the interface is localhost:8000/v1/articles/search.

Route parameter validation

Parameter validation is a certain function in the interface. Incorrect parameters will cause unexpected errors in the program. We should verify the parameters in advance, stop the wrong query and inform the user. In the project, I encapsulate a routing middleware to verify parameters based on async validator. If you don't know the workflow of koa middleware, it's necessary to understand the onion model.

definition

/server/middleware/validator/js :

const { default: Schema } = require('async-validator')

module.exports = function (descriptor) {
  return async function (ctx, next) {
    let validator = new Schema(descriptor)
    let params = {}
    // Get parameters
    Object.keys(descriptor).forEach(key => {
      if (ctx.method === 'GET') {
        params[key] = ctx.query[key]
      } else if (
        ctx.method === 'POST' ||
        ctx.method === 'PUT' ||
        ctx.method === 'DELETE'
      ) {
        params[key] = ctx.request.body[key]
      }
    })
    // Validation parameters
    const errors = await validator.validate(params)
      .then(() => null)
      .catch(err => err.errors)
    // Error if validation fails
    if (errors) {
      ctx.body = {
        s: 0,
        errors
      }
    } else {
      await next()
    }
  }
}

use

Please refer to async-validator

const Router = require('koa-router')
const router = new Router()
const request = require('../../request')
const validator = require('../../middleware/validator')
const { toObject } = require('../../../utils')

/**
 * Get user column
 * @param {string} targetUid - User id
 * @param {string} before - The last one is createdAt, which is passed in the next page
 * @param {number} limit - Number of pieces
 * @param {string} order - rankIndex: Hot, createdAt: latest
 */
router.get('/userPost', validator({
  targetUid: { type: 'string', required: true },
  before: { type: 'string' },
  limit: { 
    type: 'string', 
    required: true,
    validator: (rule, value) => Number(value) > 0,
    message: 'limit Positive integer expected'
  },
  order: { type: 'enum', enum: ['rankIndex', 'createdAt'] }
}), async (ctx, next) => {
  const headers = ctx.headers
  const options = {
    url: 'https://timeline-merger-ms.juejin.im/v1/get_entry_by_self',
    method: "GET",
    params: {
      src: "web",
      uid: headers['x-uid'],
      device_id: headers['x-device-id'],
      token: headers['x-token'],
      targetUid: ctx.query.targetUid,
      type: ctx.query.type || 'post',
      limit: ctx.query.limit || 20,
      before: ctx.query.before,
      order: ctx.query.order || 'createdAt'
    }
  };
  let { body } = await request(options)
  body = toObject(body)
  ctx.body = {
    s: body.s,
    d: body.d.entrylist || []
  }
})

module.exports = router

Type represents the parameter type, and required represents whether it is required. When the type is enum, the parameter value can only be one item in enum array.

It should be noted that the number type cannot be verified here because the parameter will be converted to a string type during transmission. But we can customize the validation rules through the validator method, just like the limit parameter above.

The following is what the interface returns when the limit parameter is wrong:

Website security

cors

Setting cors to verify the security and legitimacy of the request can improve the security of your website. With the help of koa2 cors, we can do this more easily. Koa2 cors source code is not much, suggest to see, as long as you have some basic can understand, not only to know how to use but also to know the implementation process.

install

npm install koa2-cors --save

use

/server/index.js :

const cors = require('koa2-cors')

function useMiddleware(){
  app.use(helmet())
  app.use(bodyParser())
  //Set global return header
  app.use(cors({
    // Allow cross domain domain names
    origin: function(ctx) {
      return 'http://localhost:8000';
    },
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
    maxAge: 86400,
    // Allow to carry head verification information
    credentials: true,  
    // Allowed methods
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'],
    // Allowed headers
    allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Token', 'X-Device-Id', 'X-Uid'],
  }))
}

If it does not match the way of the request, or it has a header that is not allowed. When sending the request, it will directly fail, and the browser will throw an error restricted by the cors policy. The following is an example of an error with an disallowed header:

koa-helmet

Koa helmet provides important security headers to make your application more secure by default.

install

npm install koa-helmet --save

use

const helmet = require('koa-helmet')

function useMiddleware(){
  app.use(helmet())
  // .....
}

By default, we have the following security settings:

  • X-DNS-Prefetch-Control: disable DNS prefetch for browsers.
  • X-Frame-Options: mitigate click hijacking attacks.
  • X-Powered-By: the X-Powered-By header is removed, making it more difficult for an attacker to view technologies that could potentially threaten the site.
  • Strict transport security: make your users use HTTPS.
  • X-Download-Options: prevents Internet Explorer from performing downloads in the context of your site.
  • X-Content-Type-Options: set to nosniff to help prevent browsers from trying to guess ("sniff") MIME types, which may pose a security risk.
  • X-XSS-Protection: prevent reflected XSS attacks.

More description and configuration stamp here https://www.npmjs.com/package/koa-helmet

last

I feel that the relevant knowledge points of the middle tier are not complete enough, and there are still many things to do, so I have to continue to learn. The project will be updated for some time in the future, and it will be closer to the server, such as cache optimization and exception capture.

If you have any suggestions or improvements, please let me know~

😄 Don't you see a little star here? https://github.com/ChanWahFung/nuxt-juejin-project

reference material

Tags: Vue axios npm github

Posted on Sun, 17 May 2020 06:08:32 -0700 by misty