哈喽,很高兴你能点开这篇博客,本博客是针对 Vite
的体验系列文章之实战篇,认真看完后相信你也能如法炮制写一个属于自己的 vite 插件。
Vite
是一种新型的前端构建工具,能够显著提升前端开发体验。
我将会从 0 到 1 完成一个 vite:markdown
插件,该插件可以读取项目目录中的 markdown
文件并解析成 html
,最终渲染到页面中。如果你还没有使用过
Vite
,那么你可以看看我的前两篇文章,我也是刚体验没两天呢。(如下)- Vite + Vue3 初体验 —— Vite 篇
- Vite + Vue3 初体验 —— Vue3 篇
Vite
源码进行了解读,往期文章可以看这里:- Vite 源码解读系列(图文结合) —— 本地开发服务器篇
- Vite 源码解读系列(图文结合) —— 构建篇
- Vite 源码解读系列(图文结合) —— 插件篇
vite
插件的实现思路就是 webpack
的 loader
+ plugin
,我们这次要实现的 markdown
插件其实更像是 loader
的部分,但是也会利用到 vite
插件的一些钩子函数(比如热重载)。我需要先准备一个对
markdown
文件进行转换,转换成 html
的插件,这里我使用的是 markdown-it
,这是一个很流行的 markdown
解析器。其次,我需要识别代码中的
markdown
标签,并读取标签中指定的 markdown
文件,这一步可以使用正则加上 node
的 fs
模块做到。好,实现思路都理清了,我们现在可以来实现这个插件了。
初始化插件目录 我们使用
npm init
命令来初始化插件,插件名称命名为 @vitejs/plugin-markdown
。为了方便调试,该插件目录我直接创建在我的 Vite Demo 项目 中。在
本次插件实战的仓库地址为 @vitejs/plugin-markdown,感兴趣的同学也可以直接下载代码来看。
package.json
中,我们先不用着急设置入口文件,我们可以先把我们的功能实现。创建测试文件 这里,我们在测试项目中创建一个测试文件
TestMd.vue
和 README.md
,文件内容和最终效果如下图所示。文章图片
在创建好了测试文件后,我们现在就要来研究怎么实现了。
创建插件入口文件 ——
index.ts
下面,我们来创建插件入口文件 —— index.ts
。该文件的内容主要是包含了vite
的插件支持ts
,所以这里我们直接使用typescript
来编写这个插件。
name
、enforce
、transform
三个属性。- name: 插件名称;
- enforce: 该插件在 plugin-vue 插件之前执行,这样就可以直接解析到原模板文件;
- transform: 代码转译,这个函数的功能类似于
webpack
的loader
。
export default function markdownPlugin(): Plugin {
return {
// 插件名称
name: 'vite:markdown',// 该插件在 plugin-vue 插件之前执行,这样就可以直接解析到原模板文件
enforce: 'pre',// 代码转译,这个函数的功能类似于 `webpack` 的 `loader`
transform(code, id, opt) {}
}
}module.exports = markdownPlugin
markdownPlugin['default'] = markdownPlugin
过滤非目标文件
接下来,我们要对文件进行过滤,将非
vue
文件、未使用 g-markdown
标签的 vue
文件进行过滤,不做转换。在
transform
函数的开头,加入下面这行正则代码进行判断即可。const vueRE = /\.vue$/;
const markdownRE = /\/g;
if (!vueRE.test(id) || !markdownRE.test(code)) return code;
将
markdown
标签替换成 html
文本接下来,我们要分三步走:
- 匹配
vue
文件中的所有g-markdown
标签 - 加载对应的
markdown
文件内容,将markdown
文本转换为浏览器可识别的html
文本 - 将
markdown
标签替换成html
文本,引入style
文件,输出文件内容
vue
文件中所有的 g-markdown
标签,依旧是使用上面的那个正则:const mdList = code.match(markdownRE);
然后对匹配到的标签列表进行一个遍历,将每个标签内的
markdown
文本读取出来:const filePathRE = /(?<=file=("|')).*(?=('|"))/;
mdList?.forEach(md => {
// 匹配 markdown 文件目录
const fileRelativePaths = md.match(filePathRE);
if (!fileRelativePaths?.length) return;
// markdown 文件的相对路径
const fileRelativePath = fileRelativePaths![0];
// 找到当前 vue 的目录
const fileDir = path.dirname(id);
// 根据当前 vue 文件的目录和引入的 markdown 文件相对路径,拼接出 md 文件的绝对路径
const filePath = path.resolve(fileDir, fileRelativePath);
// 读取 markdown 文件的内容
const mdText = file.readFileSync(filePath, 'utf-8');
//...
});
mdText
就是我们读取的 markdown
文本(如下图)文章图片
接下来,我们需要实现一个函数,来对这一段文本进行转换,这里我们使用之前提到的插件
markdown-it
,我们新建一个 transformMarkdown
函数来完成这项工作,实现如下:const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
export const transformMarkdown = (mdText: string): string => {
// 加上一个 class 名为 article-content 的 wrapper,方便我们等下添加样式
return `${md.render(mdText)}`;
}
然后,我们在上面的遍历流程中,加入这个转换函数,再将原来的标签替换成转换后的文本即可,实现如下:
mdList?.forEach(md => {
//...
// 读取 markdown 文件的内容
const mdText = file.readFileSync(filePath, 'utf-8');
// 将 g-markdown 标签替换成转换后的 html 文本
transformCode = transformCode.replace(md, transformMarkdown(mdText));
});
在得到了转换后的文本后,此时页面已经可以正常显示了,我们最后在
transform
函数中添加一份掘金的样式文件,实现如下:transform(code, id, opt) {
//...
// style 是一段样式文本,文本内容很长,这里就不贴出来了,感兴趣的可以在原仓库找到
transformCode = `
${transformCode}${style}`// 将转换后的代码返回
return transformCode;
}
@vitejs/plugin-markdown 实战插件地址引用插件 我们需要在测试项目中引入插件,我们在
vite.config.ts
中进行配置即可,代码实现如下:在实际开发中,这一步应该早做,因为提前引入插件,插件代码的变更可以实时看到最新效果。
在引入插件后,可能会报某些依赖丢失,此时需要在测试项目中先安装这些依赖进行调试(生产环境不需要),例如markdown-it
。
import { defineConfig } from 'vite'
import path from 'path';
import vue from '@vitejs/plugin-vue'
import markdown from './plugin-markdown/src/index';
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
}
},
plugins: [
vue(),
// 引用 @vitejs/plugin-markdown 插件
markdown()
]
});
然后,使用
vite
命令,启动我们的项目(别忘了在 App.vue
中引入测试文件 TestMd.vue
),就可以看到下面这样的效果图啦!(如下图)文章图片
配置热重载 此时,我们的插件还缺一个热重载功能,没有配置该功能的话,修改
md
文件是无法触发热重载的,每次都需要重启项目。我们需要在插件的
handleHotUpdate
钩子函数中,对我们的 md
类型文件进行监听,再将依赖该 md
文件的 vue
文件进行热重载。在此之前,我们需要先在
transform
的遍历循环中,存储引入了 md
文件的 vue
文件吧。在插件顶部创建一个
map
用于存储依赖关系,实现如下const mdRelationMap = new Map();
然后在
transform
中存储依赖关系。mdList?.forEach(md => {
//...
// 根据当前 vue 文件的目录和引入的 markdown 文件相对路径,拼接出 md 文件的绝对路径
const mdFilePath = path.resolve(fileDir, fileRelativePath);
// 记录引入当前 md 文件的 vue 文件 id
mdRelationMap.set(mdFilePath, id);
});
然后,我们配置新的热重载钩子 ——
handleHotUpdate
就可以了,代码实现如下:handleHotUpdate(ctx) {
const { file, server, modules } = ctx;
// 过滤非 md 文件
if (path.extname(file) !== '.md') return;
// 找到引入该 md 文件的 vue 文件
const relationId = mdRelationMap.get(file) as string;
// 找到该 vue 文件的 moduleNode
const relationModule = [...server.moduleGraph.getModulesByFile(relationId)!][0];
// 发送 websocket 消息,进行单文件热重载
server.ws.send({
type: 'update',
updates: [
{
type: 'js-update',
path: relationModule.file!,
acceptedPath: relationModule.file!,
timestamp: new Date().getTime()
}
]
});
// 指定需要重新编译的模块
return [...modules, relationModule]
},
此时,我们修改我们的 md 文件,就可以看到页面实时更新啦!(如下图)
文章图片
顺便吐槽一下,关于到这里,我们的插件开发工作就完成啦。handleHotUpdate
处理的文档内容很少,server.moduleGraph.getModulesByFile
这个API
还是在vite issue
里面的代码片段里找到的,如果大家发现有相关的文档资源,也请分享给我,谢谢。
发布插件 在上面的步骤中,我们都是使用本地调试模式,这样的包分享起来会比较麻烦。
接下来,我们把我们的包构建出来,然后传到
npm
上,供大家安装体验。我们在
package.json
中,添加下面几行命令。"main": "dist/index.js", // 入口文件
"scripts": {
// 清空 dist 目录,将文件构建到 dist 目录中
"build": "rimraf dist && run-s build-bundle",
"build-bundle": "esbuild src/index.ts --bundle --platform=node --target=node12 --external:@vue/compiler-sfc --external:vue/compiler-sfc --external:vite --outfile=dist/index.js"
},
然后,别忘了安装
rimraf
、run-s
、esbuild
相关依赖,安装完依赖后,我们运行 npm run build
,就可以看到我们的代码被编译到了 dist
目录中。当所有都准备就绪后,我们使用
npm publish
命令发布我们的包就可以啦。(如下图)文章图片
然后,我们可以将
vue.config.ts
中的依赖换成我们构建后的版本,实现如下:// 由于我本地网络问题,我这个包传不上去,这里我直接引入本地包,和引用线上 npm 包是同理的
import markdown from './plugin-markdown';
然后我们运行项目,成功解析
markdown
文件即可!(如下图)文章图片
小结 到这里,我们本期教程就结束了。
想要更好的掌握
vite
插件的开发,还是要对下面几个生命周期钩子的作用和职责有清晰的认识。字段 | 说明 | 所属 |
---|---|---|
name |
插件名称 | vite 和 rollup 共享 |
handleHotUpdate |
执行自定义 HMR(模块热替换)更新处理 | vite 独享 |
config |
在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并 |
vite 独享 |
configResolved |
在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作 |
vite 独享 |
configureServer |
是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件。 | vite 独享 |
transformIndexHtml |
转换 index.html 的专用钩子。 |
vite 独享 |
options |
在收集 rollup 配置前,vite (本地)服务启动时调用,可以和 rollup 配置进行合并 |
vite 和 rollup 共享 |
buildStart |
在 rollup 构建中,vite (本地)服务启动时调用,在这个函数中可以访问 rollup 的配置 |
vite 和 rollup 共享 |
resolveId |
在解析模块时调用,可以返回一个特殊的 resolveId 来指定某个 import 语句加载特定的模块 |
vite 和 rollup 共享 |
load |
在解析模块时调用,可以返回代码块来指定某个 import 语句加载特定的模块 |
vite 和 rollup 共享 |
transform |
在解析模块时调用,将源代码进行转换,输出转换后的结果,类似于 webpack 的 loader |
vite 和 rollup 共享 |
buildEnd |
在 vite 本地服务关闭前,rollup 输出文件到目录前调用 |
vite 和 rollup 共享 |
closeBundle |
在 vite 本地服务关闭前,rollup 输出文件到目录前调用 |
vite 和 rollup 共享 |
【Vite 实战(手把手教你写一个 Vite 插件)】到这篇文章位置,总共 6 期的
Vite
系列文章也圆满画上了句号,谢谢大家的支持。最后一件事 如果您已经看到这里了,希望您还是点个赞再走吧~
您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!
如果觉得本文对您有帮助,请帮忙在 github 上点亮
star
鼓励一下吧!