likes
comments
collection
share

vite中的虚拟模块技术能干的事还真不少

作者站长头像
站长
· 阅读数 53

大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在

github与好文

前言

我们在开发vite插件的时候,经常需要生成一些动态数据,并将这些数据作为依赖项被用户侧引入和使用,比如作者本人开发的unplugin-router中生成的动态路由

实现方案分析

方案一

在用户侧创建配置文件,并在插件运行阶段找到该文件后将数据写入,此时需要考虑是由用户自己新建该文件,还是有插件程序进行新建:

  • 让用户创建该文件

这会增加用户的心智负担,并且因为是动态的数据,在每一个人的电脑上大概都是不一样的,这大概率会导致git层面的conflict,如果不提交到仓库,则还需要手动设置.gitignore文件

  • 由插件完成该文件的创建工作

这是合理的方式,但将文件显示的创建到用户侧有一个几乎无法避免的问题,那就是:用户“手欠”修改了你的文件

方案二

基于方案一的不足,我们采用由插件内部生成和维护该文件,这样对用户侧是无感知的,并且我们也不需要考虑一些不受控的因素

前置准备

unplugin-router中的实现为例:

由于是TypeScript项目,我们必须为其设置对应的模块声明,否则会导致ts的引入报错,如下,使用declare定义一个名称为我们将要重定向文件url一致的module

declare module "~unplugin-router/router" {
  import type { Router, RouteRecordRaw } from "vue-router";

  interface LoadRouter {
    (visitor?: (routes: RouteRecordRaw[]) => RouteRecordRaw[]): Router;
  }
  const load: LoadRouter;

  export default load;
}

第二步,我们需要在package.json中定义该文件的exports

{
  "exports":{
    "./client":"./client.d.ts"
  }
}

第三步,在用户侧新建.d.ts文件并进行引入

/// <reference types="unplugin-router/client" />

对模块进行拦截重定向

目前,当用户引入我们的路径时

import loadRouter from "~unplugin-router/router";

由于此时vite是按照正常流程去读取文件模块的,肯定是要抛出异常的,因为在用户侧实际上并没有对应的文件,此时,我们的第一位主角就闪亮登场了:resolveId

我们通过该API,对vite的解析管道入口进行修改,当该API存在返回值时,将会被vite记录并替换、传递到下一个hook中进行解析。因此,我们如果能够将~unplugin-router/router这个import导入路径作为resolveId的return值,则在下一个阶段就能针对该值进行分析和处理

export function setVirtualId(this: Context, id: string) {
  const { resolveRouterId, pkgName, innerRouterFile } = global.UNROUTER_HELPERS;
  if (
    id.startsWith(resolveRouterId) ||
    id === innerRouterFile ||
    id.startsWith(`${pkgName}/client-router`) ||
    id === pkgName
  ) {
    return VIRTUAL_PREFIX + id;
  }
  return this.framework("virtual", {
    id,
    prefix: VIRTUAL_PREFIX,
  });
}

对拦截模块做二次加工处理

经历上一步后,vite将自动流转到下一个hook,并且在该hook中拿到的即是被resolveId篡改后的url,接下来有请我们的第二个主角:load

该hook会替换vite默认read文件模块的行为,我们可以在此回调中自定义处理逻辑,当然,首先我们要从n个import请求中定位到目标,如下,和前文中的setVirtualId是对等的

export function getVirtualId(id: string) {
  const _t = (flag: string) =>
    id.startsWith(flag) ? id.slice(flag.length) : "";
  return _t(VIRTUAL_PREFIX);
}

接着当监听到目标url后,我们使用node的readFile接口去读取我们在内部生成的动态数据,在unplugin-router中指的就是我们根据文件约定生成的路由表

if (resolveId === innerRouterFile) {
    return loadFileCode(innerRouterFile);
}

loadFileCode实现如下:

export function loadFileCode(url: string) {
  if (existsSync(url)) {
    const result = {
      code: readFileSync(url, "utf-8"),
      map: null,
    };
    return result;
  }
  return null;
}

可能存在的问题

如果你的文件模块内部又嵌套了其它的模块,则有可能会导致解析错误,此时我们还需要对子模块进行解析和处理,如下,我们将其转换拼接为绝对路径

function processChunk(this: Context, s: IMS, root: string) {
  const code = s.toString();
  const imps = parseImports(code);
  imps.forEach((v) => {
    if (v.from.includes("chunk")) {
      const {
        extra: { from },
      } = v;
      if (from) {
        const { start, end } = from;
        const actualPath = this.patch.sep(resolve(root, v.from));
        s.overwrite(start, end, actualPath);
      }
    }
  });
}

总结

本文是作者对vite中13个plugin api应用指南中的第一篇,后文还会通过《你的vite插件最好不要用handleHotUpdate来做hmr》、《如何在vite plugin中定位到App.vue文件?》等文章逐一介绍,感兴趣的可以自己先进行下查阅并结合文章的标题尝试实现对应的解决方案哦


如果本文对您有用,希望能得到您的点赞和收藏

订阅专栏,每周更新2-3篇类型体操,每月1-3篇vue3源码解析,等你哟😎


转载自:https://juejin.cn/post/7248788659930906679
评论
请登录