注册
web

Nuxt源码浅析

来聊聊Nuxt源码。


聊聊启动nuxt项目


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


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到底是什么?


目录结构


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


// 工程核心目录结构
├─ 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的主要代码:



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默认对象


// 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实例

export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}

}
}


  1. this._initServer() 来创建一个connect服务。

  _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'])
}

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方法


async _init () {
await this.moduleContainer.ready()
await this.server.ready()
}

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


moduleContainer实例ready方法


 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 模块并且执行。


buildModules: [
'@nuxtjs/eslint-module'
],
modules: [
'@nuxtjs/axios'
],

server实例的ready方法


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这些信息


export default class ServerContext {
constructor (server) {
this.nuxt = server.nuxt
this.globals = server.globals
this.options = server.options
this.resources = server.resources
}
}

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


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后,


	 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 发现


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


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


最后附上一张流程图:


img


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


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


作者:随风行酱
来源:juejin.cn/post/7306457908636287003

0 个评论

要回复文章请先登录注册