小狐狸学Vite(八、支持vue插件)

922 阅读5分钟

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

文章导航

一、核心知识

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

四、实现静态文件中间件

五、分析第三方依赖

六、预编译并保存metadata信息

七、修改导入路径

先来回顾一下,在上一节我们已经做到的事情,看以下代码

import { ref } from 'vue'

let a = ref('value')

a.value = 100

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

image.png

通过我们自己编写的vite3启动当前项目,可以看见我们自己的vite3已经将引入的第三方依赖路径重写成了通过esbuild预编译过的文件路径了。我们可以使用vue中给我们暴露出来的方法创建响应式数据了。

本小节,我们的主要目的是要实现下面的功能,让我们自己的vite3支持可以导入.vue文件。也就是我们编写一个.vue组件然后可以渲染到页面上去。

代码如下

main.js

import { createApp } from 'vue'

import App from './App.vue'

createApp(App).mount("#app")

App.vue

<template>
  <h1>App</h1>
</template>

<script>
export default {
  name: "App",
};
</script>

现在用我们的vite3跑当前的程序,运行结果如下。

image.png

本篇文章就来解决当前的问题。

首先我们要做的就是让esbuild认识.vue文件,并把.vue文件标识为一个外部模块,这样就不会对.vue文件里面的import再进行依赖分析了。

正文

1. esbuild 插件中添加代码

esbuildDepPlugin.js

...
  return {
    name: 'vite-:dep-scan',
    setup(build) {
      // 处理 .vue 结尾的文件
      build.onResolve({ filter: /\.vue$/ }, async ({ path: id, importer }) => {
        const resolved = await resolve(id, importer)
        if (resolved) {
          return {
            path: resolved.id,
            external: true  // 标记为一个外部模块
          }
        }
      })
      
...

2. vue文件也当做js文件在http请求中

lib/utils.js

const knownJsSrcRE = /\.(js|vue)/;

3. 解析vite.config.js配置文件

lib\config.js


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

async function resolveConfig() {
  // 获取当前进程执行的目录
  let root = normalizePath(process.cwd())
  // 存放预编译的信息
  const cacheDir = normalizePath(path.resolve(`node_modules/.vite3`))
  let config = {
    root,
    cacheDir
  }
+  // 读取 用户在 vite.config.js 中的配置信息
+  const jsConfigFile = path.resolve(root, 'vite.config.js')
+  const exists = await fs.pathExists(jsConfigFile)  // 判断当前路径的文件是否存在
+  if (exists) {
+    const userConfig = require(jsConfigFile)
+    config = { ...config, ...userConfig }
+  }
+  // 读取用户配置的插件信息
+  const userPlugins = config.plugins || []
+  const plugins = await resolveConfig(config, userPlugins)
  // 取出注册的插件
  config.plugins = plugins
  return config
}

module.exports = resolveConfig

lib/plugins/index.js

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

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

exports.resolvePlugins = resolvePlugins

4. vite.config.js支持传入解析.vue文件的插件

在我们使用vite3的项目中创建vite.config.js文件,将我们自己写的解析.vue文件的插件传递给vite配置文件,在内部会传递给上面我们写到的resolveConfig(config, userPlugins)中去。

use-vite3/vite.config.js


const vue = require('./plugins/vue')

// 引入 支持 解析 .vue 文件的插件
module.exports = {
  plugins: [vue()]
}

5. 编写解析.vue文件的vite插件

这里我们主要使用的是 vue/complier-sfc这个包来解析vue单文件,将其解析问js文件返回给客户端,然后浏览器加载执行js.

use-vite3/plugins/vue.js

我们先来尝试使用一下 transform钩子函数


// 一个函数,返回一个对象

function vue() {
  return {
    name: 'vue',
    async transform(code, id) {
      const { filename } = parseVueRequest(id)
      // 如果匹配到 请求的文件名是 .vue 结尾的文件则对源代码进行转换
      if (filename.endsWith('.vue')) {
        return 'hihihi'
      }
      // 否则不进行处理
      return null
    }
  }
}

// 解析请求路径 和请求 参数
function parseVueRequest(id) {
  const [filename, queryString = ''] = id.split('?')
  let query = new URLSearchParams(queryString)
  return {
    filename,
    query
  }
}

module.exports = vue

main.js 文件正常返回

image.png

再来看看App.vue请求的返回, 我们只需要在这里解析.vue文件的内容就可以了。

image.png

代码片段解释

const { parse, compileScript, rewriteDefault, compileTemplate } = require('@vue/compiler-sfc')

  // 根据路径拿到文件内容
  const content = await fs.promises.readFile(filename, 'utf8')
  // 解析文件 内容
  const result = parse(content, { filename })

拿到的文件描述器就是下面这样的一个对象,

image.png

编译script代码

function genScriptCode(descriptor, id) {
  let scriptCode = ''
  let script = compileScript(descriptor, { id })
  if (!script.lang) {
    scriptCode = rewriteDefault(script.content, '_sfc_main')  // 需要重写成非模块的形式
  }
  return scriptCode
}

image.png

image.png

编译 template模板后的结果

image.png

最终组装好返回的文件

async function transformMain(source, filename) {
  const descriptor = await getDescriptor(filename)
  // 从描述器中获取 Js 代码
  const scriptCode = genScriptCode(descriptor, filename)
  // 获取 template 编译后的代码
  const templateCode = genTemplateCode(descriptor, filename)

  let resolvedCode = [
    templateCode, // 编译成了 render 函数
    scriptCode,
    `_sfc_main['render'] = render`,
    `export default _sfc_main`     // 一个文件要默认导出
  ].join('\n')
  return {
    code: resolvedCode
  }
}
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

// 这里导入的 还是 vue 
// 因为我们后面还要执行重写第三方模块导入的内部插件的时候就会将其路径进行重写。

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("h1", null, "App"))
}

const _src_main = {
  name: "App",
};

_sfc_main['render'] = render
export default _sfc_main

image.png

现在,就可以在使用我们自己的vite3的时候,在vite.config.js文件中添加解析vue文件的插件,在项目中导入vue组件进行页面的渲染了。

6. 完整的vite解析.vue的插件


// 一个函数,返回一个对象

const { parse, compileScript, rewriteDefault, compileTemplate } = require('@vue/compiler-sfc')
const fs = require('fs-extra')

const descriptorCache = new Map()

function vue() {
  return {
    name: 'vue',
    async transform(code, id) {
      const { filename } = parseVueRequest(id)
      // 如果匹配到 请求的文件名是 .vue 结尾的文件则对源代码进行转换
      if (filename.endsWith('.vue')) {
        let result = await transformMain(code, filename)
        return result
      }
      // 否则不进行处理
      return null
    }
  }
}

// 获取描述器
async function getDescriptor(filename) {
  // 为了性能,使用 一个map 来做缓存
  let descriptor = descriptorCache.get(filename)
  if (descriptor) return descriptor

  // 根据路径拿到文件内容
  const content = await fs.promises.readFile(filename, 'utf8')
  // 解析文件 内容
  const result = parse(content, { filename })
  descriptor = result.descriptor
  // 将 parse 后的结果缓存起来
  descriptorCache.set(filename, descriptor)
  return descriptor
}

async function transformMain(source, filename) {
  const descriptor = await getDescriptor(filename)
  // 从描述器中获取 Js 代码
  const scriptCode = genScriptCode(descriptor, filename)
  // 获取 template 编译后的代码
  const templateCode = genTemplateCode(descriptor, filename)

  let resolvedCode = [
    templateCode, // 编译成了 render 函数
    scriptCode,
    `_sfc_main['render'] = render`,
    `export default _sfc_main`     // 一个文件要默认导出
  ].join('\n')
  return {
    code: resolvedCode
  }
}

function genScriptCode(descriptor, id) {
  let scriptCode = ''
  let script = compileScript(descriptor, { id })
  if (!script.lang) {
    scriptCode = rewriteDefault(script.content, '_sfc_main')
  }
  return scriptCode
}

function genTemplateCode(descriptor, id) {
  let content = descriptor.template.content
  const result = compileTemplate({ source: content, id })
  return result.code
}

// 解析请求路径 和请求 参数
function parseVueRequest(id) {
  const [filename, queryString = ''] = id.split('?')
  let query = new URLSearchParams(queryString)
  return {
    filename,
    query
  }
}

module.exports = vue

点赞 👍

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