6
头图

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:

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.

image

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 of vite supports ts , so here we directly use typescript 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 of 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

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:

  1. matches all g-markdown tags in the vue file
  2. Load the corresponding markdown file content and convert the markdown text to html text that can be recognized by the browser
  3. Replace the markdown tag with html text, import the style 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)

image

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)

image

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)

image

By the way, the content of the documents processed by handleHotUpdate is very small. The server.moduleGraph.getModulesByFile of API is still found in the code snippets in vite 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)

image

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)

image

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.

fieldillustratebelong
nameplugin namevite and rollup shared
handleHotUpdatePerform custom HMR (Hot Module Replacement) update processingExclusive vite
configCalled before parsing the Vite configuration. The configuration can be customized and will be merged with the basic configuration of viteExclusive vite
configResolvedCalled after parsing the Vite configuration. You can read the configuration of vite and perform some operationsvite Exclusive
configureServeris a hook for configuring the development server. The most common use case is adding custom middleware to an internal connect application.vite Exclusive
transformIndexHtmlDedicated hook for converting index.html .Exclusive vite
optionsCalled when the vite (local) service starts before collecting the rollup configuration, which can be merged with the rollup configurationvite and rollup shared
buildStartIn the rollup build, the vite (local) service is called when the service starts, in this function you can access the configuration of rollupvite and rollup shared
resolveIdCalled when parsing a module, you can return a special resolveId to specify a import statement to load a specific modulevite and rollup shared
loadCalled when parsing a module, can return a code block to specify a import statement to load a specific modulevite and rollup shared
transformCalled when parsing the module, converts the source code, and outputs the converted result, similar to webpack of loadervite and rollup shared
buildEndCalled before rollup output file to directory before vite local service shuts downvite and rollup shared
closeBundleBefore vite local service shuts down, call before rollup output file to directoryvite 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!


晒兜斯
1.8k 声望534 粉丝