Nuxt源码浅析

来聊聊Nuxt源码。

聊聊启动nuxt项目

废话不多说,看官网一段Nuxt项目启动

js 复制代码
const { Nuxt, Builder } = require('nuxt')

const app = require('express')()
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 3000

// 用指定的配置对象实例化 Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)

// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)

// 在开发模式下启用编译构建和热加载
if (config.dev) {
  new Builder(nuxt).build().then(listen)
} else {
  listen()
}

function listen() {
  // 服务端监听
  app.listen(port, '0.0.0.0')
  console.log('Server listening on `localhost:' + port + '`.')
}

解读一下这段代码:

导入nuxt的Nuxt类和Builder类,然后用express创建一个node服务。

导入nuxt.config.js,使用导入的nuxt的config对象,创建nuxt实例: const nuxt = new Nuxt(config)

然后重点是 app.use(nuxt.render)。把nuxt.render作为node服务中间件使用即可。 到这里在生产上就可以运行了(生成前会先nuxt build)。

然后就是监听listen端口

所以到这里有2条线索,一个是:nuxt build的产物,自动生成路由。dist下的client和server资源文件是什么? 一个是,上面的服务,怎么会根据当前页面路径渲染出当期的html的。

你知道了,今天说的是第二条,来看看,nuxt是怎么渲染页面的,它做了什么nuxt到底是什么?

目录结构

下载好源码后来看下源码的核心目录结构:

a 复制代码
// 工程核心目录结构
├─ distributions   
    ├─ nuxt                 // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
    ├─ nuxt-start           // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json               // lerna配置文件
├─ package.json         
├─ packages                 // 工作目录
    ├─ babel-preset-app     // babel初始预设
    ├─ builder              // 根据路由构建动态当前页ssr资源,产出.nuxt资源
    ├─ cli                  // 脚手架命令入口
    ├─ config               // 提供加载nuxt配置相关的方法
    ├─ core                 //  Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
    ├─ generator            // Generato实例,生成前端静态资源(非SSR)
    ├─ server               // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
    ├─ types                // ts类型
    ├─ utils                // 工具类
    ├─ vue-app              // 存放Nuxt应用构建模版,即.nuxt文件内容
    ├─ vue-renderer         // 根据构建的SSR资源渲染html
    └─ webpack              // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock

Nuxt类在core下nuxt.js文件。来看看new Nuxt的主要代码:

js 复制代码
export default class Nuxt extends Hookable {
  constructor (options = {}) {
    super(consola)

    // Assign options and apply defaults
    this.options = getNuxtConfig(options)

    this.moduleContainer = new ModuleContainer(this)

    // Deprecated hooks
    this.deprecateHooks({
    })

    this.showReady = () => { this.callHook('webpack:done') }

    // Init server
    if (this.options.server !== false) {
      this._initServer()
    }

    // Call ready
    if (this.options._ready !== false) {
      this.ready().catch((err) => {
        consola.fatal(err)
      })
    }
  }


  ready () {
  }

  async _init () {
  }

  _initServer () {
  }
}

实例化nuxt的工作内容很简单:

  1. this.options = getNuxtConfig(options) nuxt.config.js对象合并 Nuxt默认对象
js 复制代码
// getDefaultNuxtConfig
export function getDefaultNuxtConfig (options = {}) {
  if (!options.env) {
    options.env = process.env
  }

  return {
    ..._app(),
    ..._common(),
    build: build(),
    messages: messages(),
    modes: modes(),
    render: render(),
    router: router(),
    server: server(options),
    cli: cli(),
    generate: generate()
  }
}

// config
...
const nuxtConfig = getDefaultNuxtConfig()
defaultsDeep(options, nuxtConfig)
...
  1. this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例
js 复制代码
export default class ModuleContainer {
  constructor (nuxt) {
    this.nuxt = nuxt
    this.options = nuxt.options
    this.requiredModules = {}

  }
}
  1. this._initServer() 来创建一个connect服务。
js 复制代码
  _initServer () {
    if (this.server) {
      return
    }
    this.server = new Server(this)
    this.renderer = this.server
    this.render = this.server.app
    defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
  }
js 复制代码
export default class Server {
  constructor (nuxt) {
    this.nuxt = nuxt
    this.options = nuxt.options

    this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)

    this.publicPath = isUrl(this.options.build.publicPath)
      ? this.options.build._publicPath
      : this.options.build.publicPath.replace(/^\.+\//, '/')

    // Runtime shared resources
    this.resources = {}

    // Will be set after listen
    this.listeners = []

    // Create new connect instance
    this.app = connect()

    // Close hook
    this.nuxt.hook('close', () => this.close())

    // devMiddleware placeholder
    if (this.options.dev) {
      this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
        this.devMiddleware = devMiddleware
      })
    }
  }
}

server很简单,使用connect创建了一个instance. 然后实例化一些参数。其中,我们发现nuxt会触发一些hooks。在每一个节点可以去做一些事情。nuxt能设置hooks是因为nuxt继承Hookable。

随后调用this.ready()方法,就是调用了私有init方法

js 复制代码
async _init () {
   await this.moduleContainer.ready()
   await this.server.ready()
}

主要是调用两个实例的ready方法。

moduleContainer实例ready方法

js 复制代码
 async ready () {
    // Call before hook
    await this.nuxt.callHook('modules:before', this, this.options.modules)

    if (this.options.buildModules && !this.options._start) {
      // Load every devModule in sequence
      await sequence(this.options.buildModules, this.addModule)
    }

    // Load every module in sequence
    await sequence(this.options.modules, this.addModule)

    // Load ah-hoc modules last
    await sequence(this.options._modules, this.addModule)

    // Call done hook
    await this.nuxt.callHook('modules:done', this)
  }

总结就是加载 buildModules modules 模块并且执行。

js 复制代码
buildModules: [
  '@nuxtjs/eslint-module'
],
 modules: [
    '@nuxtjs/axios'
 ],

server实例的ready方法

js 复制代码
async ready () {
  	this.serverContext = new ServerContext(this)
    this.renderer = new VueRenderer(this.serverContext)
    await this.renderer.ready()
  	await this.setupMiddleware()
}

ServerContext类很简单,就是设置server 上下文resources/options/nuxt/globals这些信息

js 复制代码
export default class ServerContext {
  constructor (server) {
    this.nuxt = server.nuxt
    this.globals = server.globals
    this.options = server.options
    this.resources = server.resources
  }
}

VueRenderer ready方法做了那些事情呢?

js 复制代码
async _ready () {
  await this.loadResources(fs)
  this.createRenderer()
}
get resourceMap () {
    const publicPath = urlJoin(this.options.app.cdnURL, this.options.app.assetsPath)
    return {
      clientManifest: {
        fileName: 'client.manifest.json',
        transform: src => Object.assign(JSON.parse(src), { publicPath })
      },
      modernManifest: {
        fileName: 'modern.manifest.json',
        transform: src => Object.assign(JSON.parse(src), { publicPath })
      },
      serverManifest: {
        fileName: 'server.manifest.json',
        // BundleRenderer needs resolved contents
        transform: async (src, { readResource }) => {
          const serverManifest = JSON.parse(src)

          const readResources = async (obj) => {
            const _obj = {}
            await Promise.all(Object.keys(obj).map(async (key) => {
              _obj[key] = await readResource(obj[key])
            }))
            return _obj
          }

          const [files, maps] = await Promise.all([
            readResources(serverManifest.files),
            readResources(serverManifest.maps)
          ])

          // Try to parse sourcemaps
          for (const map in maps) {
            if (maps[map] && maps[map].version) {
              continue
            }
            try {
              maps[map] = JSON.parse(maps[map])
            } catch (e) {
              maps[map] = { version: 3, sources: [], mappings: '' }
            }
          }

          return {
            ...serverManifest,
            files,
            maps
          }
        }
      },
      ssrTemplate: {
        fileName: 'index.ssr.html',
        transform: src => this.parseTemplate(src)
      },
      spaTemplate: {
        fileName: 'index.spa.html',
        transform: src => this.parseTemplate(src)
      }
    }
  }

this.renderer.ready() 加载resourceMap下的文件资源:clientManifest:client.manifest.json / modernManifest: modern.manifest.json / serverManifest: server.manifest.json / ssrTemplate: index.ssr.html / spaTemplate: index.spa.html

然后调用 createRenderer后,

js 复制代码
	 renderer.renderer = {
     ssr: new SSRRenderer(this.serverContext),
     modern: new ModernRenderer(this.serverContext),
     spa: new SPARenderer(this.serverContext)
   }

其中,在render实例方法上有一个renderRoute方法还没有被调用。我们猜测估计是用在中间件上调用了(后面查看注册中间件也和我猜测一样)。

其调用流程renderRoute --> renderSSR(ssr.js 实例) --> renderer.renderer.render(renderContext) ssr.js 实例上的render

重点!!!!:ssr实例的render做了什么?

找到packages/vue-renderer/src/renderers/srr.js 发现

js 复制代码
import { createBundleRenderer } from 'vue-server-renderer'
async render (renderContext) {
  let APP = await this.vueRenderer.renderToString(renderContext)
  return {
      html,
      cspScriptSrcHashes,
      preloadFiles,
      error: renderContext.nuxt.error,
      redirected: renderContext.redirected
    }
}
 createRenderer () {
    // Create bundle renderer for SSR
    return createBundleRenderer(
      this.serverContext.resources.serverManifest,
      this.rendererOptions
    )
  }

createRenderer 返回值就是this.vueRenderer。

在实例化SSRRenderer的时候调用vue官方库: vue-server-renderer 的createBundleRenderer 方法生成了vueRenderer

然后调用renderToString 生成了html

然后对html做一些了HEAD 处理

所以renderRoute其实是调用 SSRRenderer(其中ssr)实例的render方法

最后看一下setupMiddleware

注册setupMiddleware

js 复制代码
// nuxt.config.js 中的中间件
for (const m of this.options.serverMiddleware) {
      this.useMiddleware(m)
 }
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
  options: this.options,
  nuxt: this.nuxt,
  renderRoute: this.renderRoute.bind(this),
  resources: this.resources
}))

....
 renderRoute () {
   return this.renderer.renderRoute.apply(this.renderer, arguments)
 }


...
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
  const result = await renderRoute(url, context)
  const {
      html,
      cspScriptSrcHashes,
      error,
      redirected,
      preloadFiles
    } = result
  	...
    return html
}

进行nuxt中间件注册:

注册了serverMiddleware中的中间件 注册了公共页的中间件page中间件

注册了nuxtMiddleware中间件 注册了错误errorMiddleware中间件

其中nuxtMiddleware中间件就是 执行了 renderRoute

最后附上一张流程图:

一句话总结:new Next(config.js) 准备好了一些资源和中间件。app.use(nuxt.render)其实就是把connect当成一个中间件,当请求路过,经过nuxt注册好的中间件,去获取资源,并且renderToString返回页面需要的html。

参考: juejin.cn/post/694166... juejin.cn/post/691724...

相关推荐
卸任6 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance7 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M7 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写7 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李7 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding8 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员