“ 本文正在参加「金石计划」 ”
文章导航
先来回顾一下,在上一节我们已经做到的事情,看以下代码
import { ref } from 'vue'
let a = ref('value')
a.value = 100
console.log('main.js', a.value)
通过我们自己编写的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跑当前的程序,运行结果如下。
本篇文章就来解决当前的问题。
首先我们要做的就是让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 文件正常返回
再来看看App.vue请求的返回, 我们只需要在这里解析.vue文件的内容就可以了。
代码片段解释
const { parse, compileScript, rewriteDefault, compileTemplate } = require('@vue/compiler-sfc')
// 根据路径拿到文件内容
const content = await fs.promises.readFile(filename, 'utf8')
// 解析文件 内容
const result = parse(content, { filename })
拿到的文件描述器就是下面这样的一个对象,
编译script代码
function genScriptCode(descriptor, id) {
let scriptCode = ''
let script = compileScript(descriptor, { id })
if (!script.lang) {
scriptCode = rewriteDefault(script.content, '_sfc_main') // 需要重写成非模块的形式
}
return scriptCode
}
编译 template模板后的结果
最终组装好返回的文件
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
现在,就可以在使用我们自己的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
点赞 👍
通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~