preface
Last article Vite development practice – project construction we talked about the Vite project environment construction, mainly to supplement the Vite scaffolding construction project defects. The specific development part of the official has given us a good experience and examples, some of the more special uses such as glob directory scanning, static resource import, it is recommended to directly read the official documents. This article will focus on how to write plug-ins during development to improve the development experience.
Plugin mechanism in Vite
Here’s an official quote:
The Vite plug-in extends the well-designed Rollup interface with some configuration items unique to Vite. Therefore, you only need to write a Vite plug-in to work in both development and production environments.
The vite plugin is based on the rollup extension. The plugin mechanism can be used to extend the application. Here is a brief introduction to the plugin mechanism in Vite.
The plug-in configuration
Using plug-ins in Vite is as simple as declaring them in the plugins option in vite.config.ts.
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default defineConfig({
plugins: [vitePlugin(), rollupPlugin()]
})
Copy the code
Each Plugin function needs to return a Plugin object with the name field, which can contain some plug-in hooks that Vite will call at execution time.
And false values for plug-ins are automatically ignored by Vite, so optional plug-ins don’t need to filter the set of available plug-ins as webPack does.
The plugin hooks
We discussed the concept of plug-in hooks in Vite. Now let’s talk about what hooks are available in Vite.
Rollup compatible hooks
Rollup () is not compatible with all of vite’s rollup hooks. Only the following hooks are called, so it can be assumed that vite’s development server only calls rollup.rollup() and not bundle.generate().
-
The following hooks are called when the server starts:
-
Options: Replaces or manipulates the option object passed to rollup. Nothing is replaced when null is returned. And this is the only hook that doesn’t have access to most plug-in context utility functionality because it runs before rollup is fully configured.
Note: If we only want to access the option object passed to rollup, we recommend using buildStart below.
Type definition:
interface PluginHooks { options: ( this: MinimalPluginContext, options: InputOptions ) = > Promise<InputOptions | null | undefined> | InputOptions | null | undefined; } Copy the code
-
BuildStart: called at every rollup build. This is the recommended hook to use when you need to access options passed to rollup, because it takes into account the conversion of all option hooks and also contains correct defaults for unset options.
Type definition:
interface PluginHooks { buildStart: (this: PluginContext, options: NormalizedInputOptions) = > Promise<void> | void; } Copy the code
-
-
The following hooks are called on each incoming module request:
-
ResolveId: can be used to define custom ID path resolvers, such as./foo.js in import foo from ‘./foo.js’, generally used to locate third party dependencies.
Type definition:
interface Plugin { Rollup resolveId is extended to add SSR flagresolveId? (this: PluginContext, source: string.importer: string | undefined.options:{ custom? : CustomPluginOptions; }, ssr? :boolean) :Promise<ResolveIdResult> | ResolveIdResult; } Copy the code
-
Load: Can be used to define a custom module parsing loader, for example, can directly return a converted AST.
Type definition:
interface Plugin { Rollup resolveId is extended to add SSR flagload? (this: PluginContext, id: string, ssr? :boolean) :Promise<LoadResult> | LoadResult; } Copy the code
-
Transform can also return the AST, but at this point the code of the path file is already in hand, so it is usually used to transform the loaded module. For example, markdown parsing is based on this hook.
Type definition:
interface Plugin { Rollup resolveId is extended to add SSR flagtransform? (this: TransformPluginContext, code: string.id: string, ssr? :boolean) :Promise<TransformResult_2> | TransformResult_2; } Copy the code
-
-
The following hooks are called when the server is shut down:
-
BuildEnd: called when the build is complete to get an error message that the build failed.
Type definition:
interface PluginHooks { buildEnd: (this: PluginContext, err? :Error) = > Promise<void> | void; } Copy the code
-
CloseBundle: Any external services that may be running can be cleaned up during this period.
Type definition:
interface PluginHooks { closeBundle: (this: PluginContext) = > Promise<void> | void; } Copy the code
-
Vite unique hooks
Vite’s unique hooks are ignored by Rollup
-
Config: called before parsing the vite configuration, where you can extend or modify the original user definition directly.
Type definition:
interfacePlugin { config? :(config: UserConfig, env: ConfigEnv) = > UserConfig | null | void | Promise<UserConfig | null | void>; } Copy the code
-
ConfigResolved: Called after the Vite configuration is resolved. Use this hook to read and store the final parsed configuration.
Type definition:
interfacePlugin { configResolved? :(config: ResolvedConfig) = > void | Promise<void>; } Copy the code
-
ConfigureServer: Hook used to configure the development server, access the development server instance from the hook, and can be used to add custom middleware to intercept requests, such as the mock plug-in based on this hook extension.
Type definition:
export declare type ServerHook = (server: ViteDevServer) = > (() = > void) | void | Promise< (() = > void) | void>; interfacePlugin { configureServer? : ServerHook; }Copy the code
-
TransformIndexHtml: Special hook for converting index.html. Type definition:
export declare type IndexHtmlTransformHook = (html: string, ctx: IndexHtmlTransformContext) = > IndexHtmlTransformResult | void | Promise<IndexHtmlTransformResult | void>; interfacePlugin { transformIndexHtml? : IndexHtmlTransform; }Copy the code
-
HandleHotUpdate: Can be used to customize HMR update handling.
interfacePlugin { handleHotUpdate? (ctx: HmrContext):Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>; } Copy the code
The execution order of the hooks
The hook execution of the plugin is basically the same as rollup, with vite’s unique plugin hooks interspersed between them.
The general order is as follows:
config
configResolved
options
configureServer
: Note that this is at initialization time, so it can be stored beforehandconfigResolved
The configuration of such hooks is then used here.buildStart
transformIndexHtml
load
resolveId
transform
buildEnd
: Triggered when packing.closeBundle
: Triggered when packing.
HandleHotUpdate is the hook that is triggered each time the HMR is triggered and does not depend very much on the internal order.
Order of plug-in execution
A Vite plugin can specify an additional Enforce attribute (similar to the Webpack loader) to adjust its application order. Enforce can be pre or Post. The parsed plug-ins are sorted in the following order:
- Alias
- with
enforce: 'pre'
User plugins for - Vite core plug-in
- User plug-in with no Enforce value
- Plugin for Vite build
- with
enforce: 'post'
User plugins for - Vite post-build plug-in (minimize, manifest, report)
Plug-in writing Practices
Now that we’ve talked about the plug-in mechanisms and specifications in Vite, let’s start writing two plug-ins. The local mock plug-in and markdown transform plug-in are examples.
Make a local Mock server plug-in
The main connection to vite from this example is to configure the development server, but it can also be used as an idea for mock data related plug-in development, so I will try to show the full code.
The local mock data in the front end can help us quickly test the front end interface, and when the real back-end interface is developed, it can be seamlessly connected. Here we mainly divided into three aspects to introduce how to write a local mock plug-in my-viet-plugin-mock.
- Service middleware in Vite
- Rules for writing mock files
- Listen and load local mock files
Service middleware in Vite
Vite’s development server is built using Node’s HTTP module and introduces the connect library as a middleware module extension. Vite also provides configureServer hooks to expose server instances.
The following is a specific usage example:
import { Plugin } from 'vite'
fcuntion myPlugin(): Plugin {
return {
name: 'configure-server'.// Server instance
configureServer(server) {
// Add middleware
server.middlewares.use((req, res) = > {
// Custom request handling...
})
// Matches the prefix
server.middlewares.use('/foo', (req, res, next) => {// Custom request handling...})}}}Copy the code
With custom middleware, development-mode requests can be intercepted and processed further.
Note: If the third parameter next is passed in, it must be called manually or it will not go to the next middleware.
Rules for writing mock files
To make it easier for mock data to communicate with the front end, we need to manually specify rules for users to follow. I limit this by using the mock path configuration of reductive forms and Typescript type definitions.
-
About pattern configuration: we need to manually specify a mock directory, our plugin will automatically traverse the entire directory and parse all the js | ts file directory, each file needs to have a default export routing configuration object, and you can also have an optional prefix export for unified path prefix.
Like this:
export const prefix = '/user' const userModule = { 'get /getUserInfo': async() = > {// coding}}export userModule Copy the code
-
Route type definition: just like the userModule above, we need to create a Routes type that limits this syntax:
import { Routes } from 'my-vite-plugin-mock' export const prefix = '/user' const userModule: Routes = { // Restrict key and value writing 'get /getUserInfo': async() = > {// coding}}export userModule Copy the code
Here’s the type definition:
// ./type.ts import http from 'http' import { Connect } from 'vite' // Parse the array type Item<T> = T extends Array<infer U> ? U : never // All requests supported export type Methods = [ 'all'.'get'.'post'.'put'.'delete'.'patch'.'options'.'head' ] export type MethodProps = Item<Methods> // Case compatible export type MethodsType = MethodProps | `${Uppercase<MethodProps>}` // Returns to the front-end in JSON format export interfaceHandlerResult { status? :numbermessage? :stringdata? :any } // Since the native HTTP module does not parse the context for us, we do a layer of parsing inside the plugin to get the parameters easily export interface HandlerContext { body: Record<string.any> query: Record<string.string | string[] | undefined> params: Record<string.string>}// Handle for the route export type RouteHandle = (ctx: HandlerContext, req: Connect.IncomingMessage, res: http.ServerResponse) = > Promise<HandlerResult | void> | HandlerResult | void // Route Map, the key must conform to the format such as 'get/XXX ' export type Routes = Record<`${MethodsType} The ${string}`, RouteHandle> // Note that the above types must be later than version 4.4 for typescript to work. If you cannot upgrade to version 4.4 for project reasons, change to the following: // export type Routes = Record<string, RouteHandle> Copy the code
Since template strings and associative index signatures are supported in the 4.4 update to typescript, they can be typed incorrectly in later versions.
Loading a mock file
The following two steps are additional. In general, we could import and merge configuration items manually, but we added them to make it easier to write mock files in the future.
There are two aspects to loading mock files, one is to load all mock files when the service starts, and the other is to load only updated files when there are file updates in the mock directory.
So I’m going to write two functions, one for file loading and the other for directory loading (essentially both call the function to load the file). When loading the file, I can use require dynamic access (but note that we can also reference ts files directly, so there is a layer of parsing). Re-execute require when the mock file changes (note that the cache of require is removed here).
But first, we need to add two types to the previous type definition file:
// ./type.ts
// ...
// The internal handler is not exposed to the user
export type MockRoutes = Record<
`${MethodsType} The ${string}`,
{
handler: RouteHandle
method: MethodsType
}
>
/ / reference from https://github.com/anncwb/vite-plugin-mock
// Node module processing capabilities, node itself is not defined to avoid use, but here we explicitly need to use
export interface NodeModuleWithCompile extends NodeModule {
_compile(code: string.filename: string) :any
}
Copy the code
Here are the full parsing functions for mock files:
// ./utils.ts
import * as fs from 'fs'
import { build } from 'esbuild'
import * as path from 'path'
import { MethodsType, MockRoutes, NodeModuleWithCompile, Routes } from './type'
export interface loadMockFilesOptions {
// Listen to the directory
dir: string | string[]
// Files to include or excludeinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}// Utility functions to match include and exclude
export function matchFiles({
file,
include,
exclude
}: {
file: stringinclude? :RegExp | ((filename: string) = >boolean) exclude? :RegExp | ((filename: string) = >boolean)}) :boolean {
if (
(exclude instanceof RegExp && exclude.test(file)) ||
(typeof exclude === 'function' && exclude(file))
) {
return false
}
if( include && ! ( (includeinstanceof RegExp && include.test(file)) ||
(typeof include === 'function' && include(file))
)
) {
return false
}
return true
}
// Load all files, which will run the first time the plug-in loads
export async function loadMockFiles({ dir, exclude, include }: loadMockFilesOptions) :Promise<MockRoutes | null> {
let mockRoutes: MockRoutes | null = null
// Determine the directories to load arrays and strings separately
if (Array.isArray(dir)) {
mockRoutes = (
await Promise.all(dir.map((d) = > loadDir({ dir: d, exclude, include })))
).reduce((prev, next) = > {
return{... prev, ... next } }, {}as MockRoutes)
} else {
mockRoutes = await loadDir({ dir, exclude, include })
}
return mockRoutes
}
// Load the directory
async function loadDir({
dir,
include,
exclude
}: Omit<loadMockFilesOptions, 'dir'> & { dir: string }) {
const mockRoutes: MockRoutes = {}
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir)
const childMockRoutesArr = await Promise.all(
files
.map((file) = > path.resolve(dir, file))
.filter((file) = > matchFiles({ include, exclude, file }))
.map((file) = > {
const currentPath = path.resolve(dir, file)
const stat = fs.statSync(currentPath)
if (stat.isDirectory()) {
// Load recursively
return loadDir({
include,
exclude,
dir: currentPath
})
} else {
return loadFile(currentPath)
}
})
)
// Merge all routes
childMockRoutesArr.forEach((childMockRoutes) = > {
Object.keys(childMockRoutes).forEach((key) = > {
mockRoutes[key as keyof MockRoutes] =
childMockRoutes[key as keyof MockRoutes]
})
})
}
return mockRoutes
}
// Load the file, also called when listening for file changes
export async function loadFile(filename: string) {
const mockRoutes: MockRoutes = {}
// Return empty routes for a directory
if (fs.statSync(filename).isDirectory()) {
return mockRoutes
}
// resolveModule is responsible for parsing modules
const { prefix: routePrefix, default: routes } = (await resolveModule(
filename
)) as{ prefix? :string
default: Routes
}
typeof routes === 'object'&& routes ! = =null &&
Object.keys(routes).forEach((routeKey) = > {
const [method, routePath] = routeKey.split(' ')
mockRoutes[path.join(routePrefix || ' ', routePath) as keyof MockRoutes] =
{
method: method as MethodsType,
handler: routes[routeKey as keyof Routes]
}
})
return mockRoutes
}
// Parse the file
async function resolveModule(filename: string) :Promise<any> {
// If it is a TS file, use esbuild to quickly package to get js
if (filename.endsWith('.ts')) {
const res = await build({
entryPoints: [filename],
write: false.platform: 'node'.bundle: true.format: 'cjs'.target: 'es2015'
})
const { text } = res.outputFiles[0]
// Change the node resolution rules
return loadConfigFromBundledFile(filename, text)
}
// Be sure to remove the cache and let node reload
delete require.cache[filename]
return require(filename)
}
async function loadConfigFromBundledFile(
filename: string,
bundle: string
) :Promise<any> {
const extension = path.extname(filename)
const defaultLoader = require.extensions[extension]
require.extensions[extension] = (module: NodeModule, fName: string) = > {
if(filename === fName) { ; (module as NodeModuleWithCompile)._compile(bundle, filename)
} else{ defaultLoader? . (module, fName)
}
}
// Delete the cache
delete require.cache[filename]
const moduleValue = require(filename)
// Revert to the original parsing rule
if (defaultLoader) {
require.extensions[extension] = defaultLoader
}
return moduleValue
}
Copy the code
Note: You may have noticed that we preprocessed the TS file using esbuild and then changed require. Extensions lodaer to address the introduction of TS.
The require.extensions have been deprecated since 10.6, however, because registration slows parsing of the entire application, but here since we are only using them in development mode, and require.extensions are deprecated but will probably never be removed officially, So for now, it can be used as a parsing method.
Listen for mock file changes
We use Chokidar to listen for changes to the specified folder and reload the mock file each time it changes.
// ./index.ts
import * as chokidar from 'chokidar'
import type { WatchOptions } from 'chokidar'
import { Plugin } from 'vite'
export * from './type'
import { loadMockFiles, loadFile, matchFiles } from './utils'
export interface viteMockPluginOptions extends WatchOptions {
dir: string[] | string
/ * * *@description: Path prefix *@default: /mock
*/mockPrefix? :stringinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}function viteMockPlugin(options: viteMockPluginOptions) :Plugin {
const { dir } = options
return {
name: 'my-vite-plugin-mock'.enforce: 'pre'.apply: 'serve'.async configureServer() {
// Initialize all mock files
let mockFiles = await loadMockFiles(options)
// Listen to the directory
chokidar.watch(dir, { ignoreInitial: true. options }).on('all'.(action, file) = > { // Listen for all actions, add files, modify files, etc
// file indicates the name of the changed file
if (
// The matching condition is also checked here
matchFiles({
include: options.include,
exclude: options.exclude,
file
})
) {
// Cache loadingmockFiles = { ... mockFiles, ... (await loadFile(file)) }
}
})
// http handler}}}export { viteMockPlugin }
export default viteMockPlugin
Copy the code
Parsing front-end requests
We learned how to load mock files and store all the routes as objects. Now we just need to parse the front-end requests and match the matching requests to the routes to complete our local mock functionality.
// ./index.ts
import * as chokidar from 'chokidar'
import type { WatchOptions } from 'chokidar'
import { parse } from 'querystring'
import { pathToRegexp, match } from 'path-to-regexp'
import { Plugin } from 'vite'
export * from './type'
import { loadFile, loadMockFiles, matchFiles } from './utils'
export interface viteMockPluginOptions extends WatchOptions {
dir: string[] | string
/ * * *@description: Path prefix *@default: /mock
*/mockPrefix? :stringinclude? :RegExp | ((filename: string) = > boolean) exclude? :RegExp | ((filename: string) = > boolean)}function safeJsonParse<T extends Record<string | number | symbol.any> > (
jsonStr: string,
defaultValue: T
) {
try {
return JSON.parse(jsonStr)
} catch (err) {
return defaultValue
}
}
// Parse the body data
function parseBody(
req: Connect.IncomingMessage
) :Promise<Record<string.any>> {
return new Promise((resolve) = > {
let body = ' '
req.on('data'.function (chunk) {
body += chunk
})
req.on('end'.function () {
resolve(safeJsonParse(body, {}))
})
})
}
function viteMockPlugin(options: viteMockPluginOptions) :Plugin {
const { dir } = options
return {
name: 'my-vite-plugin-mock'.enforce: 'pre'.apply: 'serve'.async configureServer({ middlewares }) {
let mockFiles = await loadMockFiles(options)
chokidar
.watch(dir, { ignoreInitial: true. options }) .on('all'.async (_, file) => {
if (
matchFiles({
include: options.include,
exclude: options.exclude,
file
})
) {
// Cache loadingmockFiles = { ... mockFiles, ... (await loadFile(file)) }
}
})
middlewares.use(options.mockPrefix || '/mock'.async (req, res) => {
if (mockFiles) {
const[url, search] = req.url! .split('? ')
// Traverses all routing interfaces
for (const [pathname, { handler, method }] of Object.entries(
mockFiles
)) {
// Match the route
if( pathToRegexp(pathname).test(url) && req.method? .toLowerCase() === method.toLowerCase() ) {// Parse query, params, body
// eslint-disable-next-line no-await-in-loop
const body = await parseBody(req)
const query = parse(search)
const matched = match(pathname)(url)
// eslint-disable-next-line no-await-in-loop
const result = await handler(
{
body,
query,
params:
(matched && (matched.params as Record<string.string>)) ||
{}
},
req,
res
)
// If the user does not use res.end() in the mock function, return the data directly
if(! res.headersSent && result) { res.setHeader('Content-Type'.'application/json')
res.statusMessage = result.message || 'ok'
res.statusCode = result.status || 200
res.end(
JSON.stringify({
message: 'ok'.status: 200. result }) ) }return}}// Return 404 if neither match
res.setHeader('Content-Type'.'application/json')
res.statusMessage = '404 Not Found'
res.statusCode = 404
res.end(
JSON.stringify({
message: '404 Not Found'.status: 404}))}})}}}export { viteMockPlugin }
export default viteMockPlugin
Copy the code
At this point, our mock plug-in is finally complete. There are a lot of codes, so I won’t put all of them out here. All the codes have been put on Github, and students who need them can help themselves.
Make a Markdown conversion plug-in
This plugin does the following:
- Server (here the server is in development mode
vite
Server and packaging timerollup
Parsers) parsemd
File that intercepts to generate displayablehtml
String and directory. - The React stack is compatible with the Vue component import and lazy loading.
- Ability to customize styles, can use UI component uniform styles.
Convert markDown to React components
To convert markdown to the React component, we first need to convert it to an HTML string. I used marked, a mature third-party library.
Once you’ve got the HTML string, how do you render it as a React component? React render component template (art-template) React render component template (art-template)
Here is an.art template file:
Import React, {useEffect} from 'React' // custom import {{if imports}} {{imports}} {{if}} // default display JSX, Support server to pass in component const Content = <>{{content}}</> // Original HTML const nativeContent = {{nativeContent}} // whether to use the original HTML const Native = {{isNative}} // directory const toc = {{toc}} const MarkdownComponent = ({onLoad, ClassName}) => {useEffect(() => {// provide an external function to get the content onLoad? .({ toc, html: nativeContent }) }, []) return ( <div className={`${className ? className + ' ' : ''}vite-markdown`} dangerouslySetInnerHTML={native ? { __html: nativeContent } : undefined} > {native ? Null: content} </div>)} export {toc, nativeContent as HTML} export default MarkdownComponent expose directory and original HTML contentCopy the code
Render template:
// ./utils.ts
import * as marked from 'marked'
import * as fs from 'fs'
import * as path from 'path'
import { render } from 'art-template'
/ / directory
export interface TocProps {
level: number
text: string
slug: string
}
// Render props
export interface RenderProps {
content: string
isNative: boolean
nativeContent: string
toc: stringimports? :string
}
export interface MarkedRenderOptions {
// Imports at the top of the templateimports? :string
// marked configuration itemmarkedOptions? : marked.MarkedOptions// Whether it is native HTMLnative? :boolean
}
const MarkdownComponent = fs
.readFileSync(path.resolve(__dirname, '.. /templates/markdown-component.art'))
.toString()
// Write a conversion function
export function markdown2jsx(markdown: string, options: MarkedRenderOptions) {
letrenderer = options.markedOptions? .rendererif(! renderer) { renderer =new marked.Renderer()
}
// Since we rely on the header to generate the directory during rendering, we need to get the original header
const headingRender = renderer.heading
/ / directory
const toc: TocProps[] = []
renderer.heading = function (text, level, raw, slugger) {
return headingRender.call(this, text, level, raw, { ... slugger,slug(. args) {
// Unique identifier to use when getting directory anchor points
constres = slugger.slug(... args) toc.push({ level, text,slug: res
})
return res
}
})
}
// Converted HTML
const content = marked(markdown, {
xhtml: true. options.markedOptions, renderer })const renderOptions: RenderProps = {
isNative: options.native || false.nativeContent: JSON.stringify(content) || ' '.content: options.native ? ' ' : content,
imports: options.imports,
toc: JSON.stringify(toc)
}
return {
content: render(MarkdownComponent, renderOptions, {
// Do not encode, native output
escape: false}}})Copy the code
The React component is available on the server, but the browser can’t parse JSX files. The React component is packaged into js strings by hand, which means that it needs to be packaged at runtime. In order to improve the packaging speed, we use the esbuild of Vite to package it. Because in production there is still a layer of rollup packaging).
import { transform } from 'esbuild'
import { markdown2jsx } from './utils'
// md => tsx
const { content } = markdown2jsx(code, options)
// tsx => js
transform(content, {
loader: 'tsx'.target: 'esnext'.treeShaking: true
}).then({ code } => console.log(code))
Copy the code
Intercepting MD Files
As mentioned earlier, to parse a loaded file, we usually use the TranForm hook, which provides the content and filename of the source file. We can easily do custom parsing based on both:
// ./index.ts
import { Plugin } from 'vite'
import { SourceDescription } from 'rollup'
import { transform } from 'esbuild'
import { markdown2jsx, MarkedRenderOptions } from './utils'
/ / match markdown
const mdRegex = /\.md$/
export type mdPluginOptions = MarkedRenderOptions
function mdPlugin(options: mdPluginOptions = {}) :Plugin {
return {
name: 'vite-jsx-md-plugin'.enforce: 'pre'.// The react HMR plugin provides an additional layer of transform to the result, providing the HMR function in development mode
configResolved({ plugins }) {
const reactRefresh = plugins.find(
(plugin) = > plugin.name === 'react-refresh'
)
this.transform = async function (code, id, ssr) {
if (mdRegex.test(id)) {
// md => tsx
const { content } = markdown2jsx(code, options)
// tsx => js
const { code: transformedCode } = await transform(content, {
loader: 'tsx'.target: 'esnext'.treeShaking: true
})
// Add another layer of HMR code
// This value is null for development mode only
const sourceDescription = (awaitreactRefresh? .transform? .call(this,
transformedCode,
`${id}.tsx`,
ssr
)) as SourceDescription
return (
sourceDescription || {
code: transformedCode,
map: { mappings: ' '}})}}as Plugin['transform']}}}export default mdPlugin
Copy the code
After some simple processing, we have completed a plug-in with loader functionality, which can help us parse the MD files that the application does not recognize into React components for use in the application. Also, since we generated the React component from a template, we can easily make adjustments to the component styles and so on:
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import viteMdPlugin from '@col0ring/vite-plugin-md'
import marked from 'marked'
const renderer = new marked.Renderer()
// We can replace the A tag directly here with the Link component of the React-Router as shown below
renderer.link = (href, title, text) = > {
return `<Link to="${href}" title="${title}">${text}</Link>`
}
export default defineConfig({
plugins: [
reactRefresh(),
viteMdPlugin({
markedOptions: {
renderer
},
// Use the a tag above, which is introduced here
imports: ` import { Link } from 'react-router-dom' `})]})Copy the code
The plugin code is also posted on Github, you can check it out if you need it.
conclusion
This article starts with the vite plug-in mechanism and introduces the configuration and execution hooks of the Vite plug-in. In the follow-up, I spent a lot of space on plug-in practice, and developed two plug-ins for different purposes from zero to one (one is mainly used to improve the development mode, and one provides loader parsing capability). Of course, since the article is actually the logic of the plug-in itself, and only a small part of the use of vite hooks, so it can also be regarded as a tutorial on the writing of these two special plug-ins.
For more in-depth use of the Vite plugin API, check out the other great plugin repositories, and check out the community plugin set awesome- Vite.
One more thing to mention here, because the current vite ecosystem is not and very perfect, native Vite and WebPack still have a certain gap in the solution of some customization requirements, in many cases for a single project exclusive plug-in development even included in the project development part. This is why I include plug-in development as a Vite development practice.
reference
- Rollup plug-in documentation
- Vite official document
- vite-plugin-mock
- vite-plugin-mdx