当前位置:首页 > 通信资讯 > 正文

nuxt 性能(nuxt好用吗)

nuxt 性能(nuxt好用吗)

引文

最近公司项目中使用了 Nuxt 框架,进行首屏的服务端渲染,加快了内容的到达时间 (time-to-content),于是笔者开始了对 Nuxt 的学习和使用。以下是从源码角度对 Nuxt 的一些特性的介绍和分析。

FEATURES

服务端渲染(SSR)

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。 ------Vue SSR 指南

官方Vue SSR指南的基本用法章节,给出了 demo 级别的服务端渲染实现,Nuxt 也是基于该章节实现的,大体流程几乎一致。建议先食用官方指南,再看本文定大有裨益。

Nuxt 作为一个服务端渲染框架,了解其服务端渲染的实现原理必然是重中之重,就让我们通过相关源码,看看其具体实现吧!

我们通过 nuxt 启动 Nuxt 项目,其首先会执行 startDev 方法,然后调用_listenDev 方法,获取 Nuxt 配置,调用getNuxt方法实例化 Nuxt。然后执行 nuxt.ready() 方法,生成渲染器。

  1. //@nuxt/server/src/server.js
  2. asyncready(){
  3. //Initializevue-renderer
  4. this.serverContext=newServerContext(this)
  5. this.renderer=newVueRenderer(this.serverContext)
  6. awaitthis.renderer.ready()
  7. //Setupnuxtmiddleware
  8. awaitthis.setupMiddleware()
  9. returnthis
  10. }

在 ready 中会执行 this.setupMiddleware() ,其中会调用nuxtMiddleware 中间件(这里是响应的关键)。

  1. //@nuxt/server/src/middleware/nuxt.js
  2. exportdefault({options,nuxt,renderRoute,resources})=>asyncfunctionnuxtMiddleware(req,res,next){
  3. constcontext=getContext(req,res)
  4. try{
  5. consturl=normalizeURL(req.url)
  6. res.statusCode=200
  7. constresult=awaitrenderRoute(url,context)//渲染相应路由,后文会展开
  8. const{
  9. html,
  10. redirected,
  11. preloadFiles
  12. }=result//得到html
  13. //设置头部字段
  14. res.setHeader('Content-Type','text/html;charset=utf-8')
  15. res.setHeader('Accept-Ranges','none')
  16. res.setHeader('Content-Length',Buffer.byteLength(html))
  17. res.end(html,'utf8')//做出响应
  18. returnhtml
  19. }catch(err){
  20. if(context&&context.redirected){
  21. consola.error(err)
  22. returnerr
  23. }
  24. next(err)
  25. }
  26. }

nuxtMiddleware 中间件中首先标准化请求的url,设置请求状态码,通过url匹配到相应的路由,渲染出对应的路由组件,设置头部信息,最后做出响应。

  1. renderSSR(renderContext){
  2. //CallrenderToStringfromthebundleRendererandgeneratetheHTML(willupdatetherenderContextaswell)
  3. //renderSSR只是universalapp的渲染方法,Nuxt也可以进行开发普通的SPA项目
  4. constrenderer=renderContext.modern?this.renderer.modern:this.renderer.SSR
  5. returnrenderer.render(renderContext)
  6. }

其中 renderRoute 方法会调用 @nuxt/vue-render 的renderSSR 进行服务端渲染操作。

  1. //@nuxt/vue-renderer/src/renderers/SSR.js
  2. asyncrender(renderContext){
  3. //CallVuerendererrenderToString
  4. letAPP=awaitthis.vueRenderer.renderToString(renderContext)
  5. letHEAD=''
  6. //...此处省略n行HEAD拼接代码,后续HEAD管理部分会提及
  7. //RenderwithSSRtemplate
  8. consthtml=this.renderTemplate(this.serverContext.resources.SSRTemplate,templateParams)
  9. return{
  10. html,
  11. preloadFiles
  12. }
  13. }

而 renderSSR 又会调用 renderer.render 方法,将 url 匹配的路由渲染成字符串,将字符串与模版相结合,得到最终返回给浏览器的html,至此 Nuxt 服务端渲染完成。

最后贴一张盗来的 Nuxt 执行流程图,图画的很棒,流程也很清晰,感谢。

nuxt 性能(nuxt好用吗)

数据拉取(Data Fetching)

在客户端程序(CSR)可以通过在 mounted 钩子中获取数据,但在通用程序(Universal)中则需要使用特定的钩子才能在服务端获取数据。

Nuxt 中主要提供了两种钩子获取数据

  • asyncData
    • 只可以在页面级组件中获取,不可以访问 this
    • 通过返回对象保存数据状态或与Vuex配合进行状态保存
  • fetch
    • 所有组件中都可以获取,可以访问 this
    • 无需传入 context,传入 context 会 fallback 到老版的 fetch,功能类似于 asyncData
  1. //.nuxt/server.js
  2. //ComponentsarealreadyresolvedbysetContext->getRouteData(app/utils.js)
  3. constComponents=getMatchedComponents(app.context.route)
  4. //在匹配的路由中,调用asyncData和legacy版本的fetch方法
  5. constasyncDatas=awaitPromise.all(Components.map((Component)=>{
  6. constpromises=[]
  7. //调用asyncData(context)
  8. if(Component.options.asyncData&&typeofComponent.options.asyncData==='function'){
  9. constpromise=promisify(Component.options.asyncData,app.context)
  10. promise.then((asyncDataResult)=>{
  11. SSRContext.asyncData[Component.cid]=asyncDataResult
  12. applyAsyncData(Component)
  13. returnasyncDataResult
  14. })
  15. promises.push(promise)
  16. }else{
  17. promises.push(null)
  18. }
  19. //调用legacy版本的fetch(context)兼容老版本的fetch
  20. if(Component.options.fetch&&Component.options.fetch.length){
  21. promises.push(Component.options.fetch(app.context))
  22. }else{
  23. promises.push(null)
  24. }
  25. returnPromise.all(promises)
  26. }))

在生成的 .nuxt/server.js 中,会遍历匹配的组件,查看组件中是否定义了 asyncData 选项以及 legacy 版 fetch ,存在就依次调用,获得 asyncDatas。

  1. //.nuxt/mixins/fetch.server.js
  2. //nuxtv2.14及之后
  3. asyncfunctionserverPrefetch(){
  4. //Callandawaiton$fetch
  5. //v2.14之后推荐的fetch
  6. try{
  7. awaitthis.$options.fetch.call(this)
  8. }catch(err){
  9. if(process.dev){
  10. console.error('Errorinfetch():',err)
  11. }
  12. }
  13. this.$fetchState.pending=false//设置fetchState为false
  14. }

在服务端实例化 vue 实例之后,执行 serverPrefetch,触发 fetch 选项方法,获取数据,数据会作用于生成 html的过程。

HEAD 管理(Meta Tags and SEO)

截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。但是对于异步获取数据的网站来说,主流的搜索引擎暂时还无法支持,于是造成网站搜索排名靠后,于是希望获得更好的SEO成为众多网站考虑使用SSR框架的原因。

为了获得良好的SEO,那么就需要对HEAD进行精细化的配置和管理。让我们看看其是如何实现的吧~

Nuxt框架借助 vue-meta 库实现全局、单个页面的 meta 标签的自定义。Nuxt 内部的实现也几乎遵循 vue-meta 官方的 SSR meta 管理的流程。具体详情请查看。

  1. //@nuxt/vue-app/template/index.js
  2. //step1
  3. Vue.use(Meta,JSON.stringify(vueMetaOptions))
  4. //@nuxt/vue-app/template/template.js
  5. //step2
  6. exportdefaultasync(SSRContext)=>{
  7. const_app=newVue(app)
  8. //Addmetainfos(usedinrenderer.js)
  9. SSRContext.meta=_app.$meta()
  10. return_app
  11. }

首先通过Vue插件的形式,注册vue-meta,内部会在Vue的原型上挂载$meta属性。然后将meta添加到服务端渲染上下文中。

  1. asyncrender(renderContext){
  2. //CallVuerendererrenderToString
  3. letAPP=awaitthis.vueRenderer.renderToString(renderContext)
  4. //step3
  5. letHEAD=''
  6. //Injectheadmeta
  7. //(thisisunsetwhenfeatures.metaisfalseinservertemplate)
  8. //以下就是上文省略的n行HEAD拼接代码,可以适当忽略
  9. //了解主要过程即可,具体细节按需查看
  10. constmeta=renderContext.meta&&renderContext.meta.inject({
  11. isSSR:renderContext.nuxt.serverRendered,
  12. ln:this.options.dev
  13. })
  14. if(meta){
  15. HEAD+=meta.title.text()+meta.meta.text()
  16. }
  17. if(meta){
  18. HEAD+=meta.link.text()+
  19. meta.style.text()+
  20. meta.script.text()+
  21. meta.noscript.text()
  22. }
  23. //Checkifweneedtoinjectscriptsandstate
  24. constshouldInjectScripts=this.options.render.injectScripts!==false
  25. //Injectresourcehints
  26. if(this.options.render.resourceHints&&shouldInjectScripts){
  27. HEAD+=this.renderResourceHints(renderContext)
  28. }
  29. //Injectstyles
  30. HEAD+=this.renderStyles(renderContext)
  31. //Prependscripts
  32. if(shouldInjectScripts){
  33. APP+=this.renderScripts(renderContext)
  34. }
  35. if(meta){
  36. constappendInjectorOptions={body:true}
  37. //Appendbodyscripts
  38. APP+=meta.meta.text(appendInjectorOptions)
  39. APP+=meta.link.text(appendInjectorOptions)
  40. APP+=meta.style.text(appendInjectorOptions)
  41. APP+=meta.script.text(appendInjectorOptions)
  42. APP+=meta.noscript.text(appendInjectorOptions)
  43. }
  44. //Templateparams
  45. consttemplateParams={
  46. HTML_ATTRS:meta?meta.htmlAttrs.text(renderContext.nuxt.serverRendered/*addSrrAttribute*/):'',
  47. HEAD_ATTRS:meta?meta.headAttrs.text():'',
  48. BODY_ATTRS:meta?meta.bodyAttrs.text():'',
  49. HEAD,
  50. APP,
  51. ENV:this.options.env
  52. }
  53. //RenderwithSSRtemplate
  54. //通过模版和参数生成html
  55. consthtml=this.renderTemplate(this.serverContext.resources.SSRTemplate,templateParams)
  56. letpreloadFiles
  57. if(this.options.render.http2.push){
  58. //获取需要预加载的文件
  59. preloadFiles=this.getPreloadFiles(renderContext)
  60. }
  61. return{
  62. html,
  63. preloadFiles,
  64. }
  65. }

最后在响应的 html 中注入 metadata 即可。

文件系统路由(File System Routing)

想必使用过 Nuxt 的同学应该都对其基于文件生成路由的特性,印象深刻。让我从源码角度看看 Nuxt 是如何实现基于 pages 目录(可配置),自动生成路由的。

首先在启动 Nuxt 项目或者修改文件时,会自动调 generateRoutesAndFiles 方法,生成路由 以及 .nuxt 目录下的文件。

  1. //@nuxt/builder/src/builder.js
  2. asyncgenerateRoutesAndFiles(){
  3. ...
  4. awaitPromise.all([
  5. this.resolveLayouts(templateContext),
  6. this.resolveRoutes(templateContext),//解析生成路由,需要关注的重点
  7. this.resolveStore(templateContext),
  8. this.resolveMiddleware(templateContext)
  9. ])
  10. ...
  11. }

解析路由会存在三种情况:一是修改了默认的 pages 目录名称,且未在 nuxt.config.js 中配置相关目录,二是使用 nuxt 默认的 pages 目录,三是使用调用用户自定义的路由生成方法生成路由。

  1. //@nuxt/builder/src/builder.js
  2. asyncresolveRoutes({templateVars}){
  3. consola.debug('Generatingroutes...')
  4. if(this._defaultPage){
  5. //在srcDir下未找到pages目录
  6. }elseif(this._nuxtPage){
  7. //使用nuxt动态生成路由
  8. }else{
  9. //用户提供了自定义方法去生成路由,提供用户自定义路由的能力
  10. }
  11. //router.extendRoutesmethod
  12. if(typeofthis.options.router.extendRoutes==='function'){
  13. constextendedRoutes=awaitthis.options.router.extendRoutes(
  14. templateVars.router.routes,
  15. resolve
  16. )
  17. if(extendedRoutes!==undefined){
  18. templateVars.router.routes=extendedRoutes
  19. }
  20. }
  21. }

除此之外,还可以提供相应的 extendRoutes 方法,在 nuxt 生成路由的基础上添加自定义路由。

  1. exportdefault{
  2. router:{
  3. extendRoutes(routes,resolve){
  4. //例如添加404页面
  5. routes.push({
  6. name:'custom',
  7. path:'*',
  8. component:resolve(__dirname,'pages/404.vue')
  9. })
  10. }
  11. }
  12. }

其中当修改了默认的 pages 目录,导致找不到相关的目录,会使用 @nuxt/vue-app/template/pages/index.vue 文件生成路由。

  1. asyncresolveRoutes({templateVars}){
  2. if(this._defaultPage){
  3. //createRoutes方法根据传参,生成路由。具体算法,不再展开
  4. templateVars.router.routes=createRoutes({
  5. files:['index.vue'],
  6. srcDir:this.template.dir+'/pages',//指向@nuxt/vue-app/template/pages/index.vue
  7. routeNameSplitter,//路由名称分隔符,默认`-`
  8. trailingSlash//尾斜杠/
  9. })
  10. }elseif(this._nuxtPage){
  11. constfiles={}
  12. constext=newRegExp(`\\.(${this.supportedExtensions.join('|')})$`)
  13. for(constpageofawaitthis.resolveFiles(this.options.dir.pages)){
  14. constkey=page.replace(ext,'')
  15. //.vuefiletakesprecedenceoverotherextensions
  16. if(/\.vue$/.test(page)||!files[key]){
  17. files[key]=page.replace(/(['"])/g,'\\$1')
  18. }
  19. }
  20. templateVars.router.routes=createRoutes({
  21. files:Object.values(files),
  22. srcDir:this.options.srcDir,
  23. pagesDir:this.options.dir.pages,
  24. routeNameSplitter,
  25. supportedExtensions:this.supportedExtensions,
  26. trailingSlash
  27. })
  28. }else{
  29. templateVars.router.routes=awaitthis.options.build.createRoutes(this.options.srcDir)
  30. }
  31. //router.extendRoutesmethod
  32. if(typeofthis.options.router.extendRoutes==='function'){
  33. constextendedRoutes=awaitthis.options.router.extendRoutes(
  34. templateVars.router.routes,
  35. resolve
  36. )
  37. if(extendedRoutes!==undefined){
  38. templateVars.router.routes=extendedRoutes
  39. }
  40. }
  41. }

然后就是调用 createRoutes 方法,生成路由。生成的路由大致长这样,和手动书写的路由文件几乎一致(后续还会进行打包??,懒加载引入路由组件)。

  1. [
  2. {
  3. name:'index',
  4. path:'/',
  5. chunkName:'pages/index',
  6. component:'Users/username/projectName/pages/index.vue'
  7. },
  8. {
  9. name:'about',
  10. path:'/about',
  11. chunkName:'pages/about/index',
  12. component:'Users/username/projectName/pages/about/index.vue'
  13. }
  14. ]

智能预取(Smart Prefetching)

从 Nuxt v2.4.0 开始,当 出现在可视区域后,Nuxt将会预取经过code-splitted的 page 页面的脚本,使得在用户点击之前,该路由指向的地址,就处于 ready 状态,这将极大的提升用户的体验。

相关实现逻辑集中于 .nuxt/components/nuxt-link.client.js 中。

首先 Smart Prefetching 特性的实现依赖于window.IntersectionObserver 这个实验性的 API,如果浏览器不支持该 API,就不会进行组件预取操作。

  1. mounted(){
  2. if(this.prefetch&&!this.noPrefetch){
  3. this.handleId=requestIdleCallback(this.observe,{timeout:2e3})
  4. }
  5. }

然后在需要预取的 组件挂载阶段,会调用 requestIdleCallback 方法在浏览器的空闲时段内调用 observe 方法。

  1. constobserver=window.IntersectionObserver&&newwindow.IntersectionObserver((entries)=>{
  2. entries.forEach(({intersectionRatio,target:link})=>{
  3. //如果intersectionRatio小于等于0,表示目标不在viewport内
  4. if(intersectionRatio<=0||!link.__prefetch){
  5. return
  6. }
  7. //进行预取数据(其实就是加载组件)
  8. link.__prefetch()
  9. })
  10. })

当被监听的元素的可视情况发生改变的时候(且出现在视图内时),会触发 new window.IntersectionObserver(callback) 的回调,执行真正的预取操作prefetchLink。

  1. prefetchLink(){
  2. //判断网络环境,离线或者2G环境下,不进行预取操作
  3. if(!this.canPrefetch()){
  4. return
  5. }
  6. //停止监听该元素,提高性能
  7. observer.unobserve(this.$el)
  8. constComponents=this.getPrefetchComponents()
  9. for(constComponentofComponents){
  10. //及时加载组件,使得用户点击时,该组件是一个就绪的状态
  11. constcomponentOrPromise=Component()
  12. if(componentOrPromiseinstanceofPromise){
  13. componentOrPromise.catch(()=>{})
  14. Component.__prefetched=true//已经预取的标志位
  15. }
  16. }

总结

上文从源码角度介绍了 Nuxt 服务端渲染的实现、服务端数据的获取以及 Nuxt 开箱即用的几个特性:HEAD 管理、基于文件系统的路由和智能预取 code-splitted 的路由。如果希望对 SSR 进行更深入研究,还可以横向学习 React 的 SSR 实现 Next 框架。

希望对您有所帮助,如有纰漏,望请辅正。

参考

为什么使用服务器端渲染 (SSR)?

Nuxt源码精读

Vue Meta

Introducing Smart prefetching

服务端渲染

原文链接:https://mp.weixin.qq.com/s/fvv12ZPxiEpCzER3Q-X5pQ

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。