Hello, I'm glad you can click on this blog. This blog is the actual combat article of the experience series of Vite
. After reading it carefully, I believe that you can also write a vite plugin of your own.
Vite
is a new front-end building tool that can significantly improve the front-end development experience.
I will complete a vite:markdown
plugin from 0 to 1, the plugin can read the markdown
file in the project directory and parse it into html
, and finally render it into the page.
If you haven't used Vite
, then you can read my first two articles, I just experienced it for two days. (as follows)
This series of documents also interprets the source code of Vite
. Previous articles can be found here:
- Vite Source Code Interpretation Series (Graphic and Text Combination) - Local Development Server
- Vite Source Code Interpretation Series (Graphic and Text Combination) - Construction
- Vite Source Code Interpretation Series (Graphic and Text Combination) - Plugin
Realize ideas
In fact, the implementation idea of vite
plugin is webpack
+ plugin
of loader
. The markdown
plugin we want to implement this time is actually more like the part of loader
, but it will also use the vite
plugin.
I need to prepare a plugin that converts markdown
files into html
, here I use markdown-it
, which is a very popular markdown
parser.
Second, I need to identify the markdown
tag in the code, and read the markdown
file specified in the tag. This step can be done using the fs
module with regular plus node
.
Ok, the implementation ideas are clarified, we can now implement this plug-in.
Initialize the plugin directory
We use the npm init
command to initialize the plugin, the plugin name is named @vitejs/plugin-markdown
.
For the convenience of debugging, I created the plugin directory directly in my Vite Demo project .
The warehouse address for this plugin is @vitejs/plugin-markdown . Interested students can also download the code directly.
In package.json
, we don't have to worry about setting the entry file, we can implement our function first.
Create test files
Here, we create a test file TestMd.vue
and README.md
in the test project, the file content and final effect are shown in the figure below.
After creating the test file, we are now going to study how to implement it.
Create plugin entry file - index.ts
Next, let's create the plugin entry file - index.ts
.
The plugin ofvite
supportsts
, so here we directly usetypescript
to write this plugin.
The content of the file mainly contains three attributes: name
, enforce
, and transform
.
- name: plugin name;
- enforce: The plugin is executed before the plugin-vue plugin, so that it can be directly parsed to the original template file;
- transform: Code translation, this function is similar to
webpack
ofloader
.
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
Filter non-target files
Next, we need to filter the files, and filter the non- vue
vue
that do not use the g-markdown
tag without converting them.
At the beginning of the transform
function, add the following line of regular code to judge.
const vueRE = /\.vue$/;
const markdownRE = /\<g-markdown.*\/\>/g;
if (!vueRE.test(id) || !markdownRE.test(code)) return code;
Replace markdown
label with html
text
Next, we have to go in three steps:
- matches all
g-markdown
tags in thevue
file - Load the corresponding
markdown
file content and convert themarkdown
text tohtml
text that can be recognized by the browser - Replace the
markdown
tag withhtml
text, import thestyle
file, and output the file content
Let's first match all the g-markdown
tags in the vue
file, still using the above regular:
const mdList = code.match(markdownRE);
Then perform a traversal of the matched tag list, and read the markdown
text in each tag:
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
is the markdown
text we read (as shown below)
Next, we need to implement a function to convert this piece of text. Here we use the previously mentioned plugin markdown-it
. We create a new transformMarkdown
function to complete this work. The implementation is as follows:
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>
`;
}
Then, in the above traversal process, we add this conversion function, and then replace the original label with the converted text. The implementation is as follows:
mdList?.forEach(md => {
//...
// 读取 markdown 文件的内容
const mdText = file.readFileSync(filePath, 'utf-8');
// 将 g-markdown 标签替换成转换后的 html 文本
transformCode = transformCode.replace(md, transformMarkdown(mdText));
});
After getting the converted text, the page can be displayed normally. We finally add a nugget style file to the transform
function, which is implemented as follows:
transform(code, id, opt) {
//...
// style 是一段样式文本,文本内容很长,这里就不贴出来了,感兴趣的可以在原仓库找到
transformCode = `
${transformCode}
<style scoped>
${style}
</style>
`
// 将转换后的代码返回
return transformCode;
}
@vitejs/plugin-markdown actual plugin address
Citation plugin
We need to introduce a plug-in into the test project, we can configure it in vite.config.ts
. The code is implemented as follows:
In actual development, this step should be done early, because the plug-in is introduced in advance, and the latest effect of the plug-in code changes can be seen in real time.
After the plugin is introduced, some dependencies may be reported as missing. In this case, you need to install these dependencies in the test project for debugging (not required in the production environment), such as
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()
]
});
Then, use the vite
command to start our project (don't forget to introduce the test file App.vue
in TestMd.vue
), you can see the following renderings! (As shown below)
Configure hot reload
At this time, our plugin still lacks a hot reload function. If this function is not configured, modifying the md
file cannot trigger hot reload, and the project needs to be restarted every time.
We need to monitor our md
type file in the handleHotUpdate
hook function of the plugin, and then hot reload the vue
file that depends on the md
file.
Before that, we need to store the transform
file that introduced the md
file in the traversal loop of vue
.
Create a map
at the top of the plugin for storing dependencies, implemented as follows
const mdRelationMap = new Map<string, string>();
Then store the dependencies in transform
.
mdList?.forEach(md => {
//...
// 根据当前 vue 文件的目录和引入的 markdown 文件相对路径,拼接出 md 文件的绝对路径
const mdFilePath = path.resolve(fileDir, fileRelativePath);
// 记录引入当前 md 文件的 vue 文件 id
mdRelationMap.set(mdFilePath, id);
});
Then, we configure a new hot reload hook - handleHotUpdate
, and the code is implemented as follows:
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]
},
At this point, we modify our md file, and we can see that the page is updated in real time! (As shown below)
By the way, the content of the documents processed byhandleHotUpdate
is very small. Theserver.moduleGraph.getModulesByFile
ofAPI
is still found in the code snippets invite issue
. If you find relevant document resources, please share them with me, thank you.
At this point, our plugin development work is complete.
Publish plugin
In the above steps, we all use the local debugging mode, and it will be more troublesome to share such packages.
Next, we build our package and upload it to npm
for everyone to install and experience.
We add the following lines to 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"
},
Then, don't forget to install rimraf
, run-s
, esbuild
related dependencies. After installing the dependencies, we run npm run build
, and we can see that our code is compiled into the dist
directory.
When everything is ready, we can publish our package using the npm publish
command. (As shown below)
Then, we can replace the dependencies in vue.config.ts
with our built version, as follows:
// 由于我本地网络问题,我这个包传不上去,这里我直接引入本地包,和引用线上 npm 包是同理的
import markdown from './plugin-markdown';
Then we run the project and successfully parse markdown
file! (As shown below)
summary
At this point, our tutorial is over.
If you want to better grasp the development of vite
plug-ins, you still need to have a clear understanding of the roles and responsibilities of the following lifecycle hooks.
field | illustrate | belong |
---|---|---|
name | plugin name | vite and rollup shared |
handleHotUpdate | Perform custom HMR (Hot Module Replacement) update processing | Exclusive vite |
config | Called before parsing the Vite configuration. The configuration can be customized and will be merged with the basic configuration of vite | Exclusive vite |
configResolved | Called after parsing the Vite configuration. You can read the configuration of vite and perform some operations | vite Exclusive |
configureServer | is a hook for configuring the development server. The most common use case is adding custom middleware to an internal connect application. | vite Exclusive |
transformIndexHtml | Dedicated hook for converting index.html . | Exclusive vite |
options | Called when the vite (local) service starts before collecting the rollup configuration, which can be merged with the rollup configuration | vite and rollup shared |
buildStart | In the rollup build, the vite (local) service is called when the service starts, in this function you can access the configuration of rollup | vite and rollup shared |
resolveId | Called when parsing a module, you can return a special resolveId to specify a import statement to load a specific module | vite and rollup shared |
load | Called when parsing a module, can return a code block to specify a import statement to load a specific module | vite and rollup shared |
transform | Called when parsing the module, converts the source code, and outputs the converted result, similar to webpack of loader | vite and rollup shared |
buildEnd | Called before rollup output file to directory before vite local service shuts down | vite and rollup shared |
closeBundle | Before vite local service shuts down, call before rollup output file to directory | vite and rollup shared |
If you find any better articles or documents with more detailed introduction to these hook functions, please share them.
At the position of this article, a total of 6 issues of the Vite
series of articles have also come to a successful conclusion, thank you for your support.
one last thing
If you have seen this, I hope you will give a like and go~
Your likes are the greatest encouragement to the author, and can also allow more people to see this article!
If you think this article is helpful to you, please help to light up star
on github to encourage it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。