preface

As a Vuer (Vue developer), if you don’t already know the framework, then your Vue stack is unlit.

What is Nuxt. Js

Nuxt.js official Introduction:

Nuxt.js is a universal application framework based on Vue.js. Nuxt.js focuses on the UI rendering of the application through an abstract organization of the client/server infrastructure. Our goal is to create a flexible application framework that you can use to initialize infrastructure code for new projects or to use Nuxt.js in existing Node.js projects. Nuxt.js presets the various configurations needed to develop server-side rendering applications using Vue.js.

If you are familiar with vue.js, you will soon be able to get started with NuxT.js. The development experience is not very different from vue.js, which is equivalent to extending the configuration to vue.js. Of course, you have a basic knowledge of Node.js, which is great.

What problem does NuxT.js solve

Now vue. js is mostly used for single page application, with the development of technology, single page application is not enough to meet the needs. And some disadvantages have also become a single page application common fault, single page application will be loaded when visiting all the files, the first screen access needs to wait for a period of time, which is often said to be white screen, another point is known as SEO optimization problems.

Nuxt.js is designed to solve these problems, especially if your site is a community-oriented project that needs search engine traffic.

My first NuxT.js project

I also use NuxT.js to copy the Nuggets Web site in my spare time:

Nuxt-juejin-project is a study project that uses nuxT. js to copy gold, mainly using: nuxt + KOa + vuex + Axios + Element-UI. All data for this project is synchronized with the nuggets because the interface is forwarded through KOA as the middle layer. The main page data is rendered by the server.

A few days after the completion of the project, I sorted out the notes I had taken and added some commonly used technical points. Finally, I have this article, hoping to help those who are studying.

There are some screenshots in the project introduction, if Jio can, please ask star😜~

Project address: github.com/ChanWahFung…

Basic application and configuration

The construction of the project is guided by the official website. I believe it is difficult for you to run a project, so I won’t repeat it here.

πŸƒ ♀ ️ run www.nuxtjs.cn/guide/insta…

For the configuration of the project, I chose the following:

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

context

Context is an additional object provided by Nuxt, Context is used in special Nuxt lifecycle areas such as “asyncData”, “plugins”, “middlewares”, “modules”, and “Store /nuxtServerInit”.

So, to use Nuxt.js, we must be familiar with what properties are available on the object.

The context of official document stamp here from www.nuxtjs.cn/api/context

Here are a few of the attributes that are important and commonly used in practical applications:

app

App is the most important property in the context, just like this in our Vue, where global methods and properties are mounted. Because of the specificity of server-side rendering, many of the lifecycle provided by Nuxt runs on the server, which means they precede the creation of Vue instances. Therefore, in these lifecycles, we cannot use this to get methods and properties on the instance. This can be offset by using an app, which typically injects global methods into both this and app, using the app to access the method during the server lifecycle, and using this on the client to ensure that the methods are shared.

Here’s an example:

Assuming $AXIos has been injected at the same time, the main data is pre-requested by asyncData (the life cycle initiates the request, gives the obtained data to the server to be assembled into HTML return), and the secondary data is requested by the client mounted.

export default {
  async asyncData({ app }) {
    // Table 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
    }
  }
}
Copy the code

store

Store is an instance of vuex. store. At run time Nuxt.js will try to find the store directory in the root of the application, and if it exists, it will add the module files to the build configuration.

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

/store/index.js :

export const state = () = > ({
  list: []})export const mutations = {
  updateList(state, payload){
    state.list = payload
  }
}
Copy the code

And nuxT.js will kindly help us inject the Store at the same time. Finally we can use it in the component like this:

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) {
      // The client uses it, but you can also do it using the helper function mapMutations
      this.$store.commit('updateList', list)
    }
  }
}
Copy the code

To understand the store injection process, I went through the.nuxt/index.js source code (the.nuxt directory is automatically generated by nuxt.js during the build run) and got a general idea of the process. First, in.nuxt/store.js, do a series of processing on the Store module file and expose the createStore method. Then in.nuxt/index.js, the createApp method injects both:

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
  }
}
Copy the code

Nuxt.js will also use the inject method to mount the plugin (plugin is the main way to mount global methods), which means that we can access global methods in the store via this:

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

Params, query,

Params and query are aliases for rout.params and rout.query, respectively. They are objects with routing parameters and are simple to use. There’s nothing to talk about. Just use it.

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
    }
  }
}
Copy the code

redirect

This method redirects the user request to another route and is usually used for permission authentication. Usage: redirect(params). Params parameters include status(status code, default 302), path(route path), and query(parameter), where status and Query are optional. Of course, if you’re simply redirecting routes, you can pass in a path string, like redirect(‘/index’).

Here’s an example:

Suppose we now have a routing middleware that authenticates the login identity. The logic is to do nothing if the identity has not expired, and redirect to the login page if it has.

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

error

This method jumps to the error page. Usage: Error (params). The params argument should contain statusCode and message fields. In a real scenario, there is always something going on that doesn’t make sense, so the page doesn’t really look the way you want it to, so it’s still necessary to use this method for error messages.

Here’s an example:

The tag detail page requests data that depends on query.name. If query.name does not exist, the request cannot return available data and the error page is redirected

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
    }
  }
}
Copy the code

Nuxt Common page life cycle

asyncData

You may want to fetch and render the data on the server side. Nuxt.js adds the asyncData method so that you can asynchronously fetch data before rendering the component.

AsyncData is the most common and important lifecycle and the key to server-side rendering. This lifecycle is limited to page component calls, with the first argument being context. It is invoked before the component is initialized and operates in the server environment. So in the asyncData lifecycle, we can’t refer to the current Vue instance with this, and there are no Window objects or document objects, which we need to be aware of.

In general, asyncData will make a pre-request for the main page data, and the obtained data will be pieced into HTML by the server to return front-end rendering, so as to improve the first screen loading speed and SEO optimization.

Look at the figure below. In Google Debugger, you don’t see the main data interface making the request, only the HTML document returned, proving that the data was rendered on the server.

Finally, we need to return the data obtained by the interface:

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

It is worth mentioning that asyncData is only executed on the first screen and is otherwise created or mounted to render the page on the client side.

What does that mean? Here’s an example:

Now there are two pages, the home page and the details page, and they both have asyncData set. When entering the home page, asyncData is running on the server. After rendering, click the article to enter the detail page. At this time, the asyncData of the detail page will not run on the server side, but initiate a request to get data rendering on the client side, because the detail page is not the first screen. When we refresh the detail page, the detail page asyncData will only run on the server. So, don’t go into this trap. .

fetch

The FETCH method is used to populate the application’s state tree (store) data in front of the rendered page, similar to the asyncData method, except that it does not set the component’s data.

Looking at the official description, you can see that this lifecycle is used to populate the Vuex state tree. Like asyncData, it is called before the component is initialized, and the first argument is context.

To make the fetching process asynchronous, you need to return a Promise, and NuxT.js will wait until the Promise is complete before rendering the component.

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

You can also use async or await mode 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)
  }
}
Copy the code

This is not to say that we can only populate the state tree in fetch, but also in asyncData.

validate

Nuxt.js allows you to configure a validation method in the page component corresponding to the dynamic route to verify the validity of the dynamic route parameters.

It helps us when we verify that the route parameters are valid, and the first parameter is context. Slightly different from the above, we can access the method this.methods.xxx on the instance.

Print this as follows:

Life cycle can return a Boolean, true to enter the route, false to 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)
    }
  }
}
Copy the code

Or return a Promise:

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

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

export default {
  async validate ({ params, store }) {
    // Use custom message to trigger internal server 500 error
    throw new Error('Under Construction! ')}}Copy the code

watchQuery

Listen for parameter string changes and execute component methods (asyncData, FETCH, Validate, Layout,…) when they change.

WatchQuery Boolean or Array (default: []). Use the watchQuery property to listen for changes to the parameter string. If the string defined changes, all component methods (asyncData, fetch, Validate, Layout,…) are called. . This is disabled by default to improve performance.

I also used this configuration in the search page for the Nuxt-Juejin-project project:

<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>
Copy the code
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, and execute asyncData to retrieve data again
      this.$router.push({
        name: 'search'.query: {
          type: item.type || this.type,
          keyword: this.keyword,
          period: item.period || this.period
        }
      })
    }
  }
}
Copy the code

One advantage of using watchQuery is that when we use the browser back or forward buttons, the page data is refreshed because the parameter strings change.

head

Nuxt.js uses vue-meta to update the application’s Head tag and HTML attributes.

Set the header tag for the current page using the head method, which gets the component’s data from this. In addition to looking good, the correct setting of meta tags can also help the page to be found by search engines, seo optimization. Description and keyword are usually set.

title:

meta:

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

In order to avoid the phenomenon that meta tags in child components cannot correctly overwrite the same tags in parent components, it is recommended to use hid keys to assign a unique identifier number to meta tags.

In nuxt.config.js, we can also set the global head:

module.exports = {
  head: {
    title: 'the 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: 'the nuggets, rare earth, Vue. Js, WeChat applet, Kotlin, RxJava, React Native, Wireshark, agile development, the Bootstrap, OKHttp, regular expressions, WebGL, Webpack, Docker, MVVM'},
      { hid: 'description'.name: 'description'.content: 'Diggin' is a community that helps developers grow. It's Hacker News for developers, Designer News for designers, and Medium for product managers. Nugget's technical articles are co-edited by the tech geeks and geeks on rare earth to screen out the best quality dry goods for you, including: Android, iOS, front end, back end and more. It's where users can find the headlines from the tech world every day. At the same time, there are boiling points in the nuggets, the nuggets translation program, offline activities, column articles and other content. Even if you are a GitHub, StackOverflow, open Source China user, we believe you can also learn here. '}].}}Copy the code

supplement

The following is the call order for these lifecycles, which may be helpful at some point.

validate  =>  asyncData  =>  fetch  =>  head
Copy the code

Configuring the Startup Port

You can configure the boot port in either of the following, but I personally prefer the first configuration in nuxt.config.js, which is more logical.

The first kind of

nuxt.config.js :

module.exports = {
  server: {
    port: 8000.host: '127.0.0.1'}}Copy the code

The second,

package.json :

"config": {
  "nuxt": {
    "port": "8000"."host": "127.0.0.1"}},Copy the code

Loading external resources

Global configuration

nuxt.config.js :

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

Component configuration

Components can be configured in head, which can accept objects or functions. The official example uses the object type, but uses the function type as well.

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

The environment variable

Nuxt.config.js provides the env option to configure environment variables. But I have tried to create the.env file management environment variable from the root directory and found it 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'}},Copy the code

In this configuration, we create a baseUrl environment variable that matches the address of the environment by process.env.node_env

Using environment variables

We can use the baseUrl variable in two ways:

  1. throughprocess.env.baseUrl
  2. throughcontext.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
	// ζˆ–θ€… $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
	})
}
Copy the code

plugins

As plugins are the primary means of global injection, there are several ways to use them that must be understood. Sometimes you want to use a function or property value throughout your application, and you need to inject it into the Vue instance (client), context (server), or even store(Vuex).

Plugin function parameters

Plugins typically expose a function that takes two parameters, context and inject

Context: A context object that stores many useful properties. The common app property, for example, contains the Vue root instance of all plug-ins. For example, when using Axios, you can get $axios directly by context.app.$axios.

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

Such as:

export default function (context, inject) {}
Copy the code

Inject the Vue instance

define

plugins/vue-inject.js :

import Vue from 'vue'

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

use

nuxt.config.js :

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

This makes this function available to all Vue components

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

Into the context

Context injection is similar to injecting other VUE applications.

define

plugins/ctx-inject.js :

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

use

nuxt.config.js :

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

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

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

At the same time into

If you need to inject it into context, Vue instance, or even Vuex at the same time, you can use inject method, which is the second argument of the plugin export function. The system prefixes the method name with $by default.

define

plugins/combined-inject.js :

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

use

nuxt.config.js :

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

So now you can call myInjectedFunction on context, or this on Vue, or this on Vuex’s Actions/mutations method

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

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)
  }
}
Copy the code

Plugins calling each other

When a plugin depends on another plugin call, we can access the context to get it, provided that the plugin needs to use context injection.

For example: there is a plugin that requests, and there is another plugin that needs to call request

plugins/request.js :

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

The plugins/API. Js:

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

It’s worth noting that plugins are injected in order. In the example above, the request is injected before the API

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

The routing configuration

In NUxt.js, routes are automatically generated based on the file structure and do not need to be configured. The automatically generated route 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
    }
  ]
})
Copy the code

Nuxt.js needs to create the corresponding Vue file or directory prefixed with an underscore

Take the following directory as an example:

pages/
--| users/
-----| _id.vue
--| index.vue
Copy the code

The configurations of automatically generated routes are as follows:

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

Embedded routines by

For example, we need the vue file for the level 1 page and a folder with the same name as the file (for the child pages)

pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue
Copy the code

The configurations of automatically generated routes are 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'}]}]}Copy the code

Nuxt-child is then used in the level 1 page to display child pages, just as router-View is used

<template>
  <div>
    <nuxt-child></nuxt-child>
  </div>
</template>
Copy the code

Custom Configuration

In addition to generating routes based on the file structure, you can customize them by modifying the router options in the nuxt.config.js file. These configurations are added to the nuxt.js route configuration.

The following example is a configuration for adding an emphasis direction to a route:

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

axios

The installation

Nuxt has integrated @nuxtjs/ Axios for us, this step can be ignored if you select Axios when creating the project.

npm i @nuxtjs/axios --save
Copy the code

nuxt.config.js :

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

SSR using Axios

The server side gets and renders the data. The asyncData method can asynchronously fetch the data before rendering the component and return it to the current component.

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

Use Axios for non-SSR

This will be used just as we would normally, by calling this

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

Customize Axios configuration

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

define

/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
  })
}
Copy the code

use

nuxt.config.js :

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

When finished, use it in the same way as above.

CSS preprocessor

Take SCSS as an example

The installation

npm i node-sass sass-loader scss-loader --save--dev
Copy the code

use

No configuration is required. It can be used in the template

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

Global style

When writing a layout style, there are a lot of common styles, and at this point we can extract those styles by simply adding a class name when needed.

define

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;
}
Copy the code

use

nuxt.config.js :

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

The global variable

To inject variables and mixins into a page and not have to import them every time, use @nuxtjs/style-resources.

The installation

npm i @nuxtjs/style-resources --save--dev
Copy the code

define

/assets/scss/variable.scss:

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

use

nuxt.config.js :

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

Element-ui custom theme

define

/ assets/SCSS/element – variables. SCSS:

/* Change the theme color variable */
/* $theme is defined in the above SCSS file and uses */$-color-primary: $theme;

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

/* Component styles are introduced as needed */
@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";
Copy the code

use

nuxt.config.js :

module.exports = {
  modules: [
    '@nuxtjs/style-resources'].styleResources: {
    scss: [
      SCSS uses variables defined in variables. SCSS * if the order is reversed, the variables will not be found */
      '~/assets/scss/variable.scss'.'~/assets/scss/element-variables.scss']}}Copy the code

There is another method that you can use, the plugin

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

Vue.use(myComponentsInstall)
Vue.use(eleComponentsInstall)
Copy the code

Front-end technology point

AsyncData requests are parallel

This should give you a sense of how important asyncData is, and how important it is to modify the details of the asyncData lifecycle. In general, asyncData makes not just one request, but possibly many:

export default {
  async asyncData({ app }) {
    // The list of articles
    let indexData = await app.$api.getIndexList({
      first: 20.order: 'POPULAR'.category: 1
    }).then(res= > res.s == 1 ? res.d : {})
    // Recommend the author
    let recommendAuthors = await app.$api.getRecommendAuthor({ 
      limit: 5
    }).then(res= > res.s == 1 ? res.d : [])
    // Recommendation booklet
    let recommendBooks = await app.$api.getRecommendBook().then(res= > res.s === 1 ? res.d.data : [])
    return {
      indexData,
      recommendAuthors,
      recommendBooks
    }
  }
}
Copy the code

The above operation looks fine, but there is actually a detail that can be optimized. We all know that async/await tasks will execute asynchronously. The next asynchronous task will be in the waiting state until the last asynchronous task finishes. This requires waiting for three asynchronous tasks, assuming each of these requests takes one second, which means that the page must wait at least three seconds before the content appears. We want to optimize the first screen with server-side rendering, but now we have to wait for requests to slow down the page rendering.

The best solution would be to send multiple requests at the same time. Maybe a smart buddy has already thought of Promise.all. Yes, using Promise.all to send these requests in parallel solves this problem. Promise.all takes an array of promises as an argument and returns an array of results when all promises are successful. The final time will be based on the longest Promise, so the original 3 seconds can be reduced to 1 second. Note that if one of the requests fails, the reject state is rejected first, so no data can be obtained. I’ve already done catch error handling when the project encapsulates the base request, so I make sure none of the requests are rejected.

export default {
  asyncData() {
    // Array destruct to obtain the data corresponding to the request
    let [indexData, recommendAuthors, recommendBooks] = await Promise.all([
      // The list of articles
      app.$api.getIndexList({
        first: 20.order: 'POPULAR'.category: 1
      }).then(res= > res.s == 1 ? res.d : {}),
      // Recommend the author
      app.$api.getRecommendAuthor({ 
        limit: 5
      }).then(res= > res.s == 1 ? res.d : []),
      // Recommendation booklet
      app.$api.getRecommendBook().then(res= > res.s === 1 ? res.d.data : []),
    ])
    return {
      indexData,
      recommendAuthors,
      recommendBooks
    }
  }
}
Copy the code

Token setting and storage

An essential feature of an application is token authentication. Usually, after logging in, we store the returned authentication information, and then request to carry the token for back-end verification status. In projects where the front and back ends are separated, this is typically stored in local storage. But nuxT. js is different, due to the characteristics of server rendering, part of the request is initiated in the server, we can not get localStorage or sessionStorage.

This is where cookies come in. Cookies are not only available to us on the client side, but also sent back to the server when requested. It is very troublesome to use the native operation cooike. With the help of the cookie-universal nuxt module (which only helps us with injection and mainly relies on cookie-universal), we can use cookies more conveniently. Cookie-universal-nuxt provides us with a consistent API on both the server and client side, and internally helps us adapt the methods.

The installation

To install a cookie – universal – nuxt

npm install cookie-universal-nuxt --save
Copy the code

nuxt.config.js :

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

Based on using

Similarly, cookie-universal- Nuxt is injected simultaneously, accessing $cookies for use.

Server:

/ / to get
app.$cookies.get('name')
/ / set
app.$cookies.set('name'.'value')
/ / delete
app.$cookies.remove('name')
Copy the code

Client:

/ / to get
this.$cookies.get('name')
/ / set
this.$cookies.set('name'.'value')
/ / delete
this.$cookies.remove('name')
Copy the code

More usage stamp here www.npmjs.com/package/coo…

Practical application process

Like the Nugget login, our verification information is stored long term after we log in, rather than logging in every time we use it. However, cookies only live in the browser and are destroyed when the browser closes, so we need to set a long expiration time for them.

In the project, I encapsulated the setting of identity information into a tool method, which will be called after a successful login:

/utils/utils.js :

setAuthInfo(ctx, res) {
  let $cookies, $store
  / / the client
  if (process.client) {
    $cookies = ctx.$cookies
    $store = ctx.$store
  }
  / / the server
  if (process.server) {
    $cookies = ctx.app.$cookies
    $store = ctx.store
  }
  if ($cookies && $store) {
    New Date(date.now () + 8.64e7 * 365 * 10)
    const expires = $store.state.auth.cookieMaxExpires
    / / set the cookie
    $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 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)
  }
}
Copy the code

Then you need to modify Axios so that it includes validation information on the request:

/plugins/axios.js :

export default function ({ app: { $axios, $cookies } }) {
  $axios.defaults.baseURL = process.env.baseUrl
  $axios.defaults.timeout = 30000
  $axios.interceptors.request.use(config= > {
    // Put the validation information in the header
    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
  })
}
Copy the code

Permission verification middleware

As mentioned above, the identity information is set for a long time, and then of course you need to verify that the identity has expired. I’m using routing middleware for validation here, which runs before a page or set of pages are rendered, like a routing guard. Each piece of middleware should be placed in the Middleware directory, and the name of the file will be the middleware name. The middleware can execute asynchronously, simply returning a Promise.

define

/ middleware/auth. Js:

export default function (context) {
  const { app, store } = context
  const cookiesToken = app.$cookies.get('token')
  if (cookiesToken) {
    // Each hop route verifies whether the login status has expired
    return app.$api.isAuth().then(res= > {
      if (res.s === 1) {
        if (res.d.isExpired) {   // Expiration removes login verification information
          app.$utils.removeAuthInfo(context)
        } else {                 // Reset the storage before expiration
          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'))}}}})}}Copy the code

If (cookiesToken && stateToken === “”), because some pages will open new tabs, resulting in loss of information in VUex, here we need to decide to reset the state tree.

use

nuxt.config.js :

module.exports = {
  router: {
    middleware: ['auth']}}Copy the code

This middleware usage is injected into each page globally. If you want the middleware to run only on one page, you can configure the Middleware option on the page:

export default {
  middleware: 'auth'
}
Copy the code

Routing middleware document stamp here www.nuxtjs.cn/guide/routi…

Component Registration Management

For the simplest example, create vue-global.js in the plugins folder to manage the components or methods that need to be used globally:

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

Vue.prototype.$utils = utils

Vue.use(myComponent)
Copy the code

Nuxt. Config. Js:

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

Custom components

For some custom global shared components, I manage them in the/Components/Common folder. This enables the import component to be automated using require.context, a method provided by WebPack that reads all the files in the folder. If you don’t know this method, it’s really strong and you know it and you use it, it will greatly improve your programming efficiency.

define

/components/myComponentsInstall.js :

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

use

/plugins/vue-global.js :

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

Vue.use(myComponentsInstall)
Copy the code

After the above operation, the component has been registered globally, we just need to use the short line to use. And every new component doesn’t have to be introduced, really once and for all. Also in other practical applications, if the API file is divided into modules by function, you can use this method to automatically import the interface file.

Third-party component Libraries (Element-UI)

All the introduction of

/ plugins/vue – global. Js:

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

Vue.use(elementUI)
Copy the code

nuxt.config.js :

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

According to the need to introduce

With Babel-Plugin-Component, we can reduce the size of the project by introducing only the components we need.

npm install babel-plugin-component -D
Copy the code

nuxt.config.js :

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

Next, introduce some of the components we need, and also create a elecomponentsinstall.js component 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
  }
}
Copy the code

/plugins/vue-global.js:

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

Vue.use(eleComponentsInstall)
Copy the code

Page layout Switch

When we build web applications, most pages will have the same layout. However, in some cases, an alternative layout may be needed, and the page Layout configuration option can help. Each layout file should be located in the layouts directory, and the name of the file will become the layout name. The default layout is default. The following example changes the background color of the page layout. In fact, using Vue feels like switching app.vue.

define

/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>
Copy the code

/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>
Copy the code

use

Page component file:

export default {
  layout: 'default-white'./ / or
  layout(context) {
    return 'default-white'}}Copy the code

Custom error pages

The custom error page needs to be in the layouts directory with the file name Error. Although this file is in the layouts directory, you should think of it as a page. This layout file does not need to contain the
tag. You can think of this layout file as a component that displays application errors (404,500, etc.).

define

<template>
  <div class="error-page">
    <div class="error">
      <div class="where-is-panfish">
        <img class="elem bg" src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/v3/static/img/bg.1f516b3.png~tplv-t2oaga2asx-image.image">
        <img class="elem panfish" src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/v3/static/img/panfish.9be67f5.png~tplv-t2oaga2asx-image.image ">
        <img class="elem sea" src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/v3/static/img/sea.892cf5d.png~tplv-t2oaga2asx-image.image">
        <img class="elem spray" src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/v3/static/img/spray.bc638d2.png~tplv-t2oaga2asx-image.image">
      </div>
      <div class="title">{{statusCode}} - {{ message }}</div>
      <nuxt-link class="error-link" to="/">Back to the home page</nuxt-link>
    </div>
  </div>
</template>
Copy the code
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: `The ${this.statusCode === 404 ? 'Page not found' : 'Render page error'}- the nuggets `.meta: [{name: 'viewport'.content: 'width = device - width, initial - scale = 1.0, the minimum - scale = 1.0, the maximum - scale = 1.0, user - scalable = no'}}}Copy the code

The error object

The props object in the error page takes an error object that contains at least two properties, statusCode and Message.

In addition to these two attributes, we can pass other attributes, which brings us back to 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 {
        // This adds the query attribute to the error object
        error({
          statusCode: 404.message: 'Label does not exist',
          query 
        })
        return}})return {
      tagInfo
    }
  }
}
Copy the code

And the validate lifecycle of the page:

export default {
  async validate ({ params, store }) {
    throw new Error('Incorrect page parameter')}}Copy the code

In this case, the statusCode passed is 500, and message is what’s in new Error. If you want to pass an object, the message will be turned into a string [object object]. You can pass it using json.stringify, and the error page will parse it out.

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

Encapsulate the bottom event

Almost every page in the project will have a touch bottom event, so I extracted this logic into a mixin, and the required page can be introduced and used.

/mixins/reachBottom.js :

export default {
  data() {
    return {
      _scrollingElement: null._isReachBottom: false.// Prevent repeated firing when entering the execution area
      reachBottomDistance: 80 // How far from the bottom to 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
      }
      // The bottom event is triggered
      if (currentHeight >= scrollHeight) {
        this._isReachBottom = true
        typeof this.reachBottom === 'function' && this.reachBottom()
      }
    }
  },
}
Copy the code

The core of the implementation, of course, is the timing of the trigger: scrollTop (page scrolling distance) + clientHeight (page visible height) >= scrollHeight (total page height, including scrolling area). But this requires a full bottom to trigger the event, so on top of that, I add reachBottomDistance to control the distance at which the event is triggered. Finally, the triggering event calls the reachBottom method of the page methods.

Imperative popover assembly

What is an imperative component? The Message component of the Elemental-UI is a good example. When we need a popover, we just call this.message() instead of switching components via V-if. The advantage of this is that there is no component to introduce, it is easy to use, and where needs to be adjusted.

In the Nuxt-Juejin-project project I also encapsulate two common popover components, the login popover and the Preview large image popover. The technical point is to mount the components manually. The implementation code is not much, just a few lines.

define

/components/common/picturesModal/picturesModal.vue :

export default {
  data() {
    return {
      url: ' '.// Link to the current image
      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 the popover
    hideModal() {
      typeof this.cb === 'function' && this.cb()
      document.body.removeChild(this.$el)
      document.body.style.overflow = ' '
      // Destroy the component instance
      this.$destroy()
    },
    // Close the popover
    cancel() {
      this.reject()
      this.hideModal()
    },
  }
}
Copy the code

/components/common/picturesModal/index.js

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

let componentInstance = null

// Construct subclasses
let ModalConstructor = Vue.extend(picturesModal)

function createModal(options) {
  // Instantiate the component
  componentInstance = new ModalConstructor()
  // Merge options
  Object.assign(componentInstance, options)
  // $mount can be passed a selector string to indicate mount to the selector
  // If you don't pass in the selector and render as an element outside the document, you can imagine document.createElement() generating the DOM in memory
  // $el gets the DOM element
  document.body.appendChild(componentInstance.$mount().$el)
}

function caller (options) {
  // The singleton global has only one popover
  if(! componentInstance) { createModal(options)// The callback passed to the show method in the component is called when the component is destroyed
    return componentInstance.show(() = > { componentInstance = null}}})export default {
  install(Vue) {
    // Register the popup method, which returns Promise then for successful login catch to close the popup
    Vue.prototype.$picturesModal = caller
  }
}
Copy the code

use

/ plugins/vue – global. Js:

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

Vue.use(picturesModal)
Copy the code

The objects passed in are the options parameters received by the createModal, which are merged into the data overwritten by the component.

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

Intermediate layer technology point

The general flow of the middle tier’s work is that the front end sends the request to the middle tier, and the middle tier sends the request to the back end to get the data. The advantage of this is that we gain control of the agent in the interaction from front end to back end. There’s more we can do with that power. Such as:

  • Agents: In a development environment, we can use agents to solve the most common cross-domain problems; In an online environment, we can use proxies to forward requests to multiple servers.
  • Caching: Caching is actually a requirement closer to the front end. User actions trigger data updates. The node middle tier can directly handle some of the caching requirements.
  • Logging: Logs in the middle tier of node are used to locate problems more easily and quickly than in other server-side languages.
  • Monitoring: Good at high concurrency request processing, monitoring is also a good option.
  • Data processing: Return the required data, data field aliases, data aggregation.

The existence of the middle layer also makes the responsibilities of the front and back end more thoroughly separated. The back end only needs to manage the data and write the interface, and the data that needs to be processed by the middle layer.

The middle layer of nuxt-Juejin-project project uses the KOA framework. The HTTP request method of the middle layer is simple encapsulation based on the request library, and the code is implemented in /server/request/index.js. I’ll mention it here because I need it later.

Forward requests

Installing middleware

npm i koa-router koa-bodyparser --save
Copy the code

Koa-router: Router middleware that can quickly define and manage routes

Koa-bodyparser: Parameter parsing middleware that supports parsing JSON and form types. It is commonly used for parsing POST requests

The usage method of related middleware is searched on NPM, and how to use it is not described here

Routing design

As the so-called no rules, no circumference, routing design specifications, I refer to the Teacher Ruan Yifeng RESTful API design guide.

Routing directory

I will store the routing file in the /server/routes directory. According to the specification, a folder specifying the API version number is also required. The final routing file is stored in /server/routes/v1.

Routing path

In a RESTful architecture, each URL represents a resource, so there cannot be a verb in the url, only a noun, and the noun often corresponds to the name of the table in the database. In general, tables in a database are “collections” of the same records, so nouns in the API should also be plural.

Such as:

  • Article related interface file namedarticles
  • Label related interface files are named astag
  • Boiling point related interface file namedpins

Routing type

The specific type of routing operation resource, represented by an HTTP verb

  • GET (SELECT) : Retrieves one or more resources from the server.
  • POST (CREATE) : Creates a resource on the server.
  • PUT (UPDATE) : UPDATE the resource on the server (the client provides the full resource after the change).
  • DELETE (DELETE) : Deletes resources from the server.

The routing logic

Here is an example of a user column list interface

/server/routes/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- createdAt the last entry, pass * on the next page@param {number} limit- a number *@param {string} order- rankIndex: popular, 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 obtained data 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
Copy the code

Registered routing

/server/index.js is a good server entry 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 shows only 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()

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

// Register the 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()
Copy the code

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

Automatic Route Registration

Yes, it’s coming again. Automation is incense, once and for all can not incense.

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 the route
function useRouter(path){
  path = path || __dirname + '/routes'
  // Retrieve all the file names in the routes directory, with urls as an array of file names
  let urls = fs.readdirSync(path)
  urls.forEach((element) = > {
    const elementPath = path + '/' + element
    const stat = fs.lstatSync(elementPath);
    // Whether it is a folder
    const isDir = stat.isDirectory();
    // Folder recursively registered route
    if (isDir) {
      useRouter(elementPath)
    } else {
      let module = require(elementPath)
      let routeRrefix = path.split('/routes') [1] | |' '
      // The file name in routes is used as the 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()
Copy the code

The above code takes routes as the home directory of the route, searches for the JS file to register the route, and finally takes the JS file path as the route name. / server/routes, for example, / v1 / articles. A search interface in the js/search, then the interface call address for localhost: 8000 / v1 / articles/search.

Route Parameter Verification

Parameter validation is a mandatory feature in interfaces, and incorrect parameters can cause unexpected errors in the program. We should verify the parameters ahead of time, stop the wrong query and inform the user. In the project, I encapsulated a routing middleware based on async-Validator to validate parameters. If you don’t know the workflow of KOA middleware, it’s worth learning about the Onion model.

define

/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 the parameter
    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]
      }
    })
    // Verify the parameters
    const errors = await validator.validate(params)
      .then(() = > null)
      .catch(err= > err.errors)
    // Return an error if the validation fails
    if (errors) {
      ctx.body = {
        s: 0,
        errors
      }
    } else {
      await next()
    }
  }
}
Copy the code

use

For details, see 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- createdAt the last entry, pass * on the next page@param {number} limit- a number *@param {string} order- rankIndex: popular, 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: 'Pass in a positive integer for limit'
  },
  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
Copy the code

Type indicates the parameter type. Required indicates whether this parameter is mandatory. When type is enum (enumeration), the parameter value can only be an item in the enum array.

Note that the number type is not verifiable here because the argument is converted to a string type during transmission. However, we can customize the validation rules with the validator method, just like the limit argument above.

Here’s what the interface returns if the limit argument is wrong:

Website security

cors

Setting up CORS to verify the security validity of requests can increase the security of your site. With KOA2-CORS, we can do this more easily. Koa2-cors source code is not much, it is recommended to see, as long as you have a little basic can understand, not only to know how to use also want to know the implementation process.

The installation

npm install koa2-cors --save
Copy the code

use

/server/index.js :

const cors = require('koa2-cors')

function useMiddleware(){
  app.use(helmet())
  app.use(bodyParser())
  // Set the 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 header validation
    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'],}}))Copy the code

If not in the requested manner, or with an unpermitted header. When sending the request, it will directly fail, and the browser will throw the error of CORS policy restriction. Here is an example with an unallowed header error:

koa-helmet

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

The installation

npm install koa-helmet --save
Copy the code

use

const helmet = require('koa-helmet')

function useMiddleware(){
  app.use(helmet())
  / /...
}
Copy the code

The following security Settings are made for us by default:

  • X-dns-prefetch -Control: disables the browserDNSPrefetch.
  • X-frame-options: Mitigate click-hijacking attacks.
  • X-powered-by: DeletedX-Powered-ByHeader to make it more difficult for an attacker to view the technology that potentially makes the site vulnerable.
  • Strict-transport-security: enables your users to use itHTTPS.
  • X – Download – Options: to preventInternet ExplorerPerform the download in the context of your site.
  • X-content-type-options: set tonosniffHelps prevent browsers from trying to guess (” sniff “)MIMEType, which can be a security concern.
  • X-xss-protection: To prevent reflectionXSSAttack.

More instructions and configuration stamp here www.npmjs.com/package/koa…

The last

I feel that the relevant knowledge of the middle layer is still not complete, there are a lot of things to do, or continue to learn. The project will be updated for some time to come.

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

πŸ˜„ see here still don’t come a little star? Github.com/ChanWahFung…

The resources

  • Other common questions: www.nuxtjs.cn/faq

  • Official Github documentation: github.com/nuxt/docs/t… (There is a comprehensive configuration and example to use, the part is not mentioned in the nuxT.js documentation, it is recommended to see)