小狐狸学Vite(五、分析第三方依赖)
文章导航
使用esbuild 扫描项目依赖
1. lib\server\index.js
这里要写的就是在(二、实现命令行+三、实现http服务器)中创建http
服务之前先使用esbuild
分析从index.html
入口文件中的script
文件作为js
入口文件都引入了那些第三方依赖模块生成一个map
对象。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="/src/main.js" type="module"></script>
</body>
</html>
src/main.js
import { ref } from 'vue'
import './msg.js'
console.log('main')
src/msg.js
import lodash from 'lodash'
console.log(lodash)
我们要得到的结果就是生成以下的map
对象
{
vue: 'D:/code/vite/zf-hand-write-vite/use-vite3/node_modules/vue/dist/vue.runtime.esm-bundler.js',
lodash: 'D:/code/vite/zf-hand-write-vite/use-vite3/node_modules/lodash/lodash.js'
}
继续在上一节的基础上进行修改,引入esbuild
const connect = require('connect')
const resolveConfig = require('../config')
const serveStaticMiddleWare = require('./middlewares/static')
+ const { createOptimizeDepsRun } = require('../optimizer')
async function createServer() {
// connect 本事也可以最为 http 的中间件使用
const middlewares = connect()
const config = await resolveConfig()
// 构造一个用来创建服务的对象
const server = {
async listen(port) {
// 在创建服务器之前
+ await runOptimize(config, server)
require('http').createServer(middlewares)
.listen(port, async () => {
console.log(`开发环境启动成功请访问:http://localhost:${port}`)
})
}
}
middlewares.use(serveStaticMiddleWare(config))
return server
}
+ async function runOptimize(config, server) {
+ await createOptimizeDepsRun(config)
+ }
exports.createServer = createServer
2. lib\optimizer\index.js
将真正扫描使用esbuild的代码抽离出去
const scanImports = require('./scan')
// 这里使用 esbuild 扫描项目依赖了那些模块(第三方依赖或者自己写的模块)
async function createOptimizeDepsRun(config) {
// 使用 esbuild 获取依赖信息
const deps = await scanImports(config)
console.log(deps)
}
exports.createOptimizeDepsRun = createOptimizeDepsRun
在这里真正的使用esbuild
3.lib\optimizer\scan.js
const { build } = require('esbuild')
// 编写一个 esbuild 插件来辅助完成依赖扫描的过程
const path = require('path');
const esbuildScanPlugin = require('./esbuildScanPlugin');
async function scanImports(config) {
// 存执依赖信息
const depImports = {};
const esPlugin = await esbuildScanPlugin(config, depImports);
await build({
// 工作目录从配置文件中读取
absWorkingDir: config.root,
entryPoints: [path.resolve('./index.html')],
bundle: true,
format: 'esm',
outfile: 'dist/index.js',
write: false, // 在这里只用来分析依赖信息,不用来生成打包文件
// 使用 自己编写的插件
plugins: [esPlugin]
})
return depImports
}
module.exports = scanImports
编写一个esbuild
插件来专门完成依赖文件的分析, 之前在那块看见过一张图片画了esbuild
插件钩子函数执行的顺序找不见了,有了解的可以评论一下哈,感谢。
4. esbuild插件
lib\optimizer\esbuildScanPlugin.js
// 编写一个 esbuild 插件
// onResolve 找路径
// onLoad 找内容
const fs = require('fs-extra')
const path = require('path')
const resolvePlugin = require('../plugins/resolve')
const { createPluginContainer } = require('../server/pluginContainer')
const { normalizePath } = require('../utils')
// 正则 用来匹配html结尾的文件
const htmlTypesRE = /\.html$/
// 用来匹配 index.html 入口文件中引入的 script 脚本路径
const scriptModuleRE = /<script\s+src\="(.+?)"\s+type="module"><\/script>/
// 匹配js结尾的文件
const JS_TYPE_RE = /\.js$/
//
async function esbuildScanPlugin(config, depImports) {
config.plugins = [resolvePlugin(config)]
// 在执行 esbuild 的时候创建一个 vite 的插件容器
const container = await createPluginContainer(config)
const resolve = async (id, importer) => {
return await container.resolveId(id, importer)
}
return {
name: 'vite-:dep-scan',
setup(build) {
// onResolve 函数的回调函数会在 Esbuild 构建每个模块的导入路径(可匹配的)时执行。
// 也就是当匹配到 .html 结尾的文件会走这个路径
// 找 index.html的真实路径
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
// 把任意路径转成绝对路径
const resolved = await resolve(path, importer)
if (resolved) {
return {
path: resolved.id || resolved,
namespace: 'html'
}
}
})
// 解析其他的路径
build.onResolve({ filter: /.*/ }, async ({ path, importer }) => {
const resolved = await resolve(path, importer);
if (resolved) {
// 如果路径中包含 node_modules 则说明是依赖 npm 包
const id = resolved.id || resolved
const included = id.includes('node_modules')
if (included) {
// 将依赖添加到 map 对象中去
depImports[path] = normalizePath(id)
return {
path: id,
external: true // 标记为外部的,后序将不再解析
}
}
return {
path: id
}
}
// 真实存在的路径不需要添加 namespace
return {
path
}
})
// 处理并返回模块的内容
build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
// 读取html 文件内容
const html = fs.readFileSync(path, 'utf-8')
// 匹配 html 中的 script module 中引入文件的路径
let [, scriptSrc] = html.match(scriptModuleRE)
// scriptSrc = await resolve(scriptSrc)
// console.log(scriptSrc)
// console.log(scriptSrc) ./src/main.js
// import "D:/code/vite/zf-hand-write-vite/use-vite3/src/main.js" 这里需要将相对路径转换成绝对路径 , 所以要在上面解析路径的时候进行处理
// 将 其转换成 js 导入的模式
scriptSrc = config.root + scriptSrc
let js = `import ${JSON.stringify(scriptSrc)}` // 当这里 import 了新的模块之后,会再次走 解析的钩子
return {
loader: 'js',
contents: js
}
})
// 读取 js 文件的内容并返回
build.onLoad({ filter: JS_TYPE_RE }, async ({ path: id }) => {
let ext = path.extname(id).slice(1)
let contents = fs.readFileSync(id, 'utf-8')
return {
loader: ext,
contents
}
})
}
}
}
module.exports = esbuildScanPlugin
创建插件容器,
5. vite插件容器
lib\server\pluginContainer.js
const { normalizePath } = require("../utils")
async function createPluginContainer({ plugins, root }) {
class PluginContext {
async resolve({ id, importer }) {
// 由插件容器进行路径解析,返回绝对路径
return await container.resolveId(id, importer)
}
}
// 创建一个插件容器, 插件容器只是用来管理插件的
const container = {
async resolveId(id, importer) {
let ctx = new PluginContext()
let resolveId = id
// 遍历用户传进来的插件
for (const plugin of plugins) {
// 如果插件中没有 resolveId 方法,则执行下一个插件
if (!resolveId) continue
const result = await plugin.resolveId.call(ctx, id, importer)
if (result) {
resolveId = result.id || result;
break;
}
}
return {
id: normalizePath(resolveId)
}
}
}
return container
}
exports.createPluginContainer = createPluginContainer
6. vite路径解析插件
const path = require('path')
function resolvePlugin(config) {
return {
name: 'vite:resolve',
resolveId(id, importer) {
// 如果是/开头,则表示的绝对路径
if (id.stratsWith('/')) {
return { id: path.resolve(config.root, id.slice(1)) }
}
// 如果是绝对路径
if (path.isAbsolute(id)) {
return { id }
}
//如果是相对路径的话
if (path.startsWith('.')) {
const baseDir = importer ? pathLib.dirname(importer) : root;
const fsPath = pathLib.resolve(baseDir, path);
return { id: fsPath }
}
/* if (path.startsWith('@')) {
const baseDir = alias['@'];
const fsPath = pathLib.resolve(baseDir, path);
return { id: fsPath }
} */
//如果是第三方的话
let res = tryNodeResolve(path, importer, root);
if (res) return res;
}
}
}
function tryNodeResolve(path, importer, root) {
//vue/package.json
const pkgPath = resolve.sync(`${path}/package.json`, { basedir: root });
const pkgDir = pathLib.dirname(pkgPath);
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const entryPoint = pkg.module;//module字段指的是是es module格式的入口
const entryPointPath = pathLib.join(pkgDir, entryPoint);
console.log(entryPoint)
//D:/code/vite/zf-hand-write-vite/use-vite3/node_modules/vue/dist/vue.runtime.esm-bundler.js
//现在返回的vue的es module的入口文件
return { id: entryPointPath }
}
module.exports = resolvePlugin;
后面会写把插件容器挂载到http server
这个对象上面,用来执行用户传入的插件里面的 transform
方法从而实现对源代码进行转换的效果。
关于插件的执行方式可以参考
webpack
或者rollup
中的几种执行方式 强烈推荐看下这篇文章# 【中级/高级前端】为什么我建议你一定要读一读 Tapable 源码?
点赞 👍
通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~
转载自:https://juejin.cn/post/7204809985548107813