乐趣区

关于vite:Vite-实战手把手教你写一个-Vite-插件

哈喽,很快乐你能点开这篇博客,本博客是针对 Vite 的体验系列文章之实战篇,认真看完后置信你也能如法炮制写一个属于本人的 vite 插件。

Vite 是一种新型的前端构建工具,可能显著晋升前端开发体验。

我将会从 0 到 1 实现一个 vite:markdown 插件,该插件能够读取我的项目目录中的 markdown 文件并解析成 html,最终渲染到页面中。

如果你还没有应用过 Vite,那么你能够看看我的前两篇文章,我也是刚体验没两天呢。(如下)

  • Vite + Vue3 初体验 —— Vite 篇
  • Vite + Vue3 初体验 —— Vue3 篇

本系列文件还对 Vite 源码进行了解读,往期文章能够看这里:

  • Vite 源码解读系列(图文联合)—— 本地开发服务器篇
  • Vite 源码解读系列(图文联合)—— 构建篇
  • Vite 源码解读系列(图文联合)—— 插件篇

实现思路

其实 vite 插件的实现思路就是 webpackloader + plugin,咱们这次要实现的 markdown 插件其实更像是 loader 的局部,然而也会利用到 vite 插件的一些钩子函数(比方热重载)。

我须要先筹备一个对 markdown 文件进行转换,转换成 html 的插件,这里我应用的是 markdown-it,这是一个很风行的 markdown 解析器。

其次,我须要辨认代码中的 markdown 标签,并读取标签中指定的 markdown 文件,这一步能够应用正则加上 nodefs 模块做到。

好,实现思路都理清了,咱们当初能够来实现这个插件了。

初始化插件目录

咱们应用 npm init 命令来初始化插件,插件名称命名为 @vitejs/plugin-markdown

为了不便调试,该插件目录我间接创立在我的 Vite Demo 我的项目 中。

本次插件实战的仓库地址为 @vitejs/plugin-markdown,感兴趣的同学也能够间接下载代码来看。

package.json 中,咱们先不必焦急设置入口文件,咱们能够先把咱们的性能实现。

创立测试文件

这里,咱们在测试项目中创立一个测试文件 TestMd.vueREADME.md,文件内容和最终成果如下图所示。

在创立好了测试文件后,咱们当初就要来钻研怎么实现了。

创立插件入口文件 —— index.ts

上面,咱们来创立插件入口文件 —— index.ts

vite 的插件反对 ts,所以这里咱们间接应用 typescript 来编写这个插件。

该文件的内容次要是蕴含了 nameenforcetransform 三个属性。

  • name: 插件名称;
  • enforce: 该插件在 plugin-vue 插件之前执行,这样就能够间接解析到原模板文件;
  • transform: 代码转译,这个函数的性能相似于 webpackloader
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 文本

接下来,咱们要分三步走:

  1. 匹配 vue 文件中的所有 g-markdown 标签
  2. 加载对应的 markdown 文件内容,将 markdown 文本转换为浏览器可辨认的 html 文本
  3. 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"
  },

而后,别忘了装置 rimrafrun-sesbuild 相干依赖,装置完依赖后,咱们运行 npm run build,就能够看到咱们的代码被编译到了 dist 目录中。

当所有都准备就绪后,咱们应用 npm publish 命令公布咱们的包就能够啦。(如下图)

而后,咱们能够将 vue.config.ts 中的依赖换成咱们构建后的版本,实现如下:

// 因为我本地网络问题,我这个包传不下来,这里我间接引入本地包,和援用线上 npm 包是同理的
import markdown from './plugin-markdown';

而后咱们运行我的项目,胜利解析 markdown 文件即可!(如下图)

小结

到这里,咱们本期教程就完结了。

想要更好的把握 vite 插件的开发,还是要对上面几个生命周期钩子的作用和职责有清晰的意识。

字段 阐明 所属
name 插件名称 viterollup 共享
handleHotUpdate 执行自定义 HMR(模块热替换)更新解决 vite 独享
config 在解析 Vite 配置前调用。能够自定义配置,会与 vite 根底配置进行合并 vite 独享
configResolved 在解析 Vite 配置后调用。能够读取 vite 的配置,进行一些操作 vite 独享
configureServer 是用于配置开发服务器的钩子。最常见的用例是在外部 connect 应用程序中增加自定义中间件。 vite 独享
transformIndexHtml 转换 index.html 的专用钩子。 vite 独享
options 在收集 rollup 配置前,vite(本地)服务启动时调用,能够和 rollup 配置进行合并 viterollup 共享
buildStart rollup 构建中,vite(本地)服务启动时调用,在这个函数中能够拜访 rollup 的配置 viterollup 共享
resolveId 在解析模块时调用,能够返回一个非凡的 resolveId 来指定某个 import 语句加载特定的模块 viterollup 共享
load 在解析模块时调用,能够返回代码块来指定某个 import 语句加载特定的模块 viterollup 共享
transform 在解析模块时调用,将源代码进行转换,输入转换后的后果,相似于 webpackloader viterollup 共享
buildEnd vite 本地服务敞开前,rollup 输入文件到目录前调用 viterollup 共享
closeBundle vite 本地服务敞开前,rollup 输入文件到目录前调用 viterollup 共享

如果大家发现有什么比拟好的文章或者文档对这些钩子函数有更具体的介绍,也欢送分享进去。

到这篇文章地位,总共 6 期的 Vite 系列文章也圆满画上了句号,谢谢大家的反对。

最初一件事

如果您曾经看到这里了,心愿您还是点个赞再走吧~

您的点赞是对作者的最大激励,也能够让更多人看到本篇文章!

如果感觉本文对您有帮忙,请帮忙在 github 上点亮 star 激励一下吧!

退出移动版