小狐狸学Vite(七、修改导入路径)

“ 本文正在参加「金石计划」 ”

文章导航

一、核心知识

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

四、实现静态文件中间件

五、分析第三方依赖

六、预编译并保存metadata信息

这小节主要做的就是当引入了第三方包的时候,使用之前预编译在.vite缓存目录下面的文件。具体来说就是,

import { ref } from 'vue'

// 将其转换成下面的路径

import { ref } from '/node_modules/.vite3/deps/vue.js'

下面要写的内容多部分结合着这个图里面的东西,包含插件容器的实现,以及插件在http请求中间件中调用插件容器的方法,从而执行插件容器中所有的方法。还是比较容易懵的。写完就会明白的,大家跟上,同时在后面也会写一部分的调试方法,来帮助理解整个流程。

小狐狸学Vite(七、修改导入路径)

1.创建server入口

创建插件容器,并将插件容器保存到当前服务上面作为属性,同时编写一个用来转换http请求的中间件(在内部执行vite插件的钩子)

lib\server\index.js

const connect = require('connect')
const resolveConfig = require('../config')
const serveStaticMiddleWare = require('./middlewares/static')
const { createOptimizeDepsRun } = require('../optimizer')
+ const transformMiddleware = require('./middlewares/transform')
+ const { createPluginContainer } = require('./pluginContainer')

async function createServer() {
  // connect 本事也可以最为 http 的中间件使用
  const middlewares = connect()
  const config = await resolveConfig()
  + // 创建一个插件容器,
  + const pluginContainer = await createPluginContainer(config)
  // 构造一个用来创建服务的对象
  const server = {
  +   pluginContainer,
    async listen(port) {
      // 在创建服务器之前
      await runOptimize(config, server)
      require('http').createServer(middlewares)
        .listen(port, async () => {
          console.log(`开发环境启动成功请访问:http://localhost:${port}`)
        })
    }
  }
+  for (const plugin of config.plugins) {
+    if (plugin.configureServer) {
+      await plugin.configureServer(server)
+    }
+  }
  // 
+  middlewares.use(transformMiddleware(server))
  middlewares.use(serveStaticMiddleWare(config))
  return server
}

async function runOptimize(config, server) {
  const optimizeDeps = await createOptimizeDepsRun(config)
  // 把生成的优化信息保存在 server 上面 
  server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer

2.请求转换中间件(transform.js)

这里就主要负责将被vite插件处理过的代码返回给客户端。

lib\server\middlewares\transform.js

// 转换请求插件
const { normalizePath, isJSRequest } = require('../../utils')
const send = require('../send')
const transformRequest = require('../transformRequest')
const { parse } = require('url')

// 请求转化中间件
function transformMiddleware(server) {
  return async function (req, res, next) {
    // 如果请求方式不是 get 请求的话, 则直接放行
    if (req.method !== 'GET') {
      return next()
    }
    let url = parse(req.url).pathname;
    // 如果请求的是 js 文件
    if (isJSRequest(url)) {
      debugger
      const result = await transformRequest(url, server)
      if (result) {
        const type = 'js'
        return send(req, res, result.code, type)
      }
    } else {
      next()
    }
  }
}

module.exports = transformMiddleware

3.插件容器

在插件容器中执行plugins中传入的load,transform钩子函数。插件上下文中的方法是为了拿到插件钩子执行的结果。

lib\server\pluginContainer.js

const { normalizePath } = require("../utils")
+ const path = require('path')


async function createPluginContainer({ plugins, root }) {

  class PluginContext {
    // 调用插件容器身上的方法
+    async resolve(id, importer = path.join(root, 'index.html')) {
+      // 由插件容器进行路径解析,返回绝对路径
+      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)
      }
    },
+    async load(id) {
+      const ctx = new PluginContext()
+      for (const plugin of plugins) {
+        // 如果当前插件没有 load 方法,则跳过当前插件
+        if (!plugin.load) continue
+        const result = await plugin.load.call(ctx, id)
+        if (result !== null) {
+          return result
+        }
+      }
+    },
+    // async sequential  
+    // 异步串行钩子
+    async transform(code, id) {
+      for (const plugin of plugins) {
+        if (!plugin.transform) continue
+        const ctx = new PluginContext()
+        const result = await plugin.transform.call(ctx, code, id)
+        if (!result) continue
+        // 将上一次的结果给下一次
+        code = result.code || result
+      }
+      return { code }
+    }
  }
  return container
}

exports.createPluginContainer = createPluginContainer

4.工具函数

lib/utils.js

新增

// 判断是否是 js 或者 .vue 结尾的文件
const knownJsSrcRE = /\.(js|vue)/;

function isJSRequest(url) {
  if (knownJsSrcRE.test(url)) {
    return true
  }
  return false
}

module.exports = {
  normalizePath,
  isJSRequest
}

5.插件容器调用插件钩子

lib/server/transformRequest.js

// 转换请求
const fs = require('fs-extra')

// 转换请求中间件调用的函数
async function transformRequest(url, server) {
  const { pluginContainer } = server
  // 调用插件容器的 解析路径方法
  const { id } = await pluginContainer.resolveId(url)
  // 调用插件容器的 加载路径文件的方法
  const loadResult = await pluginContainer.load(id)
  let code
  if (loadResult) {
    code = loadResult.code
  } else {
    // 如果在 load 函数里面没有返回结果,则直接读取文件
    code = await fs.readFile(id, 'utf-8')
  }
  // 调用插件容器的转换方法
  const transformResult = await pluginContainer.transform(code, id)
  return transformResult
}

module.exports = transformRequest

6.设置正确的响应头

lib/server/send.js

const alias = {
  js: 'application/javascript',
  css: 'text/css',
  html: 'text/html',
  json: 'application/json'
}

// 设置正确的响应头

function send(req, res, content, type) {
  res.setHeader('Content-Type', alias[type] || type);
  res.statusCode = 200
  return res.end(content)
}

module.exports = send

7.整合内部插件

lib/plugins/index.js

// 导入分析插件
const importAnalysisPlugin = require('./importAnalysis')
const preAliasPlugin = require('./preAlias')
const resolvePlugin = require('./resolve')

async function resolvePlugins(config) {
  return [
    preAliasPlugin(config),
    resolvePlugin(config),
    importAnalysisPlugin(config)
  ]
}

exports.resolvePlugins = resolvePlugins

8. 导入分析插件

lib/plugins/importAnalysis.js

通过es-module-laxer插件解析源代码,分析当前文件中import了那些东西,对导入的路径进行重写。

// 导入分析
const { init, parse } = require('es-module-lexer')
const MagicString = require('magic-string')

function importAnalysisPlugin(config) {
  // 拿到根目录
  const { root } = config
  return {
    name: 'vite:import-analysis',
    async transform(source, importer) {
      await init
      let imports = parse(source)[0]
      if (!imports.length) {
        return source
      }
      let ms = new MagicString(source)

      const normalizeUrl = async (url) => {
        // 获取绝对路径
        const resolved = await this.resolve(url, importer)
        if (resolved.id.startsWith(root + '/')) {
          url = resolved.id.slice(root.length)
        }
        return url
      }
      // 遍历解析出来的 import 
      for (let index = 0; index < imports.length; index++) {
        const { s: start, e: end, n: specifier } = imports[index]
        if (specifier) {
          const normalizedUrl = await normalizeUrl(specifier)
          if (normalizedUrl !== specifier) {
            // 重写导入路径的字符串
            ms.overwrite(start, end, normalizedUrl)
          }
        }
      }
      return ms.toString()
    }
  }
}

module.exports = importAnalysisPlugin

9.预代理插件

lib/plugins/preAlias.js


const path = require('path')
const fs = require('fs-extra')

// 从预编译信息中 读取第三方依赖文件存在的真实路径进行返回。
function preAliasPlugin() {
  let server
  return {
    name: 'vite:pre-alias',
    configureServer(_server) {
      server = _server
    },
    resolveId(id) {
      const metadata = server._optimizeDepsMetadata;
      const isOptimized = metadata.optimized[id]
      if (isOptimized) {
        return {
          id: isOptimized.file
        }
      }
    }
  }
}

module.exports = preAliasPlugin

10.将插件信息挂载到配置信息中去

lib/config.js

const path = require('path')
const { normalizePath } = require("./utils")
+ const { resolvePlugins } = require('./plugins')

async function resolveConfig() {
  // 获取当前进程执行的目录
  let root = normalizePath(process.cwd())
  // 存放预编译的信息
  const cacheDir = normalizePath(path.resolve(`node_modules/.vite3`))
  let config = {
    root,
    cacheDir
  }
+  // 取出注册的插件
+  const plugins = await resolvePlugins(config)
+  config.plugins = plugins
  return config
}

module.exports = resolveConfig

11.测试

我们新建一个项目分别使用原始的vite和我们编写的vite3启动项目

{
  "name": "use-vite3",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "start": "vite3"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "lodash": "^4.17.21",
    "vite": "^4.2.1",
    "vue": "^3.2.47"
  }
}

main.js

在其中引入vue,我们观察在浏览器端返回导入的是node_modules下缓存的路径,就代表我们成功了。

如下

小狐狸学Vite(七、修改导入路径)

import { ref } from 'vue'

let a = ref('value')

a.value = 100

console.log('main.js', a.value)

12.调试方法

如下面我们可以通过断点的形式进行调试,这里拿到的import xx from 'vue'中的路径就是取自preAlias插件返回的metadata信息中的真实路径。

小狐狸学Vite(七、修改导入路径)

创建lunch.json文件

小狐狸学Vite(七、修改导入路径)

小狐狸学Vite(七、修改导入路径)

小狐狸学Vite(七、修改导入路径)

lunch.json文件如下,

{
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch via NPM",
      "request": "launch",
      "runtimeArgs": ["run-script", "start"],  // start 就是我们要运行的npm命令
      "runtimeExecutable": "npm",
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

配置好之后我们就可以点下面这个三角形启动调试了。

小狐狸学Vite(七、修改导入路径)

点赞 👍

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

原文链接:https://juejin.cn/post/7214831216028041271 作者:一咻

(0)
上一篇 2023年3月27日 上午11:25
下一篇 2023年3月27日 上午11:36

相关推荐

发表回复

登录后才能评论