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 accessible
this
- Data state is saved by returning objects or in conjunction with Vuex
- It is only available in the page-level component, not accessible
- fetch
- All components are available and accessible
this
- No incoming
context
Passing the context willfallback
To the older version of Fetch, the function is similar to asyncData
- All components are available and accessible
// .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