Nuxt源码(一)之从nuxt dev到生成.nuxt文件

1,986 阅读5分钟

一、前言

最近忙着去搭建一个商城项目,已经有一个多月没写文章。然后因为我们商城是用nuxt去写的,刚好对nuxt这种约定式的写法很感兴趣,就去看了看nuxt源码,也方便日后有bug好排查

我目前计划是先写两篇文章,第一篇就是如标题,第二篇就是服务端渲染的内容。

本文主要就是说说从nuxt dev启动项目到最后生成.nuxt文件过程中都做了哪些事情,如何进行转换的

二、目录结构和入口

我们先到github下载完nuxt源码后,可以看到目录结构如下

image.png

我们就从脚手架开始分析起,因为我们启动项目都是运行nuxt-cli定义的nuxt dev。

所以我们看看cli结构

├── command.js  
├── commands   // 命令相关
│   ├── build.js
│   ├── dev.js
│   ├── export.js
│   ├── generate.js
│   ├── help.js
│   ├── index.js
│   ├── serve.js
│   ├── start.js
│   └── webpack.js
├── imports.js  //导入相关。就是导入上面的builder、webpack等文件
├── index.js
├── list.js
├── options
│   ├── common.js
│   ├── index.js
│   ├── locking.js
│   └── server.js
├── run.js //运行命令相关
├── setup.js
└── utils // 工具
    ├── banner.js
    ├── config.js
    ├── constants.js
    ├── dependencies.js
    ├── dir.js
    ├── formatting.js
    ├── generate.js
    ├── index.js
    ├── memory.js
    ├── serve.js
    └── webpack.js

于是我们到src/run.js文件中,里面写着关于我们运行nuxt dev等命令的入口

export default async function run (_argv, hooks = {}) {
  const dupPkg = pkgName === '@nuxt/cli-edge' ? 'cli' : 'cli-edge'
  const dupPkgJSON = resolve(__dirname, '../..' /* dist/../.. */, dupPkg, 'package.json')
  // Read from process.argv
  const argv = _argv ? Array.from(_argv) : process.argv.slice(2)
  // Check for internal command
  let cmd = await getCommand(argv[0])
  // Matching `nuxt` or `nuxt [dir]` or `nuxt -*` for `nuxt dev` shortcut
  if (!cmd && (!argv[0] || argv[0][0] === '-' || isNuxtDir(argv[0]))) {
    argv.unshift('dev')
    cmd = await getCommand('dev')
  }
  // Check for dev
  const dev = argv[0] === 'dev'
  // Try internal command
  if (cmd) {
    return NuxtCommand.run(cmd, argv.slice(1), hooks)
  }
}

分析:

  • cli-edge 就是beta版本,我们这里大部分都是默认cli 版本
  • argv[0] 就是对应我们package.json中执行的命令 nuxt dev、nuxt build、nuxt start、nuxt generate 中的dev、build、start、generate 等等
  • getCommand 就可以根据指定的命令加载对应的文件,并且执行NuxtCommand.run方法

关于getCommand 就在src/command/index文件中

const commands = {
  start: () => import('./start'),
  serve: () => import('./serve'),
  dev: () => import('./dev'),
  build: () => import('./build'),
  generate: () => import('./generate'),
  export: () => import('./export'),
  webpack: () => import('./webpack'),
  help: () => import('./help')
}
export default function getCommand (name) {
  if (!commands[name]) {
    return Promise.resolve(null)
  }
  return commands[name]().then(m => m.default)
}

于是拿到了对应的命令的内容,就可以执行NuxtCommand.run进行编译了。

接下来我们来看看NuxtCommand类中都有什么内容

三、NuxtCommand

import Hookable from 'hable'
export default class NuxtCommand extends Hookable {
  constructor (cmd = { name: '', usage: '', description: '' }, argv = process.argv.slice(2), hooks = {}) {
    super(consola)
    this.addHooks(hooks)
    if (!cmd.options) {
      cmd.options = {}
    }
    this.cmd = cmd
    this._argv = Array.from(argv)
  }
  async run () {
  //...
    try {
      await this.cmd.run(this)
    } catch (e) {
      cmdError = e
    }
  }
}

分析:

  • 能看到NuxtCommand继承了hable,hable主要提供了一些钩子函数。addHooks就是其方法,添加钩子
  • 而NuxtCommand的关键方法run 其实就是执行通过getCommand引入的文件中的run方法
  • 所以可以把NuxtCommand看成一个公共的管理钩子函数的中心,run方法其实就调用各个命令中自己的run方法

四、dev的详细内容

因为我们分析的是nuxt dev的情况,所以我们就到对应的文件中去 cli/src/commands/dev.js

先看看大概的内容

export default {
  name: 'dev',
  description:
    'Start the application in development mode (e.g. hot-code reloading, error reporting)',
  usage: 'dev <dir>',
  options: {
    ...common,
    ...server,
    open: {
      alias: 'o',
      type: 'boolean',
      description: 'Opens the server listeners url in the default browser'
    }
  },

  async run (cmd) {
  },

  async startDev (cmd, argv) {
  },

  async _listenDev (cmd, argv) {
  },

  async _buildDev (cmd, argv, nuxt) {
  },

  logChanged ({ event, path }) {
  },

  async onWatchRestart ({ event, path }, { nuxt, cmd, argv }) {
  },

  onBundlerChange (path) {
  }
}

分析:

  • 可以看到dev文件向外暴露了一些属性和方法,这些方法在后续执行的过程都会被调用到,到时候再说吗,就先不展开了

  • 我们重点看看dev中的options都有哪些自带的默认属性:

    • 找到common的文件位于 cli/src/options/common.js中可以看到

      export default {
        spa: {
          alias: 's',
          type: 'boolean',
          description: 'Launch in SPA mode'
        },
        universal: {
          alias: 'u',
          type: 'boolean',
          description: 'Launch in Universal mode (default)'
        },
        'config-file': {
          alias: 'c',
          type: 'string',
          default: defaultNuxtConfigFile, 
          description: `Path to Nuxt config file (default: \`${defaultNuxtConfigFile}\`)`
        },
        modern: {
          alias: 'm',
          type: 'string',
          description: 'Build/Start app for modern browsers, e.g. server, client and false',
          prepare (cmd, options, argv) {
            if (argv.modern !== undefined) {
              options.modern = normalizeArg(argv.modern)
            }
          }
        },
        target: {
          alias: 't',
          type: 'string',
          description: 'Build/start app for a different target, e.g. server, serverless and static',
          prepare (cmd, options, argv) {
            if (argv.target) {
              options.target = argv.target
            }
          }
        },
        'force-exit': {
          type: 'boolean',
          default (cmd) {
            return ['build', 'generate', 'export'].includes(cmd.name)
          },
          description: 'Whether Nuxt should force exit after the command has finished'
        },
        version: {
          alias: 'v',
          type: 'boolean',
          description: 'Display the Nuxt version'
        },
        help: {
          alias: 'h',
          type: 'boolean',
          description: 'Display this message'
        },
        processenv: {
          type: 'boolean',
          default: true,
          description: 'Disable reading from `process.env` and updating it with dotenv'
        },
        dotenv: {
          type: 'string',
          default: '.env',
          description: 'Specify path to dotenv file (default: `.env`). Use `false` to disable'
        }
      }
      
      

      可以看到很多官网熟悉的内容:

      • spa 是否开始spa

      • config-file 的默认值就是nuxt.config,也就是我们文件中的nuxt.config.js

      • modern ,默认值是false

         "scripts": {
            "build:modern": "nuxt build --modern=server",
            "start:modern": "nuxt start --modern=server"
          }
        
      • target, 默认是server用于服务器端渲染,还有static用于静态站点

      • force-exit 就是运行命令后需要退出程序的,有'build', 'generate', 'export’

      • processenv 就是是否能通过 process.env 去读取环境变量,这也是项目中经常用到的

      • dotenv 就是我们定义的环境变量文件,这里默认指向.env文件

4.1 执行run方法

在看到所有初始化的值后,我们就可以开始分析dev中run都执行了什么?

  async run (cmd) {
    const { argv } = cmd
    await this.startDev(cmd, argv, argv.open)
  },
   async startDev (cmd, argv) {
    let nuxt
    try {
      nuxt = await this._listenDev(cmd, argv)
    } catch (error) {
      consola.fatal(error)
      return
    }
    try {
      await this._buildDev(cmd, argv, nuxt)
    } catch (error) {
    }
    return nuxt
  },

整个run结构很清晰,做了两件事_listenDev 开始去获取Nuxt类然后交给_buildDev去打包编译。

五、_listenDev 生成Nuxt类实例

  async _listenDev (cmd, argv) {
    const config = await cmd.getNuxtConfig({ dev: true, _build: true })
    const nuxt = await cmd.getNuxt(config)
    //...
    // Wait for nuxt to be ready
    await nuxt.ready()
    // Start listening
    await nuxt.server.listen()
    // Show banner when listening
    showBanner(nuxt, false)
    // Opens the server listeners url in the default browser (only once)
    if (argv.open) {
      argv.open = false
      const openerPromises = nuxt.server.listeners.map(listener => opener(listener.url))
      await Promise.all(openerPromises)
    }
    // Return instance
    return nuxt
  },

我们分别从几个方法去研究它

5.1 getNuxtConfig 获取配置文件

  async getNuxtConfig (extraOptions = {}) {
    // Flag to indicate nuxt is running with CLI (not programmatic)
    extraOptions._cli = true
    const context = {
      command: this.cmd.name,
      dev: !!extraOptions.dev
    }
    const config = await loadNuxtConfig(this.argv, context)
    const options = Object.assign(config, extraOptions)

    for (const name of Object.keys(this.cmd.options)) {
      this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, this.argv)
    }
    await this.callHook('config', options)
    return options
  }

能看到主要是通过loadNuxtConfig去拿到options值,然后如果属性中有prepare预加载就执行对应的函数

(1)loadNuxtConfig

export async function loadNuxtConfig (argv, configContext) {
  const rootDir = path.resolve(argv._[0] || '.')
  const configFile = argv['config-file'] //前面提到过这里config-file默认值就是nuxt.config
  // Load config
  const options = await _loadNuxtConfig({
    rootDir,
    configFile,
    configContext,
    envConfig: {
      dotenv: argv.dotenv === 'false' ? false : argv.dotenv,
      env: argv.processenv ? process.env : {}
    }
  })
  if (argv.spa === true) {
    options.ssr = false
  } else if (argv.universal === true) {
    options.ssr = true
  }
  // Server options
  options.server = defu({
    port: argv.port || null,
    host: argv.hostname || null,
    socket: argv['unix-socket'] || null
  }, options.server || {}, getDefaultNuxtConfig().server)
  return options
}


分析:

  • 这里在通过_loadNuxtConfig去拿到nuxt.config.js里的文件内容后,看用户有没有开启spa,如果有就关闭ssr(也就是我们一开始创建项目的时候会让你选择是否启用ssr)
  • 然后server就是服务端的一些初始化的值,比如端口号等就不展开了

_loadNuxtConfig

  
export async function _loadNuxtConfig ({
  rootDir = '.',
  envConfig = {},
  configFile = defaultNuxtConfigFile,
  configContext = {},
  configOverrides = {}
} = {}) {
  // ..
  rootDir = path.resolve(rootDir)
  const _require = createRequire(rootDir, true)
  let options = {}
  try {
    configFile = _require.resolve(path.resolve(rootDir, configFile))
  } catch (e) {
  }
  if (configFile) {
    // Clear cache
    clearRequireCache(configFile)
    options = _require(configFile) || {}
    if (options.default) {
      options = options.default
    }
    // Don't mutate options export
    options = { ...options }
    //...
  }
  // Load env to options._env
  return options
}

分析:

  • 这里我删减了一些默认配置,比如env的默认配置,_nuxtConfigFile等等。这里只关注nuxt.config.js
  • 因为我们知道require是有缓存(第一次加载后会把模块中的loaded属性设置为true,再访问的时候就不会再下载了),所以通过clearRequireCache去清空缓存,这样每次都能拿到最新的nuxt.config.js里的值

看看最终options返回的结果吧:

image.png 都是一些默认值和我们nuxt.config.js的内容

5.2 Nuxt

  async getNuxt (options) {
    const { Nuxt } = await core();
    const nuxt = new Nuxt(options);
    await nuxt.ready();
    return nuxt
  }

所以关键在于Nuxt类都有什么

export default class Nuxt extends Hookable {
  constructor (options = {}) {
    super(consola)
    this.options = getNuxtConfig(options)
    this.moduleContainer = new ModuleContainer(this)
    // Call ready
    if (this.options._ready !== false) {
      this.ready().catch((err) => {
        consola.fatal(err)
      })
    }
  }
  static get version () {
  }
  ready () {
  }
  async _init () {
  }
  _initServer () {
    // ..
     utils.defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen']);
  }
}

注意一下:

  • _initServer方法中有'renderRoute', 'renderAndGetWindow', ‘listen’ 三个方法,这是服务端渲染的关键方法(也是我们下一篇文章讲解的关键点),最终也会挂载在Nuxt上

可以看到new Nuxt会去执行 ready方法

 ready () {
    if (!this._ready) {
      this._ready = this._init()
    }
    return this._ready
  }
  async _init () {
    if (this._initCalled) {
      return this
    }
    this._initCalled = true
    // Await for modules
    await this.moduleContainer.ready()
    // Await for server to be ready
    if (this.server) {
      await this.server.ready()
    }
    return this
  }

分析:

  • 其中的server.ready()是涉及到服务端渲染的内容,就不在这里展开。可以看下一篇文章
  • moduleContainer.ready()主要就是去调用了ModuleContainer中的ready方法,去解析各种模块

(1)ModuleContainer 添加各种模块

export default class ModuleContainer {
  constructor (nuxt) {
    this.nuxt = nuxt
    this.options = nuxt.options
    this.requiredModules = {}
  }
  async ready () {
    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)
  }
  addVendor () {
  }

  addTemplate (template) {
  }

  addPlugin (template) {
  }

  addLayout (template, name) {
  }

  addErrorLayout (dst) {
  }

  addServerMiddleware (middleware) {
  }

  extendBuild (fn) {
  }

  extendRoutes (fn) {
  }

  requireModule (moduleOpts, { paths } = {}) {
  }
  async addModule (moduleOpts, arg2, arg3) {
  }
}

分析:

  • sequence 就是通过reduce不断去解析模块里的内容,也就是nuxt.config.js中module模块里的东西

  • 这里我在nuxt.config.js里写入的模块有

      modules: [
        '@nuxtjs/style-resources',
        '@nuxtjs/axios',
        'cookie-universal-nuxt'
      ],
    
  • 所以在这一步就是去解析nuxt.config.js 中的modules内容

  • 而this.options._modules就是默认的一些模块就不展开了

  • 然后可以看到ModuleContainer类就是主要用来添加各种版本Vendor,模版Template,插件plugin,中间件Middleware,布局Layout,错误布局ErrorLayout,服务端的中间件ServerMiddleware,然后扩展一些编译的内容和路由

到此整个Nuxt 类就初始化完成,它是解析整个nuxt项目的关键,包含了ModuleContainer 添加各种模块,render相关的各种方法去进行服务端渲染,listen去监听,以及一些hook

image.png

5.3 showBanner

showBanner(nuxt, false)

就是我们启动项目出现的窗口

image.png

如果我们把值设置为true,就能看到内存 image.png

最后把nuxt实例返回出去,到此整个初始化的过程就完成

六、_buildDev 生成.nuxt文件

  async _buildDev (cmd, argv, nuxt) {
    // Create builder instance
    const builder = await cmd.getBuilder(nuxt)
    // Start Build
    await builder.build()
    // Print memory usage
    showMemoryUsage()
    // Display server urls after the build
    for (const listener of nuxt.server.listeners) {
      consola.info(chalk.bold('Listening on: ') + listener.url)
    }
    // Return instance
    return nuxt
  },

分析:

  • 看起来也很清晰就是先获取到builder
  • 然后通过builder.build()编译

6.1 BundleBuilder

  async getBuilder (nuxt) {
    const { Builder } = await builder();
    const { BundleBuilder } = await webpack();
    return new Builder(nuxt, BundleBuilder)
  }

我们先看看BundleBuilder这其实也就是WebpackBundler,里面主要是关于webpack编译相关的内容

export class WebpackBundler {
  constructor (buildContext) {
    this.buildContext = buildContext
    // Class fields
    this.compilers = []
    this.compilersWatching = []
    this.devMiddleware = {}
    this.hotMiddleware = {}
    // Bind middleware to self
    this.middleware = this.middleware.bind(this)
    // Initialize shared MFS for dev
    if (this.buildContext.options.dev) {
      this.mfs = new AsyncMFS()
    }
  }
  getWebpackConfig (name) {
  }
  
  async build () {
  }

  async webpackCompile (compiler) {
  }

  async webpackDev (compiler) {
  }

  async middleware (req, res, next) {
  }

  async unwatch () {
  }

  async close () {
  }

  forGenerate () {
  }
}

分析:

  • 主要就是build和webpackCompile 进行编译

  • AsyncMFS类就是操作文件的方法,看看下图

image.png

6.2 Builder

export default class Builder {
  constructor (nuxt, bundleBuilder) {
    this.nuxt = nuxt
    this.plugins = []
    this.options = nuxt.options
    this.supportedExtensions = ['vue', 'js', ...(this.options.build.additionalExtensions || [])] // 支持的扩展 'vue', 'js', 'ts', 'tsx’
    this._buildStatus = STATUS.INITIAL
    // Resolve template
    this.template = this.options.build.template || VueAppTemplate
    if (typeof this.template === 'string') {
      this.template = this.nuxt.resolver.requireModule(this.template).template
    }
    // Create a new bundle builder
    this.bundleBuilder = this.getBundleBuilder(bundleBuilder) //就是BundleBuilder类
  }

  getBundleBuilder (BundleBuilder) {
  }

  forGenerate () {
  }

  async build () {
  }

  // Check if pages dir exists and warn if not
  async validatePages () {
  }

  globPathWithExtensions (path) {
  }

  createTemplateContext () {
  }

  async generateRoutesAndFiles () {
  }

  async normalizePlugins () {
  }

  async resolveFiles (dir, cwd = this.options.srcDir) {
  }

  async resolveRelative (dir) {
  }

  async resolveLayouts ({ templateVars, templateFiles }) {
  }

  async resolveRoutes ({ templateVars }) {
  }

  async resolveStore ({ templateVars, templateFiles }) {
  }

  async resolveMiddleware ({ templateVars, templateFiles }) {
  }

  async resolveCustomTemplates (templateContext) {
  }

  async resolveLoadingIndicator ({ templateFiles }) {
  }

  async compileTemplates (templateContext) {
  }

  resolvePlugins () {
  }
  
  createFileWatcher (patterns, events, listener, watcherCreatedCallback) {
  }

  assignWatcher (key) {
  }

  watchClient () {
  }

  serverMiddlewareHMR () {
  }

  watchRestart () {
  }

  unwatch () {
  }

  async close () {
  }
}

我们来看看new Builder 都发生了啥:

  • template 的默认值是VueAppTemplate,主要就是vue相关的文件版本好, 看看如下的默认值

image.png

  • bundleBuilder 就是去new BundleBuilder,上面已经分析过了,主要是提供了一些操作文件的方法

6.3 builder.build()

​ 因为拿到Builder实例后,我们需要进行编译,所以来看看Builder类中的build都做了什么

  async build () {
    // 状态 状态完成就直接返回了
    if (this._buildStatus === STATUS.BUILD_DONE && this.options.dev) {
      return this
    }
    // 正在编译就等待1s再继续重新编译
    if (this._buildStatus === STATUS.BUILDING) {
      await waitFor(1000)
      return this.build()
    }
    // 更改状态
    this._buildStatus = STATUS.BUILDING

    // Wait for nuxt ready
    await this.nuxt.ready()

    // Create or empty .nuxt/, .nuxt/components and .nuxt/dist folders
    await fsExtra.emptyDir(r(this.options.buildDir))
    const buildDirs = [r(this.options.buildDir, 'components')]

    await Promise.all(buildDirs.map(dir => fsExtra.emptyDir(dir)))

    // Generate routes and interpret the template files
    await this.generateRoutesAndFiles()

    // Add vue-app template dir to watchers
   this.options.build.watch.push(this.globPathWithExtensions(this.template.dir))

    await this.resolvePlugins()

    // Start bundle build: webpack, rollup, parcel...
    await this.bundleBuilder.build()

    // Flag to set that building is done
    this._buildStatus = STATUS.BUILD_DONE

    return this
  }

分析:

  • 这里会通过STATUS去管理状态,这一块写在注释上了
  • 然后会调用之前初始化的时候Nuxt类中的ready方法,这里其实就是调用ModuleContainer中的ready方法,去解决各个模块的内容(就是nuxt.config.js中的modules里的内容),其中的server.ready()是涉及到服务端渲染的内容,就不在这里展开。可以看下一篇文章
  • buildDir 值为.nuxt 也就是我们的目标文件,fsExtra.emptyDir就是为了看是否存在.nuxt文件,没有就创建,有就清空
  • components这里也是同理,也创建一个components文件
  • generateRoutesAndFiles和resolvePlugins,bundleBuilder.build() 内容比较多就单独抽离出来讲解

(1)generateRoutesAndFiles

  async generateRoutesAndFiles () {
    this.plugins = Array.from(await this.normalizePlugins());
    const templateContext = this.createTemplateContext();

    await Promise.all([
      this.resolveLayouts(templateContext),
      this.resolveRoutes(templateContext),
      this.resolveStore(templateContext),
      this.resolveMiddleware(templateContext)
    ]);

    this.addOptionalTemplates(templateContext);

    await this.resolveCustomTemplates(templateContext);

    await this.resolveLoadingIndicator(templateContext);

    await this.compileTemplates(templateContext);

    consola__default['default'].success('Nuxt files generated');
  }


分析:

  • plugins,拿到在nuxt.config.js定义的plugin值和一些初始化值

    先看看我的nuxt.config.js中的plugin

      plugins: [
        '@/plugins/antd-ui',
        '@/plugins/lodash.ts',
        '@/plugins/mixin.ts',
        '@/assets/api/index.ts',
        {
          src: '@/plugins/axios.ts',
          ssr: true,
        },
      ],
    

    这里会做了一些处理,mode中的all就是代表是客户端和服务端都能使用的,client就是客户端使用。

    0:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…component-injection/.nuxt/components/plugin.js', mode: 'all', name: 'nuxt_plugin_plugin_78da769d'}
    1:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…onent-injection/.nuxt/cookie-universal-nuxt.js', mode: 'all', name: 'nuxt_plugin_cookieuniversalnuxt_2b5a824a'}
    2:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/examples/async-component-injection/.nuxt/axios.js', mode: 'all', name: 'nuxt_plugin_axios_b428359a'}
    3:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/examples/async-component-injection/plugins/antd-ui', mode: 'all', name: 'nuxt_plugin_antdui_2b2dca90'}
    4:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…es/async-component-injection/plugins/lodash.ts', mode: 'all', name: 'nuxt_plugin_lodash_e3ee5ff8'}
    5:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…les/async-component-injection/plugins/mixin.ts', mode: 'all', name: 'nuxt_plugin_mixin_17d0d500'}
    6:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…/async-component-injection/assets/api/index.ts', mode: 'all', name: 'nuxt_plugin_index_b8984412'}
    7:{src: '/Users/tt/Code/CodeSource/nuxt/nuxt.js-dev/exa…les/async-component-injection/plugins/axios.ts', mode: 'all', name: 'nuxt_plugin_axios_397e54eb'}
    
  • templateContext 就是能拿到模版。这也就是我们生成的.nuxt文件里各个文件。nuxt就是通过把数据都往模版里套,就生成了对应的.nuxt文件夹内容。

image.png

  • templateFiles 就是对应的模版

  • templateVars 可以理解成options

image.png

  • 接下来就是比较重点进行编译的阶段我们重点看看
resolveRoutes
  async resolveRoutes ({ templateVars }) {
  	// ..
    if (this._defaultPage) {
    } else if (this._nuxtPages) {
      //..
      for (const page of await this.resolveFiles(this.options.dir.pages)) {
        const key = page.replace(ext, '')
        // .vue file takes precedence over other extensions
        if (/\.vue$/.test(page) || !files[key]) {
          files[key] = page.replace(/(['"])/g, '\\$1')
        }
      }
      templateVars.router.routes = createRoutes({
        files: Object.values(files),
        srcDir: this.options.srcDir,
        pagesDir: this.options.dir.pages,
        routeNameSplitter,
        supportedExtensions: this.supportedExtensions,
        trailingSlash
      })
    } else { // If user defined a custom method to create routes
    }
    // Make routes accessible for other modules and webpack configs
    this.routes = templateVars.router.routes
  }

分析:

  • 这里files 就是去看我们pages里的文件,然后遍历出来,如下:

    pages/_slug:'pages/_slug.vue'
    pages/index:'pages/index.vue'
    

    我pages下就两个文件

  • 然后我们看看从createRoutes后返回的数据结构

image.png

  • 能看到从createRoutes将文件转换为路由
resolveStore、resolveMiddleware、resolveLayouts、resolveCustomTemplates 和resolveLoadingIndicator

这些都同理,都是根据文件目录转换为我们平常使用的数据结构

compileTemplates
 async compileTemplates (templateContext) {
    // Prepare template options
    const { templateVars, templateFiles, templateOptions } = templateContext;
    templateOptions.imports = {
      ...templateOptions.imports,
      resolvePath: this.nuxt.resolver.resolvePath,
      resolveAlias: this.nuxt.resolver.resolveAlias,
      relativeToBuild: this.relativeToBuild
    };

    // Interpret and move template files to .nuxt/
    await Promise.all(
      templateFiles.map(async (templateFile) => {
        const { src, dst, custom } = templateFile;
        // Add custom templates to watcher
        if (custom) {
          this.options.build.watch.push(src);
        }
        // Render template to dst
        const fileContent = await fsExtra__default['default'].readFile(src, 'utf8');
        let content;
        try {
          const templateFunction = lodash.template(fileContent, templateOptions);
          content = utils.stripWhitespace(
            templateFunction({
              ...templateVars,
              ...templateFile
            })
          );
        } catch (err) {
        }
        // Ensure parent dir exits and write file
        const relativePath = utils.r(this.options.buildDir, dst);
        await fsExtra__default['default'].outputFile(relativePath, content, 'utf8');
      })
    );
  }

分析:

  • 前面有说过templateFiles都是一些之前我们在初始化的时候导入的模版

  • 这里通过Promise.all去readFile 读取fileContent内容

  • 最后通过outputFile把文件输出

  • 看看最后创建的内容

image.png

(2)bundleBuilder.build()

​ 最后看看webpack都做了哪些事

  async build () {
    const { options } = this.buildContext;
    const webpackConfigs = [
      this.getWebpackConfig('Client')
    ];
    if (options.modern) {
      webpackConfigs.push(this.getWebpackConfig('Modern'));
    }
    if (options.build.ssr) {
      webpackConfigs.push(this.getWebpackConfig('Server'));
    }
    // Configure compilers
    this.compilers = webpackConfigs.map((config) => {
      const compiler = webpack__default['default'](config);
      // In dev, write files in memory FS
      if (options.dev) {
        compiler.outputFileSystem = this.mfs;
      }
      return compiler
    });

    // Start Builds
    const runner = options.dev ? utils.parallel : utils.sequence;

    await runner(this.compilers, compiler => this.webpackCompile(compiler));
  }

这里就大概看看就不展开了,就是ssr里需要打包前后端两个bundle,后端渲染,前端挂载

七、总结

image.png

Nuxt 通过我们执行的nuxt dev去拿到dev标识,然后通过NuxtCommand去做统一去调度,执行run方法从而生成Nuxt类实例并且读取了我们在nuxt.config.js中定义的内容,最后再通过_buildDev去解析我们的项目的目录结构(因为我们的目录是按照nuxt约定的规则创建),进而去转为我们通用的文件,并且写入模版中。最后再编译到.nuxt文件中