用于开发构建工具插件的聚合系统
已支持
用于开发构建工具插件的聚合系统
已支持
import { createUnplugin } from 'unplugin' export const unplugin = createUnplugin((options: UserOptions) => { return { name: 'my-first-unplugin', transformInclude (id) { return id.endsWith('.vue') }, transform (code) { return code.replace( /<template>/, `<template><div>Injected</div>` ) }, } }) export const vitePlugin = unplugin.vite export const rollupPlugin = unplugin.rollup export const webpackPlugin = unplugin.webpack
import { createUnplugin } from 'unplugin' export const unplugin = createUnplugin((options: UserOptions) => { return { name: 'my-first-unplugin', transformInclude (id) { return id.endsWith('.vue') }, transform (code) { return code.replace( /<template>/, `<template><div>Injected</div>` ) }, } }) export const vitePlugin = unplugin.vite export const rollupPlugin = unplugin.rollup export const webpackPlugin = unplugin.webpack
插件的 生命周期
transform 🌶🌶🌶
最常用的钩子,可读取并转换代码了;一般的操作是字符串替换。
buildStart 🌶🌶
开始构建钩子,可读取并改写配置等。
transformInclude *🌶🌶
类似过滤器,可以根据 id(文件名)来决定处理与否
resolveId 🌶
解析到文件资源
load 🌶
加载文件资源,可以返回代码字符串
实现一个 babel-plugin-import,达到按需加载的目的
把组件的导入,替换为精确导入;同时导入对应的样式文件 style.js
import { Button } from 'mand-mobile-next'; ↓ ↓ ↓ ↓ ↓ ↓ import Button from 'mand-mobile-next/dist/es/button' import 'mand-mobile-next/dist/es/button/style.js'
import { Button } from 'mand-mobile-next'; ↓ ↓ ↓ ↓ ↓ ↓ import Button from 'mand-mobile-next/dist/es/button' import 'mand-mobile-next/dist/es/button/style.js'
🚀 环境准备
编辑器:做好技术选型事半功倍
构建工具:tsup 🚁
基于 esbuild 的极速构建工具
# dev tsup src/index.ts --watch # build tsup src/index.ts --format esm,cjs,iife
# dev tsup src/index.ts --watch # build tsup src/index.ts --format esm,cjs,iife
词法解析器:es-module-lexer ⚡️
我比 babel 快 100 倍
import { init, parse } from 'es-module-lexer' (async () => { await init const [imports, exports] = parse('export var p = 5') exports[0] === 'p' })();
import { init, parse } from 'es-module-lexer' (async () => { await init const [imports, exports] = parse('export var p = 5') exports[0] === 'p' })();
字符串处理:magic-string 😁
哎,就是玩儿字符串
import MagicString from 'magic-string' const s = new MagicString( 'problems = 99' ) s.overwrite( 0, 8, 'answer' ) s.toString() // 'answer = 99'
import MagicString from 'magic-string' const s = new MagicString( 'problems = 99' ) s.overwrite( 0, 8, 'answer' ) s.toString() // 'answer = 99'
怎么优雅怎么来
import { init, parse, ImportSpecifier } from 'es-module-lexer' import MagicString from 'magic-string' import { createFilter } from '@rollup/pluginutils' const unplugin = createUnplugin((options: UserOptions) => { const include = ['**/*.vue', '**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'] const exclude = 'node_modules/**' return { name: 'md-import', async transform (code, id) { if (!code || !filter(id) || !needTransform(code)) return null // transformer }, } }) function needTransform(code) { return /("|')mand-mobile-next("|')/.test(code) }
import { init, parse, ImportSpecifier } from 'es-module-lexer' import MagicString from 'magic-string' import { createFilter } from '@rollup/pluginutils' const unplugin = createUnplugin((options: UserOptions) => { const include = ['**/*.vue', '**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'] const exclude = 'node_modules/**' return { name: 'md-import', async transform (code, id) { if (!code || !filter(id) || !needTransform(code)) return null // transformer }, } }) function needTransform(code) { return /("|')mand-mobile-next("|')/.test(code) }
// transformer await init let imports: ImportSpecifier[] = []; try { imports = parse(code)[0] } catch (e) { console.error(id, e) return } if (!imports.length) { return null }
// transformer await init let imports: ImportSpecifier[] = []; try { imports = parse(code)[0] } catch (e) { console.error(id, e) return } if (!imports.length) { return null }
只要能实现,代码难看一点也问题不大
let s: MagicString | undefined const str = () => s || (s = new MagicString(code)) for (let index = 0; index < imports.length; index++) { const { s: start, e: end, se, ss } = imports[index] const name = code.slice(start, end) if (!name) { continue } const importStr = code.slice(ss, se) const exportVariables = transformImportVar(importStr) // overwrite } function transformImportVar(importStr: string) { if (!importStr) return [] const exportStr = importStr.replace('import', 'export').replace(/\s+as\s+\w+,?/g, ',') let exportVariables: string[] = [] return exportVariables = parse(exportStr)[1] }
let s: MagicString | undefined const str = () => s || (s = new MagicString(code)) for (let index = 0; index < imports.length; index++) { const { s: start, e: end, se, ss } = imports[index] const name = code.slice(start, end) if (!name) { continue } const importStr = code.slice(ss, se) const exportVariables = transformImportVar(importStr) // overwrite } function transformImportVar(importStr: string) { if (!importStr) return [] const exportStr = importStr.replace('import', 'export').replace(/\s+as\s+\w+,?/g, ',') let exportVariables: string[] = [] return exportVariables = parse(exportStr)[1] }
// overwrite str() .overwrite( ss, se, exportVariables .map(m => { const path = `mand-mobile/components/${hyphenate(m)}/index.vue` if (fs.existsSync(path)) { return `import ${m} from "${path}"` } else { return `import ${m} from "mand-mobile/components/${hyphenate(m)}"` } }) .join('\n') ) const hyphenateRE = /\B([A-Z])/g; const hyphenate = function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase() }
// overwrite str() .overwrite( ss, se, exportVariables .map(m => { const path = `mand-mobile/components/${hyphenate(m)}/index.vue` if (fs.existsSync(path)) { return `import ${m} from "${path}"` } else { return `import ${m} from "mand-mobile/components/${hyphenate(m)}"` } }) .join('\n') ) const hyphenateRE = /\B([A-Z])/g; const hyphenate = function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase() }
见得多了,就会非常熟悉西方的那一套理论 🤓
构建插件往往在本地 node 中执行,为了执行性能我们可以考虑一些编程技巧
// bad function trim(string) { function trimStart(string) { return string.replace(/^s+/g, ""); } function trimEnd(string) { return string.replace(/s+$/g, ""); } return trimEnd(trimStart(string)) }
// bad function trim(string) { function trimStart(string) { return string.replace(/^s+/g, ""); } function trimEnd(string) { return string.replace(/s+$/g, ""); } return trimEnd(trimStart(string)) }
// good function trimStart(string) { return string.replace(/^s+/g, ""); } function trimEnd(string) { return string.replace(/s+$/g, ""); } function trim(string) { return trimEnd(trimStart(string)) }
// good function trimStart(string) { return string.replace(/^s+/g, ""); } function trimEnd(string) { return string.replace(/s+$/g, ""); } function trim(string) { return trimEnd(trimStart(string)) }
// bad const a = {}
// bad const a = {}
// good const b = Object.create(null)
// good const b = Object.create(null)
// bad class Promise { constructor(executor) { this._promiseCreatedHook(); } _promiseCreatedHook() { // do something } }
// bad class Promise { constructor(executor) { this._promiseCreatedHook(); } _promiseCreatedHook() { // do something } }
// good class Promise { constructor(executor) { this._promiseCreatedHook(); } // Just an empty no-op method. _promiseCreatedHook() {} } function enableMonitoringFeature() { Promise.prototype._promiseCreatedHook = function() { // Actual implementation here } }
// good class Promise { constructor(executor) { this._promiseCreatedHook(); } // Just an empty no-op method. _promiseCreatedHook() {} } function enableMonitoringFeature() { Promise.prototype._promiseCreatedHook = function() { // Actual implementation here } }