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