citation

Recently, Nuxt framework was used in the company’s project to render the server side of the first screen, which accelerated the time-to-content. Therefore, THE author started to learn and use Nuxt. The following is an introduction and analysis of Nuxt’s features from a source point of view.

FEATURES

Server Side Rendering (SSR)

Vue.js is a framework for building client applications. By default, the Vue component can be exported to the browser for DOM generation and DOM manipulation. However, it is also possible to render the same component as HTML strings on the server side, send them directly to the browser, and finally “activate” these static tags into a fully interactive application on the client side. — — — — — – Vue SSR guide

The basic usage section of the official Vue SSR guide gives the demo level server rendering implementation, and Nuxt is also based on this section. The general process is almost the same. It would be helpful to eat the official guidelines before reading this article.

Nuxt as a server rendering framework, understanding the implementation principle of its server rendering is bound to be a top priority, let us through the relevant source code, see its specific implementation!

We start the nuxt project with nuxt, which first executes the startDev method, then calls the _listenDev method to get the NUxT configuration, and the getNuxt method to instantiate nuxT. The nuxt.ready() method is then executed to generate the renderer.

// @nuxt/server/src/server.js
async ready () {
  // Initialize vue-renderer
  this.serverContext = new ServerContext(this)
  this.renderer = new VueRenderer(this.serverContext)
  await this.renderer.ready()

  // Setup nuxt middleware
  await this.setupMiddleware()

  return this
}
Copy the code

This.setupmiddleware () is executed in Ready, which calls the nuxtMiddleware (which is the key to the response).

// @nuxt/server/src/middleware/nuxt.js
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
  const context = getContext(req, res)
  try {
    const url = normalizeURL(req.url)
    res.statusCode = 200
    const result = await renderRoute(url, context) // Render the corresponding route, which will be expanded later
    
    const {
      html,
      cspScriptSrcHashes,
      error,
      redirected,
      preloadFiles
    } = result

    // Send response
    res.setHeader('Content-Type'.'text/html; charset=utf-8')
    res.setHeader('Accept-Ranges'.'none')
    res.setHeader('Content-Length', Buffer.byteLength(html))
    res.end(html, 'utf8')
    return html
  } catch (err) {
    if (context && context.redirected) {
      consola.error(err)
      return err
    }
    next(err)
  }
}
Copy the code

NuxtMiddleware first standardizes the URL of the request, sets the request status code, matches the corresponding route through the URL, renders the corresponding route component, sets the header information, and finally responds.

renderSSR (renderContext) {
  // Call renderToString from the bundleRenderer and generate the HTML (will update the renderContext as well)
  const renderer = renderContext.modern ? this.renderer.modern : this.renderer.ssr
  return renderer.render(renderContext)
}
Copy the code

RenderRoute method will call renderSSR of @nuxt/vue-render for server-side rendering operation.

// @nuxt/vue-renderer/src/renderers/ssr.js
async render (renderContext) {
  // Call Vue renderer renderToString
  let APP = await this.vueRenderer.renderToString(renderContext)

  let HEAD = ' '
  / /... Omit n lines of HEAD concatenation code here
    
  // Render with SSR template
  const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams)

  return {
    html,
    preloadFiles
  }
}
Copy the code

RenderSSR calls renderer.render to render the url matching route into a string, which is then combined with the template to get the HTML returned to the browser. Nuxt server rendering is complete.

Finally post a stolen Nuxt implementation flow chart, the picture is very good, the process is also very clear, thanks to 😁😉👍

Data Fetching

Mounted hooks are used to retrieve data in a CSR program, but in a Universal program, a specific hook is used to retrieve data from a server.

Nuxt provides two main hooks for retrieving data

  • asyncData
    • It is only available in the page-level component, not accessiblethis
    • Data state is saved by returning objects or in conjunction with Vuex
  • fetch
    • All components are available and accessiblethis
    • No incomingcontextPassing the context willfallbackTo the older version of Fetch, the function is similar to asyncData
// .nuxt/server.js
// Components are already resolved by setContext -> getRouteData (app/utils.js)
const Components = getMatchedComponents(app.context.route)
  
// Call asyncData & fetch hooks on components matched by the route.
const asyncDatas = await Promise.all(Components.map((Component) = > {
  const promises = []

  // Call asyncData(context)
  if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
    const promise = promisify(Component.options.asyncData, app.context)
    promise.then((asyncDataResult) = > {
      ssrContext.asyncData[Component.cid] = asyncDataResult
      applyAsyncData(Component)
      return asyncDataResult
    })
    promises.push(promise)
    } else {
      promises.push(null)}// Call fetch(context)
    if (Component.options.fetch && Component.options.fetch.length) {
      promises.push(Component.options.fetch(app.context))
    } else {
    promises.push(null)}return Promise.all(promises)
}))
Copy the code

In the generated.nuxt/server.js, the matching components will be traversed to check whether asyncData option and legacy version fetch are defined in the component. If asyncDatas exists, the asyncDatas will be called successively.

// .nuxt/mixins/fetch.server.js 
// Nuxt v2.14 and later
async function serverPrefetch() {
  // Call and await on $fetch
  try {
    await this.$options.fetch.call(this)}catch (err) {
    if (process.dev) {
      console.error('Error in fetch():', err)
    }
    this.$fetchState.error = normalizeError(err)
  }
  this.$fetchState.pending = false
}
Copy the code

After the server instantiates the Vue instance, execute serverPrefetch, which triggers the FETCH option method to fetch the data that will be used in the HTML generation process.

HEAD Management Meta Tags and SEO

So far, Google and Bing have done a good job indexing synchronous JavaScript applications. However, for sites that obtain data asynchronously, mainstream search engines cannot support it for the time being, which leads to a lower ranking in website search. Therefore, many websites consider using SSR framework in the hope of obtaining better SEO.

In order to achieve good SEO, HEAD needs to be finely configured and managed. Let’s see how it works

Nuxt framework with vuE-meta library to achieve global, single page meta tag customization. Nuxt’s internal implementation almost follows vuE-Meta’s official SSR meta management process. Please check out the details.

// @nuxt/vue-app/template/index.js
// step1
Vue.use(Meta, JSON.stringify(vueMetaOptions))

// @nuxt/vue-app/template/template.js
// step2
export default async (ssrContext) => {
  const _app = new Vue(app)
  // Add meta infos (used in renderer.js)
  ssrContext.meta = _app.$meta()
  return _app
}
Copy the code

First, we register vue-meta in the form of a Vue plug-in. The $meta attribute is mounted internally on the Vue prototype. Meta is then added to the server-side rendering context.

async render (renderContext) {
    // Call Vue renderer renderToString
    let APP = await this.vueRenderer.renderToString(renderContext)
    // step3
    let HEAD = ' '

    // Inject head meta
    // (this is unset when features.meta is false in server template)
    const meta = renderContext.meta && renderContext.meta.inject({
      isSSR: renderContext.nuxt.serverRendered,
      ln: this.options.dev
    })

    if (meta) {
      HEAD += meta.title.text() + meta.meta.text()
    }

    if (meta) {
      HEAD += meta.link.text() +
        meta.style.text() +
        meta.script.text() +
        meta.noscript.text()
    }

    // Check if we need to inject scripts and state
    const shouldInjectScripts = this.options.render.injectScripts ! = =false

    // Inject resource hints
    if (this.options.render.resourceHints && shouldInjectScripts) {
      HEAD += this.renderResourceHints(renderContext)
    }

    // Inject styles
    HEAD += this.renderStyles(renderContext)


    // Prepend scripts
    if (shouldInjectScripts) {
      APP += this.renderScripts(renderContext)
    }

    if (meta) {
      const appendInjectorOptions = { body: true }
      // Append body scripts
      APP += meta.meta.text(appendInjectorOptions)
      APP += meta.link.text(appendInjectorOptions)
      APP += meta.style.text(appendInjectorOptions)
      APP += meta.script.text(appendInjectorOptions)
      APP += meta.noscript.text(appendInjectorOptions)
    }

    // Template params
    const templateParams = {
      HTML_ATTRS: meta ? meta.htmlAttrs.text(renderContext.nuxt.serverRendered /* addSrrAttribute */) : ' '.HEAD_ATTRS: meta ? meta.headAttrs.text() : ' '.BODY_ATTRS: meta ? meta.bodyAttrs.text() : ' ',
      HEAD,
      APP,
      ENV: this.options.env
    }

    // Render with SSR template
    const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams)

    let preloadFiles
    if (this.options.render.http2.push) {
      preloadFiles = this.getPreloadFiles(renderContext)
    }

    return {
      html,
      preloadFiles
    }
  }
Copy the code

Finally, inject metadata into the HTML of the response.

File System Routing

Those of you who have used Nuxt should be impressed by its file-generated routing features. Let me see from a source code perspective how Nuxt automatically generates routes based on the (configurable) Pages directory.

The generateRoutesAndFiles method is automatically called to generateRoutesAndFiles in the.nuxt directory when the Nuxt project is started or files are modified.

// @nuxt/builder/src/builder.js
async generateRoutesAndFiles(){...await Promise.all([
    this.resolveLayouts(templateContext),
    this.resolveRoutes(templateContext), // Parse to generate a route
    this.resolveStore(templateContext),
    this.resolveMiddleware(templateContext)
  ])
  ...
}
Copy the code

There are three cases of route resolution: the default pages directory name is changed and the relevant directory is not configured in nuxt.config.js; the default Pages directory is used in Nuxt; the user-defined route generation method is invoked to generate routes.

// @nuxt/builder/src/builder.js
async resolveRoutes({ templateVars }) {
  consola.debug('Generating routes... ')
  if (this._defaultPage) {
    // The pages directory was not found under srcDir
  } else if (this._nuxtPage) {
    // Use NUxT to dynamically generate routes
  } else {
    // The user provides custom methods to generate routes
  }
  // router.extendRoutes method
  if (typeof this.options.router.extendRoutes === 'function') {
    const extendedRoutes = await this.options.router.extendRoutes(
      templateVars.router.routes,
      resolve
    )
    if(extendedRoutes ! = =undefined) {
      templateVars.router.routes = extendedRoutes
    }
  }
}
Copy the code

In addition, a corresponding extendRoutes method can be provided to add custom routes on top of those generated by NUXT.

export default {
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        name: 'custom'.path: The '*'.component: resolve(__dirname, 'pages/404.vue')})}}}Copy the code

When changed the default pages directory, lead to can not find the related catalogue, will use the @ nuxt/vue – app/template/pages/index. The vue file generated routing.

async resolveRoutes({ templateVars }) {
  if (this._defaultPage) {
    templateVars.router.routes = createRoutes({
      files: ['index.vue'].srcDir: this.template.dir + '/pages'./ / points to @ nuxt/vue - app/template/pages/index. Vue
      routeNameSplitter, // Route name separator, default '-'
      trailingSlash Slash / / end})}else if (this._nuxtPage) {
    const files = {}
    const ext = new RegExp((` \ \.The ${this.supportedExtensions.join('|')}) $`)
    for (const page of await this.resolveFiles(this.options.dir.pages)) {
      const key = page.replace(ext, ' ')
      // .vue file takes precedence over other extensions
      if (/\.vue$/.test(page) || ! files[key]) { files[key] = page.replace(/(['"])/g.'\ \ $1')
      }
    }
    templateVars.router.routes = createRoutes({
      files: Object.values(files),
      srcDir: this.options.srcDir,
      pagesDir: this.options.dir.pages,
      routeNameSplitter,
      supportedExtensions: this.supportedExtensions,
      trailingSlash
    })
    } else {
      templateVars.router.routes = await this.options.build.createRoutes(this.options.srcDir)
    }
    // router.extendRoutes method
    if (typeof this.options.router.extendRoutes === 'function') {
      const extendedRoutes = await this.options.router.extendRoutes(
        templateVars.router.routes,
        resolve
      )
      if(extendedRoutes ! = =undefined) {
        templateVars.router.routes = extendedRoutes
      }
  }
}
Copy the code

The createRoutes method is then called to generate the route. The generated route looks like this, almost the same as the manually written route file (it will be packaged later 📦 and lazily loaded imported route components).

[{name: 'index'.path: '/'.chunkName: 'pages/index'.component: 'Users/username/projectName/pages/index.vue'
  },
  {
    name: 'about'.path: '/about'.chunkName: 'pages/about/index'.component: 'Users/username/projectName/pages/about/index.vue'}]Copy the code

Smart Prefetching

Starting from Nuxt V2.4.0, when

appears in the visible area, Nuxt will prefetch scripts from code-splitted page pages, making the addresses of the route to be in ready state before the user clicks. This will greatly improve the user experience.

The implementation logic is concentrated in.nuxt/components/nuxt-link.client.js.

The realization of the first Smart Prefetching feature depends on the window. The experimental IntersectionObserver API, if the browser does not support this API, wouldn’t Prefetching for components.

mounted () {
  if (this.prefetch && !this.noPrefetch) {
    this.handleId = requestIdleCallback(this.observe, { timeout: 2e3}}})Copy the code

The requestIdleCallback method is then called during the mount phase of the

component that requires prefetching and the Observe method is called during the browser’s idle time.

observe () {
  / / browser does not support Windows. IntersectionObserver, without prefetching
  if(! observer) {return
  }
  if (this.shouldPrefetch()) {
    this.$el.__prefetch = this.prefetchLink.bind(this)
    observer.observe(this.$el)
    this.__observed = true}}Copy the code

Observe. Observe (this.$el) In the observe method, we first determine whether prefetch is required (filter out prefetches and avoid duplicate pulls), then set prefetchLink to __prefetch and call observer.observe(this.$el) to listen for the current element

const observer = window.IntersectionObserver && new window.IntersectionObserver((entries) = > {
  entries.forEach(({ intersectionRatio, target: link }) = > {
    // If intersectionRatio is less than or equal to 0, the target is not in the viewport
    if (intersectionRatio <= 0| |! link.__prefetch) {return
    }
    link.__prefetch()
  })
})
Copy the code

When being monitored elements visible at the time of change situation (and appeared in the view), will trigger a new window. The IntersectionObserver (the callback) callback, perform real prefetching prefetchLink.

prefetchLink () {
  // Do not perform prefetch in offline or 2G environments
  if (!this.canPrefetch()) {
    return
  }
  // Stop listening on this element to improve performance
  observer.unobserve(this.$el)
  const Components = this.getPrefetchComponents()

  for (const Component of Components) {
    // Load the component in time so that when the user clicks, the component is in a ready state
    const componentOrPromise = Component()
    if (componentOrPromise instanceof Promise) {
      componentOrPromise.catch(() = >{}) ## Why server-side rendering (SSR)? Component.__prefetched =true // The prefetched flag bit}}Copy the code

conclusion

From the perspective of source code, the implementation of Nuxt server rendering, the acquisition of server data, and several features of Nuxt out of the box were introduced above: HEAD management, routing based on file system, and intelligent prefetch code-splitted routes. If you want to further study SSR, you can also learn React SSR to implement the Next framework horizontally.

Hope to help you, if there is a mistake, please supplement 😄.

reference

  • Why server side Rendering (SSR)?
  • Nuxt source intensive reading
  • Vue Meta
  • Introducing Smart prefetching
  • Server side rendering