小狐狸学Vite(五、分析第三方依赖)

1,422 阅读3分钟

“我正在参加「掘金·启航计划」”

文章导航

一、核心知识

二、实现命令行+三、实现http服务器

四、实现静态文件中间件

五、分析第三方依赖

使用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 源码?

点赞 👍

通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~