likes
comments
collection
share

nuxt3拆包剖析——一个有意思的依赖读取功能

作者站长头像
站长
· 阅读数 14

前言

在阅读nuxt源码的时候,会有一些很有意思的点,比如说,nuxt抽取了一个工具包:@nuxt/kit,工具包的作用是抽取了一些nuxt的构建方法和常用的composablenuxt中对这个包的调用是动态import,这里就做了一些有意思的操作,我们下面来讲解下

通过阅读本文,你能学到:

  • import.meta
  • unjs/mlly
  • 阅读部分nuxt源码

import.meta

ES2020为import方法添加了一个属性import.metaimport.meta 是一个在模块作用域中由宿主填充的对象,可以获取关于该模块的元数据,其中,node环境和web环境填充的内容还有一些区别,我们通过一个例子来讲解下使用方法

我们创建一个工程,然后在工程中创建src/index.ts,里面写上

console.log(import.meta.url)

在node环境时结果是

nuxt3拆包剖析——一个有意思的依赖读取功能

我们创建一个index.html,引入我们的模块文件

<script type="module" src="dist/index.mjs"></script>

然后用Live Server插件运行一下

nuxt3拆包剖析——一个有意思的依赖读取功能

node环境的结果是file:///D:/project/nuxt-mlly-demo/src/index.ts,即文件的本地路径

web环境的结果是http://127.0.0.1:5500/dist/index.mjs

import.meta对象还有一个属性import.meta.resolve,能将一个相对路径读取为绝对路径,比如我在src目录下创建brother.ts,然后运行下面代码

const res = await import.meta.resolve('./brother.ts')
console.log(res) // file:///D:/project/nuxt-mlly-demo/src/brother.ts

简单总结一下,import.meta能获取当前模块的元信息,其中我们可以使用import.meta.url来获取当前模块的路径

unjs/mlly

为什么我需要写一节import.meta呢,因为接下来介绍的这个包会需要用到import.meta.url作为入参

这是unjs/mlly文档

While ESM Modules are evolving in Node.js ecosystem, there are still many required features that are still experimental or missing or needed to support ESM. This package tries to fill in the gap.

虽然ESM模块在Node.js生态系统中不断发展,但仍有许多必需的功能仍处于实验阶段,或者缺少或需要支持ESM。这个包试图填补空白。

我们简单使用下这几个api:

  • resolve
  • resolvePath
  • createResolve
  • resolveImports

我这里展示下当前目录结构,全部操作都在index.ts

|--src
|----index.ts
|----brother.ts
const path = await resolve('./brother.ts', { url: import.meta.url })
console.log(path) // file:///D:/project/nuxt-mlly-demo/src/brother.ts

const path2 = await resolvePath('./brother.ts', { url: import.meta.url })
console.log(path2) // D:/project/nuxt-mlly-demo/src/brother.ts

const _resolve = createResolve({ url: import.meta.url })
const path3 = await _resolve('./brother.ts')
console.log(path3) // file:///D:/project/nuxt-mlly-demo/src/brother.ts

const path4 = await resolveImports("import { bro } from './brother.ts'", { url: import.meta.url })
console.log(path4) // import { bro } from 'file:///D:/project/nuxt-mlly-demo/src/brother.ts'

总结一下:

  • resolve:找出文件的绝对路径,是file协议
  • resolvePath:与resolve类似,只不过经过了fileURLToPath处理
import { fileURLToPath } from 'node:url'

const path = await resolve('./brother.ts', { url: import.meta.url })
const path2 = await resolvePath('./brother.ts', { url: import.meta.url })

console.log(path2) // D:/project/nuxt-mlly-demo/src/brother.ts
console.log(fileURLToPath(path)) // D:\project\nuxt-mlly-demo\src\brother.ts,这里应该是操作系统问题,我的是window,所以是\
  • createResolve:创建一个带有默认参数的resolve方法
  • resolveImports:使用相对路径解析所有静态和动态导入到完整解析路径。

阅读部分nuxt源码

在nuxt项目中,运行pnpm dev,基本是在运行nuxt dev/nuxi dev,这里有一个操作就是

export default defineNuxtCommand({
  meta: {
    name: 'dev',
    usage: 'npx nuxi dev [rootDir] [--dotenv] [--log-level] [--clipboard] [--open, -o] [--port, -p] [--host, -h] [--https] [--ssl-cert] [--ssl-key]',
    description: 'Run nuxt development server'
  },
  async invoke (args, options = {}) {
      // ...
      const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir)
      // ...
  }
})

我们不需要深究这份代码的功能是什么,只需要知道在执行nuxt dev的时候,会执行invoke方法

invoke方法中,使用了loadKit方法,来动态加载@nuxt/kit工具包,导入了loadNuxt loadNuxtConfig buildNuxt三个方法

export const loadKit = async (rootDir: string): Promise<typeof import('@nuxt/kit')> => {
  try {
    // 尝试读取工程中的@nuxt/kit
    const localKit = await tryResolveModule('@nuxt/kit', rootDir)
    // 如果工程中包含@nuxt/kit则直接使用,如果没有则从nuxt中读取@nuxt/kit
    const rootURL = localKit ? rootDir : await tryResolveNuxt() || rootDir
    return await importModule('@nuxt/kit', rootURL) as typeof import('@nuxt/kit')
  } catch (e: any) {
    if (e.toString().includes("Cannot find module '@nuxt/kit'")) {
      throw new Error('nuxi requires `@nuxt/kit` to be installed in your project. Try installing `nuxt` v3 or `@nuxt/bridge` first.')
    }
    throw e
  }
}

这么看代码其实也不难,loadKit方法主要是用于动态加载(import)@nuxt/kit工具包

如果用户自行安装了@nuxt/kit则直接使用,如果没有,则从nuxt中获取

async function tryResolveNuxt () {
  for (const pkg of ['nuxt3', 'nuxt', 'nuxt-edge']) {
    const path = await tryResolveModule(pkg)
    if (path) { return path }
  }
  return null
}

这段代码是检测用户使用的是nuxt3 还是nuxt2 还是nuxt-edge

这两段代码都使用了tryResolveModule方法,我们现在来解析一下

import { resolvePath } from 'mlly'

export async function tryResolveModule (id: string, url = import.meta.url) {
  try {
    return await resolvePath(id, { url })
  } catch { }
}

其实就是使用了mllyresolvePath方法,从上面一节可以知道,resolvePath方法,就是返回模块的绝对路径,我们可以写个demo来验证下

直接通过官网的这个步骤创建一个空白的nuxt项目,然后在根目录下创建一个index.mjs

nuxt3拆包剖析——一个有意思的依赖读取功能

运行一下,即可看到nuxt的绝对路径在node_modules中

接下来,我们把nuxt的这段代码,按照功能模拟到我的这个index.mjs

import { resolvePath } from 'mlly'
import { pathToFileURL } from 'node:url'

(async() => {
  async function loadKit() {
    const localKit = await resolvePath('@nuxt/kit', { url: import.meta.url })
    console.log({localKit}) // 尝试在本地寻找@nuxt/kit

    const rootURL = localKit ? import.meta.url : await resolvePath('nuxt', { url: import.meta.url })
    console.log({rootURL})  // 如果本地有,则以本地路径为基准,如果没有,则以nuxt路径为基准

    const finalKitPath = await resolvePath('@nuxt/kit', rootURL)
    console.log({finalKitPath}) // 最终确定的@nuxt/kit的路径
    
    return await import(pathToFileURL(finalKitPath).href) // 动态导入@nuxt/kit
  }

  const { loadNuxt } = await loadKit()
  console.log(loadNuxt) // 这里就是动态导入了@nuxt/kit中的其中一个方法
})()

运行结果如下

nuxt3拆包剖析——一个有意思的依赖读取功能

下图是这个工程的依赖关系

nuxt3拆包剖析——一个有意思的依赖读取功能

可以看到,工程的依赖不多,只安装了一个nuxtnode_modules下也存在@nuxt/kit,所以能直接读取出@nuxt/kit的绝对路径,然后动态导入读取功能方法loadNuxt

我们可以尝试着去探究一下这个函数的读取过程

await resolvePath('avite', { url: import.meta.url })

我们在文件中运行下这行代码,因为没有avite这个依赖,所以会报错

Error: Cannot find module avite imported from 
file:///D:/project/nuxt-empty-project/index.mjs, 
file:///D:/project/nuxt-empty-project/, 
file:///D:/project/nuxt-empty-project/index.mjs/_index.js, 
file:///D:/project/nuxt-empty-project/node_modules

报错信息我格式化了一下,可以看到,resolvePath会从以上四个路径获取

可以看到最后一步,会从node_modules下寻找,并且安装了nuxt就会安装@nuxt/kit,所以才能顺利找到@nuxt/kit的绝对路径,从而能动态加载

总结

阅读本文,你能学习到import.meta的内容,unjs/mlly工具包的使用,以及阅读了部分nuxt脚手架的源码

说实话我觉得这篇文章写的其实不算好,感觉有好多能梳理的点并没有梳理通,可能是我模块化方面的知识并不完善导致的,如果你阅读了本文觉得有不当的点,欢迎大家在评论区踊跃抨击,我会虚心学习~

最后,附上nuxt官方文档中的模块化文章,个人认为写的不错