Vite插件开发及钩子执行过程

2,854 阅读4分钟

用vite初始化vue3项目,我们会发现vite启动快,因为它不需要打包,你肯定也会好奇vue文件中的各部分怎么被解析的,这时就需要运用官方提供的一些核心插件来解析vue文件,如@vitejs/plugin-vue插件,用来解析vue文件,一般都是函数调用,可以传入配置项,返回插件在plugins数组中。

image.png

项目中每次出现import,浏览器就会发送请求到devServer,就会通过vite.config.ts中添加的插件来解析文件、处理配置项及优化打包等,可以更好的扩展vite能力。

示例

vite 插件应该带 vite-plugin- 前缀,vite-plugin-vue- 前缀作为 vue 插件,接下来先通过简单的示例来实现一下插件,其中会涉及一下钩子函数,如

  • resolveId 用于命中第三方依赖
  • load 加载函数,可返回自定义的内容、
// my-plugin
export default function virtualModule() {
    const virtualModuleId = 'virtual-module';
  
    return {
      name: 'virtual-module', // 必须的,将会在 warning 和 error 中显示
      resolveId(id) { // 命中第三方依赖,执行load加载方法
        if (id === virtualModuleId) {  
          return virtualModuleId;
        }
        return null; // 返回null表明是其他id要继续处理
      },
      load(id) {
        if (id === virtualModuleId) {
          return `export const msg = "this is virtual module"`
        }
        return null;
      }
    }
}

// main.ts
import {msg} from 'virtual-module';
console.log(msg) // 输出: this is virtual module

了解了插件的写法后,去看看plugin-vue插件的运行过程中插件钩子都起了哪些作用:

@vitejs/plugin-vue

在开发过程中,总会想.vue文件中的templatescriptstyle是经过哪些处理被浏览器识别的呢。上面的示例知道插件导出配置对象,具体的解析细节还得去源码中寻找,对应每个钩子中的处理函数。

插件钩子执行顺序如下:

  • config => 解析vite配置前调用可以修改vite配置
  • configResolved => 解析vite配置后调用,不可修改,读取配置进行操作
  • options => 打包时,可以修改rollup选项,如果插件不是用于打包,不会用到
  • configureServer => 是用于配置dev server。在 connect 中添加自定义中间件。例如mock请求数据。
  • biuldStart => 开始构建
  • transformIndexHtml => 可以注入或者删除index.html 中的内容
  • resolvedId => 用于命中第三方依赖
  • load => 加载函数,可返回自定义的内容
  • transform => 对已经加载的模块内容进行修改
  • buildEnd => 结束构建

接下来按照运行顺序看看都执行了哪些操作。

config、configResolved

config(config) {
  return {
    define: {
      __VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
      __VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false
    },
    ssr: {
      external: ['vue', '@vue/server-renderer']
    }
  }
},

configResolved(config) {
  options = {
    ...options,
    root: config.root,
    sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
    isProduction: config.isProduction
  }
},

config中返回的配置会与vite配置合并,configResolved在options中添加根目录rootisProduction属性,以便提供给后续的钩子函数使用。

configureServer

configureServer(server) {
  options.devServer = server
}

添加devServer服务

biuldStart

configureServer(server) {
  options.compiler = options.compiler || resolveCompiler(options.root)
}

添加编译方法,涉及到一些编译的内置方法

load

image.png 点击链接,浏览器发起请求时,vite会处理文件在其路径上添加?vue,并通过参数&type来区分类型,如template(模板)、script(js 脚本)、css等,进入load钩子,根据不同类型进行编译处理。

image.png 在钩子函数中通过parseVueRequest方法获取文件名及携带参数。

export function parseVueRequest(id: string): {
  filename: string
  query: VueQuery
} {
  const [filename, rawQuery] = id.split(`?`, 2)
  const query = qs.parse(rawQuery) as VueQuery
  if (query.vue != null) {
    query.vue = true
  }
  if (query.index != null) {
    query.index = Number(query.index)
  }
  if (query.raw != null) {
    query.raw = true
  }
  return {
    filename,
    query
  }
}

通过文件目录解析编译获取文件中不同模块,返回文件中对应的template、script、style模块,加载完成.

load(id, opt) {
  ...
  const { filename, query } = parseVueRequest(id)
  if (query.vue) {
    if (query.src) {
      return fs.readFileSync(filename, 'utf-8')
    }
    const descriptor = getDescriptor(filename, options)!
    let block: SFCBlock | null | undefined
    if (query.type === 'script') {
      block = getResolvedScript(descriptor, ssr)
    } else if (query.type === 'template') {
      block = descriptor.template!
    } else if (query.type === 'style') {
      block = descriptor.styles[query.index!]
    } else if (query.index != null) {
      block = descriptor.customBlocks[query.index]
    }
    if (block) {
      return {
        code: block.content,
        map: block.map as any
      }
    }
  }
},

transform

load返回的内容进行转译,浏览器并不识别vue文件中的组件、事件处理以及其他语法糖等,需要先将 template 模板语法解析成了 AST 语法树,根据生成的对象,createVnode创建虚拟dom,通过vue diff算法,移动或创建真实dom,完成解析。

transform(code, id, opt) {
  ...
  if (!query.vue) {
    return transformMain(
      code,
      filename,
      options,
      this,
      ssr,
      customElementFilter(filename)
    )
  } else {
    ...
    // 拆分后的template、style转译
    if (query.type === 'template') {
      return transformTemplateAsModule(code, descriptor, options, this, ssr)
    } else if (query.type === 'style') {
      return transformStyle(
        code,
        descriptor,
        Number(query.index),
        options,
        this
      )
    }
  }
}

总结

通过plugin-vue插件的运行过程,进一步了解了vite插件,知道各钩子函数的执行顺序及运行过程,也可以在解决问题过程中尝试另一种方案。