nuxt3拆包剖析——一个有意思的依赖读取功能
前言
在阅读nuxt源码的时候,会有一些很有意思的点,比如说,nuxt抽取了一个工具包:@nuxt/kit,工具包的作用是抽取了一些nuxt的构建方法和常用的composable,nuxt中对这个包的调用是动态import,这里就做了一些有意思的操作,我们下面来讲解下
通过阅读本文,你能学到:
- import.meta
- unjs/mlly
- 阅读部分nuxt源码
import.meta
ES2020为import方法添加了一个属性import.meta,import.meta 是一个在模块作用域中由宿主填充的对象,可以获取关于该模块的元数据,其中,node环境和web环境填充的内容还有一些区别,我们通过一个例子来讲解下使用方法
我们创建一个工程,然后在工程中创建src/index.ts,里面写上
console.log(import.meta.url)
在node环境时结果是

我们创建一个index.html,引入我们的模块文件
<script type="module" src="dist/index.mjs"></script>
然后用Live Server插件运行一下

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 { }
}
其实就是使用了mlly的resolvePath方法,从上面一节可以知道,resolvePath方法,就是返回模块的绝对路径,我们可以写个demo来验证下
直接通过官网的这个步骤创建一个空白的nuxt项目,然后在根目录下创建一个index.mjs

运行一下,即可看到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中的其中一个方法
})()
运行结果如下

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

可以看到,工程的依赖不多,只安装了一个nuxt,node_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官方文档中的模块化文章,个人认为写的不错
转载自:https://juejin.cn/post/7238423148555960381