来聊聊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的工作内容很简单:
- 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)
...
- this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例
js
export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}
}
}
- 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。