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...

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui