哈喽,很快乐你能点开这篇博客,本博客是针对 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-markdown.*\/\>/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 `
<section class='article-content'>
${md.render(mdText)}
</section>
`;
}
而后,咱们在下面的遍历流程中,退出这个转换函数,再将原来的标签替换成转换后的文本即可,实现如下:
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 scoped>
${style}
</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<string, string>();
而后在 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 共享 |
如果大家发现有什么比拟好的文章或者文档对这些钩子函数有更具体的介绍,也欢送分享进去。
到这篇文章地位,总共 6 期的 Vite
系列文章也圆满画上了句号,谢谢大家的反对。
最初一件事
如果您曾经看到这里了,心愿您还是点个赞再走吧~
您的点赞是对作者的最大激励,也能够让更多人看到本篇文章!
如果感觉本文对您有帮忙,请帮忙在 github 上点亮 star
激励一下吧!