likes
comments
collection
share

⚡️ Vite 官方教程插件实现与踩坑从与 Vite 的相知,相识到在工作中使用已经一年有余,用 Anthony Fu

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

从与 Vite 的相知,相识到在工作中使用已经一年有余,用 Anthony Fu 的话说这期间 Vite 社区爆炸式的增长,大量周边生态也在不断的增加;即这也表明 Vite 使用过程中难免会有一些社区可能还没有即时跟进的功能、插件。工作中这种情况很容易碰到,那么学会写一个自己的 Vite 插件是很有必要的!

本文目的针对官方的入门级插件教程做一个补充、实现;希望通过学习本文你能够自己发散下思维写一个自己的插件,或解决当下在工作中遇到的问题。

有小伙伴可能要问了,官方这个插件就这么几行代码没啥好说的呀!你确定?不信你自己用官方的代码放到自己的 Vite 项目中试试看呢。准确的来说,官方这个 Demo 放到 Rollup 中肯定没有问题,但是放到 Vite 中不一定哦!

Vite vs Rollup

在这之前,我们且先“肤浅”的谈谈 Vite 的大概组成

Vite 中有三个重要概念,serverbuildPre-building 既对应着内置的三条命令 vit servevite buildvite optimizer

  • build 这个就是 Rollup + 内置插件。。。很短 😢

  • serve 命令即一个 Node.js httpServer,Vite 通过 connect 链接了一些中间件用来处理各种请求,比如比较重要的中间件 - transformMiddleware 以及内部引入的 transformRequest

部分 Vite 源码

// src/node/server/middlewares/transform.ts
export function transformMiddleware(
  server: ViteDevServer
): Connect.NextHandleFunction

// src/node/server/transformRequest.ts
export function transformRequest(
  url: string,
  server: ViteDevServer,
  options: TransformOptions = {}
): Promise<TransformResult | null> {
  // 🚧 重点:内部实现了 Rollup plugin 的运行机制
}

transformRequest 负责的就是将请求合理的走一遍 插件集合,这个地方 Vite 会执行一大堆内置的插件,注意这里只是 模仿 Rollup 而不是用到 Rollup

内置插件源码

// src/node/plugins/index.ts

export async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[]
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'

  const buildPlugins = isBuild
    ? (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] }

  return [
    isBuild ? metadataPlugin() : null,
    isBuild ? null : preAliasPlugin(),
    aliasPlugin({ entries: config.resolve.alias }),
    // 🚨重点③ -- 记下来,一会儿要考的
    ...prePlugins,
    config.build.polyfillModulePreload
      ? modulePreloadPolyfillPlugin(config)
      : null,
    // 🚨重点① -- 记下来,一会儿要考的
    resolvePlugin({
      ...config.resolve,
      root: config.root,
      isProduction: config.isProduction,
      isBuild,
      packageCache: config.packageCache,
      ssrConfig: config.ssr,
      asSrc: true
    }),
    isBuild ? null : optimizedDepsPlugin(),
    htmlInlineProxyPlugin(config),
    cssPlugin(config),
    config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
    jsonPlugin(
      {
        namedExports: true,
        ...config.json
      },
      isBuild
    ),
    wasmPlugin(config),
    webWorkerPlugin(config),
    workerImportMetaUrlPlugin(config),
    assetPlugin(config),
    // 🚨重点② -- 记下来,一会儿要考的
    ...normalPlugins,
    definePlugin(config),
    cssPostPlugin(config),
    config.build.ssr ? ssrRequireHookPlugin(config) : null,
    ...buildPlugins.pre,
    ...postPlugins,
    ...buildPlugins.post,
    // internal server-only plugins are always applied after everything else
    ...(isBuild
      ? []
      : [clientInjectionsPlugin(config), importAnalysisPlugin(config)])
  ].filter(Boolean) as Plugin[]
}

看起来一大堆插件,也可以说 Vite 是 “Rollup + 内置插件集合” 的更上层的工具,但是 Vite 在启动之前还有个重要的概念 - Pre-building

  • optimizer 既是对外宣称的 Pre-building 主要负责所有的 裸模块(bare-package) 的构建,什么是裸模块?不是绝对路径、也不是相对路径、也不是别名,或内置的标识符开头的 - 就是裸模块,听着好像很绕的亚子!不精确的一句话总结即 “npm 模块”
// 都不是裸模块
import './mod1'
import '/User/project/src/mod1'
import '@/mod1'
import '/@fs/node_modules/npm-pkg'

// 我是裸模块
import 'module-name'

比如你有个裸模块 vue 那这个模块肯定会被 Pre-building 构建到 node_modules/.vite/vue.js 中的 - 没得跑 重点来了 这时候你用官方的 Demo 插件思路,可能写了这么个插件 - 将 vue 变成 external 模块导入,这很常见吧!Webpack 中大把大把的案例

// vite.config.ts
export default {
  plugin: [
    {
      name: 'external-vue-plugin',
      resolveId(id) {
        if (id === 'vue') {
          return '\0' + 'vue'
        }
      },
      load(id) {
        if (id === '\0' + 'vue') {
          return `const vue=window.Vue; export default Vue;`
        }
      },
    },
  ],
}

就这么 easy 的一个 external 的插件,发到 npm 上周下载量几k不难,面试装逼妥妥滴~ 🤩

还记得上面的 🚨重点① 么,而你的插件排在 🚨重点② 的地方;即实际运行时候,你的插件没机会处理 vue 这裸模块,反而会被内置的 resolvePlugin 给传送到 optimizer 中给预构建掉!而后在 Vite 运行时加载裸模块且刚好找到了 node_modules/.vite/vue.js 预构建模块就会优先命中,干脆不鸟你写的 load 钩子。这就是为啥说官方的 Demo 可能不是按照预期运行

实现一个按照预期运行的 Demo

既然我们自己实现的,别总叫它 Demo 了,就好像喊人家张三李四似的;干脆起个名字吧 vite-plugin-resolve - 就是要和内置的 resolvePlugin 对标!

我们可以将我们的插件提前到 🚨重点③ 的地方来实现我们的预期行为。

// vite.config.ts
export default {
  plugin: [
    {
      name: 'vue-plugin-resolve',
      // 我要去 “🚨重点③” 的位置
      enforce: 'pre',
      resolveId(id) { /* 同原来的逻辑 */ },
      load(id) { /* 同原来的逻辑 */ },
      config(config) {
        // 告诉 Vite 不要预购建 vite 模块
        config.optimizeDeps.exclude.push('vue');
      },
    },
  ],
}

嗯! 就这么简单,两处改动就可能完美实现 Vite 官方的 Demo 了。是不是好神奇的!

不要小瞧这个插件,它能实现你想要的几乎所有功能,比如 externals、加载预先定义好的模板,可以是任何可执行的 js 代码。发挥空间极大!比如我们这个插件还支持自定义返回:

import resolve from 'vite-plugin-resolve'

export default {
  plugins: [
    resolve({
      // 当 Webpack 的 externals 用
      vue: `const vue = window.Vue; export { vue as default }`,

      // 使用 Promise 的方式读取一个模版文件
      '@scope/name': () => require('fs').promises.readFile('path', 'utf-8'),

      // 用在 Electron 中
      electron: `const { ipcRenderer } = require('electron'); export { ipcRenderer };`,
    }),
  ],
}

完整代码 👉 vite-plugin-resolve

或 npm 安装 npm i vite-plugin-resolve -D

小结

一个小小 Demo 引发的思考,现实中往往能发挥很大威力; 希望此文章能使你对 Vite 插件有个更清晰的认识,不要因为害怕没有足够的社区插件而对 Vite 望而却步; 同时也希望 Vite 生态有更多的小伙伴加入、共建 Vite 这个大生态!

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