Vite plugin mechanism
Naming conventions
If the plug-in does not use Vite specific hooks, it can be implemented as a compatible Rollup plug-in. The Rollup plug-in name convention is recommended.
- The Rollup plug-in should have a strip
rollup-plugin-
A name with a prefix and clear meaning. - Included in package.json
rollup-plugin
和vite-plugin
The keyword.
Thus, plug-ins can also be used for pure Rollup or WMR-based projects.
For Vite specific plug-ins:
- The Vite plugin should have a strap
vite-plugin-
A name with a prefix and clear meaning. - Included in package.json
vite-plugin
The keyword. - Add a section to the plug-in documentation explaining why this plug-in is a Vite-specific plug-in (for example, this plug-in uses vite-specific plug-in hooks).
If your plugin only works with a specific framework, its name should follow the following prefix format:
vite-plugin-vue-
Prefix as Vue plug-invite-plugin-react-
Prefix as React pluginvite-plugin-svelte-
Prefix as Svelte plugin
The plug-in configuration
The user adds plug-ins to the project’s devDependencies and configures them using the plugins option in the form of an array.
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default {
plugins: [vitePlugin(), rollupPlugin()]
}
Copy the code
Plug-ins with false values are ignored and can be used to easily enable or disable plug-ins.
Plugins can also accept multiple plug-ins as a default for a single element. This is useful for complex features such as framework integration that are implemented using multiple plug-ins. The array will be flatten internally.
// Frame plugin
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'
export default function framework(config) {
return [frameworkRefresh(config), frameworkDevTools(config)]
}
Copy the code
// vite.config.js
import framework from 'vite-plugin-framework'
export default {
plugins: [framework()]
}
Copy the code
A simple example
Import a virtual file
export default function myPlugin() {
const virtualFileId = '@my-virtual-file'
return {
name: 'my-plugin'.// Yes, will be displayed in warning and error
resolveId(id) {
if (id === virtualFileId) {
return virtualFileId
}
},
load(id) {
if (id === virtualFileId) {
return `export const msg = "from virtual file"` // Replace the file contents}}}}Copy the code
This makes it possible to introduce these files in JavaScript:
import { msg } from '@my-virtual-file'
console.log(msg) // from virtual file
Copy the code
Convert custom file types
const fileRegex = /\.(my-file-ext)$/
export default function myPlugin() {
return {
name: 'transform-file'.transform(src, id) {
if (fileRegex.test(id)) {
return {
code: compileFileToJS(src), // File type conversion
map: null // Source map will be provided if feasible
}
}
}
}
}
Copy the code
General hook
During development, the Vite development server creates a plug-in container to invoke the Rollup build hook, just like Rollup.
The following hooks are called when the server starts
options
buildStart
options
- Type: ( inputOptions ) => options
The first hook that rollup packages, used to replace or manipulate rollup configuration until rollup is fully configured, returns null, indicating that nothing will be done. If it is simply to read the rollup configuration file, it can be obtained in the buildStart hook. Also, it is the only hook in rollup that cannot retrieve the Plugin context, and this hook should be rarely used.
buildStart
- Type:
(options: InputOptions) => void
- Previous Hook:
options
- Next Hook:
resolveId
This hook follows the Options hook and is fired at rollup build time to get rollup configuration
The following hooks are called on each module request
resolveId
- Type:
(importee, importer) => (id|Promise)
- Previous Hook: buildStart, moduleParsed, resolveDynamicImport.
- Next Hook:
load
If buildStart, moduleParsed, and resolveDynamicImport are configured, the resolveId hook will fire after the first three hooks. It should be noted that the moduleParsed and resolveDynamicImport hooks are not used in serve(development mode) rollup. Resolve triggers the resolveId hook whenever a plugin triggers this.emitFile or this.resolve manually resolves an ID; Returns NULL, indicating that the default parsing method is used. Returns false to indicate that Importee is an external module and will not be packaged into the bundle.
async resolveId(importee,importer) {
// Importee indicates the chunk itself, the importer indicates the chunk that importee is introduced
Importee = @my-virtual-file, importer = the absolute path to app.tsx
if(! importer) {// We need to skip this plugin to avoid an infinite loop
const resolution = await this.resolve(importee, undefined, { skipSelf: true });
// If it cannot be resolved, return `null` so that Rollup displays an error
if(! resolution)return null;
return `${resolution.id}? entry-proxy`;
}
return null;
},
load(id) {
if (id.endsWith('? entry-proxy')) {
// get resolution.id
const importee = id.slice(0, -'? entry-proxy'.length);
// Note that this will throw if there is no default export
return `export {default} from '${importee}'; `;
}
return null;
}
Copy the code
load
- Type:
(id: string) => string | null | {code: string, map? : string | SourceMap, ast? : ESTree.Program, moduleSideEffects? : boolean | "no-treeshake" | null, syntheticNamedExports? : boolean | string | null, meta? : {[plugin: string]: any} | null}
- Previous Hook:
resolveId
orresolveDynamicImport
- Next Hook:
transform
Custom loader to return custom file contents; If null is returned, the default content of the chunk parsed by the system is returned. Load can return many types of content, including sourceMap and AST. For details, see Load
transform
- Type:
(code: string, id: string) => string | null | {code? : string, map? : string | SourceMap, ast? : ESTree.Program, moduleSideEffects? : boolean | "no-treeshake" | null, syntheticNamedExports? : boolean | string | null, meta? : {[plugin: string]: any} | null}
- Previous Hook:
load
- NextHook:
moduleParsed
once the file has been processed and parsed.
It is used to convert the chunk after load to avoid extra compilation overhead
The following hooks are called when the server is shut down
buildEnd
closeBundle
buildEnd
- Type:
(error? : Error) => void
- Previous Hook:
moduleParsed
,resolveId
orresolveDynamicImport
. - Next Hook:
outputOptions
in the output generation phase as this is the last hook of the build phase.
This can be triggered when bunding finished, before writing a file, or it can return a Promise. This hook is also triggered if an error is reported during build
Vite unique hook
config
- Type:
(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void
Called before parsing the Vite configuration. The hook receives the original user configuration (which is merged with the configuration file as specified by the command line option) and a variable that describes the configuration environment, including the mode and command being used. It can return a partial configuration object that will be deeply merged into an existing configuration, or simply change the configuration if the default merge does not achieve the desired result. Call anything else inside the Config hook
// Return to partial configuration (recommended)
const partialConfigPlugin = () = > ({
name: 'return-partial'.config: () = > ({
alias: {
foo: 'bar'}})})// Change configuration directly (should only be used if merge does not work)
const mutateConfigPlugin = () = > ({
name: 'mutate-config'.config(config, { command }) {
if (command === 'build') {
config.root = __dirname
}
}
})
Copy the code
configResolved
- Type:
(config: ResolvedConfig) => void | Promise<void>
Called after the Vite configuration is parsed. Use this hook to read and store the final parsed configuration. It is also useful when the plug-in needs to do something different depending on the command being run.
const exmaplePlugin = () = > {
let config
return {
name: 'read-config'.configResolved(resolvedConfig) {
// Stores the final parsed configuration
config = resolvedConfig
},
// Use the configuration stored with other hooks
transform(code, id) {
if (config.command === 'serve') {
// Serve: a plug-in used to start the development server
} else {
// build: calls the Rollup plug-in}}}}Copy the code
configureServer
- Type:
(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>
- ViteDevServer Interface: ViteDevServer
Is the hook used to configure the development server. The most common use case is to add custom middleware to an internal CONNECT application:
const myPlugin = () = > ({
name: 'configure-server'.configureServer(server) {
server.middlewares.use((req, res, next) = > {
// Custom request handling...})}})Copy the code
Inject the back-end middleware
The configureServer hook will be called before the internal middleware is installed, so the custom middleware will run before the internal middleware by default. If you want to inject a middleware running behind the internal middleware, you can return a function from configureServer that will be called after the internal middleware is installed:
const myPlugin = () = > ({
name: 'configure-server'.configureServer(server) {
// Returns an internal middleware after installation
// The post-hook to be called
return () = > {
server.middlewares.use((req, res, next) = > {
// Custom request handling...}}}})Copy the code
Note that configureServer is not called when running the production version, so other hooks need to be careful not to miss it.
transformIndexHtml
- Type:
IndexHtmlTransformHook | { enforce? : 'pre' | 'post' transform: IndexHtmlTransformHook }
Convert the special hook for index.html. The hook receives the current HTML string and the conversion context. The context exposes the ViteDevServer instance during development and the Rollup output package during build.
This hook can be asynchronous and can return one of the following:
- Converted HTML string
- An array of tag descriptor objects injected into existing HTML (
{ tag, attrs, children }
). Each tag can also specify where it should be injected (default is<head>
Before) - A contain
{ html, tags }
The object of
const htmlPlugin = () = > {
return {
name: 'html-transform'.transformIndexHtml(html) {
return html.replace(
/(.*?) <\/title>/ .`Title replaced! `)}}}Copy the code
handleHotUpdate
- Type:
(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
interface HmrContext {
file: string
timestamp: number
modules: Array<ModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}
Copy the code
modules
Is an array of modules affected by the change file. It is an array because a single file may map to multiple service modules (such as a Vue single-file component).read
This is an asynchronous read function that returns the contents of the file. This is done because on some systems, the callback function for file changes may fire too quickly before the editor has finished updating the file, andfs.readFile
Empty content is returned. The incomingread
Functions regulate this behavior.
Hooks can be selected:
- Filter and narrow the list of affected modules to make HMR more accurate.
- Returns an empty array and performs full custom HMR processing by sending custom events to the client:
handleHotUpdate({ server }) {
server.ws.send({
type: 'custom'.event: 'special-update'.data: {}})return[]}Copy the code
Client code should register the corresponding handler using the HMR API (this should be injected by the transform hook of the same plug-in) :
if (import.meta.hot) {
import.meta.hot.on('special-update'.(data) = > {
// Perform custom updates})}Copy the code
Summary of hook execution order
export default function myExample () {
// The plugin object is returned
return {
name: 'hooks-order'.// Initialize hooks, only once
options(opts) {
console.log('options');
},
buildStart() {
console.log('buildStart');
},
// Vite has unique hooks
config(config) {
console.log('config');
return{}},configResolved(resolvedCofnig) {
console.log('configResolved');
},
configureServer(server) {
console.log('configureServer');
// server.app.use((req, res, next) => {
// // custom handle request...
// })
},
transformIndexHtml(html) {
console.log('transformIndexHtml');
return html
// return html.replace(
// /(.*?) <\/title>/,
// `Title replaced! `
// )
},
// Generic hook
resolveId(source) {
console.log(resolveId)
if (source === 'virtual-module') {
console.log('resolvedId');
return source;
}
return null;
},
load(id) {
console.log('load');
if (id === 'virtual-module') {
return 'export default "This is virtual!" ';
}
return null;
},
transform(code, id) {
console.log('transform');
if (id === 'virtual-module') {}return code
},
};
}
Copy the code
The execution result
config
configResolved
options
configureServer
buildStart
transformIndexHtml
load
load
transform
transform
Copy the code
Hook execution order
Order of plug-in execution
Similar to WebPack, this is controlled by the Enforce field
- Alias handling Alias
- User plug-in Settings
enforce: 'pre'
- Vite core plug-in
- The user plug-in is not configured
enforce
- Vite builds plug-ins
- User plug-in Settings
enforce: 'post'
- Vite build post-plugins (Minify, Manifest, Reporting)
Since vue3+ Vite will be used in the subsequent architecture upgrade of the company, considering that some wheels of Vite may not be perfect for the time being, it is not ruled out that we need to write vite plug-in for the subsequent work, so I will make a summary here, and I hope to correct any mistakes.
Reference links:
Juejin. Cn/post / 695021…
Cn. Vitejs. Dev/guide/API – p…
Rollupjs.org/guide/en/#p…