本文为Varlet组件库源码主题浏览系列第七篇,读完本篇,能够理解到如何通过unplugin-vue-components插件来为你的组件库实现按需引入。
手动引入
后面的文章中咱们介绍过Varlet
组件库的打包流程,最初会打包成几种格局,其中module
和commonjs
格局都不会把所有组件的代码打包到同一个文件,而是保留着各个组件的独立,每个组件都导出作为一个Vue
插件。
第一种按需应用的办法是咱们手动导入某个组件并进行注册:
import { createApp } from 'vue'import { Button } from '@varlet/ui'import '@varlet/ui/es/button/style/index.js'createApp().use(Button)
Button
组件并不是从它的本身目录中引入的,而是从一个对立的入口,@varlet/ui
包的package.json
中配置了两个导出入口:
按需引入,也能够了解成是tree shaking
,它依赖于ES6
模块,因为ESM
模块语法是动态的,和运行时无关,只能顶层呈现,这就能够只剖析导入和导出,不运行代码即可晓得模块导出的哪些被应用了哪些没有,没有用到的就能够被删除。
所以想要反对按需引入那么必然应用的是module
入口,这个字段目前各种构建工具应该都是反对的,module
入口它是个对立的入口,这个文件中显然导出了所有组件,那么比方咱们只导出Button
组件,其余没有用到的组件最终是不是不会被打包进去呢,实际上并没有这么简略,因为有的文件它会存在副作用,比方批改了原型链、设置了全局变量等,所以尽管没有显式的被应用,然而只有引入了该文件,副作用就失效了,所以不能被删除,要解决这个问题须要在package.json
中再配置一个sideEffects
字段,指明哪些文件是存在副作用的,没有指明的就是没有副作用的,那么构建工具就能够释怀的删除了:
能够看到Varlet
通知了构建工具,这些款式文件是有副作用的,不要给删除了,其余文件中没有用到的模块能够纵情删除。
主动引入
如果你感觉后面的手动引入比拟麻烦,Varlet
也反对主动引入,这个实现依赖的是unplugin-vue-components插件,这个插件会扫描所有申明在模板中的组件,而后主动引入 组件逻辑
和 款式文件
并 注册组件
。
在Vite
中的配置形式:
import vue from '@vitejs/plugin-vue'import components from 'unplugin-vue-components/vite'import { VarletUIResolver } from 'unplugin-vue-components/resolvers'import { defineConfig } from 'vite'export default defineConfig({ plugins: [ vue(), components({ resolvers: [VarletUIResolver()] }) ]})
如果想要这个插件反对你的组件库,须要编写一个解析器,也就是相似下面的VarletUIResolver
,如果想要给更多人用就须要提个pr
,这个插件目前曾经反对如下这些风行的组件库:
以VarletUIResolver
为例来看一下这个解析器都做了什么:
// unplugin-vue-components/src/core/resolvers/varlet-ui.tsconst varDirectives = ['Ripple', 'Lazy']export function VarletUIResolver(options: VarletUIResolverOptions = {}): ComponentResolver[] { return [ { type: 'component', resolve: (name: string) => { const { autoImport = false } = options if (autoImport && varFunctions.includes(name)) return getResolved(name, options) if (name.startsWith('Var')) return getResolved(name.slice(3), options) }, }, { type: 'directive', resolve: (name: string) => { const { directives = true } = options if (!directives) return if (!varDirectives.includes(name)) return return getResolved(name, options) }, }, ]}
执行VarletUIResolver
办法会返回一个数组,unplugin-vue-components
反对主动导入组件和指令,所以能够看到下面返回了两种解析办法,尽管目前咱们没有看到unplugin-vue-components
的源码,然而咱们能够猜测unplugin-vue-components
在模板中扫描到某个组件时会调用type
为component
的resolve
,扫描到指令时会调用type
为directive
的resolve
,如果解析胜利,那么就会主动增加导入语句。
当扫描到的组件名以Var
结尾或扫描到Varlet
的指令时,两个解析办法最初都会调用getResolved
办法:
// unplugin-vue-components/src/core/resolvers/varlet-ui.tsexport function getResolved(name: string, options: VarletUIResolverOptions): ComponentResolveResult { const { importStyle = 'css', importCss = true, importLess, autoImport = false, version = 'vue3', } = options // 默认是vue3版本 const path = version === 'vue2' ? '@varlet-vue2/ui' : '@varlet/ui' const sideEffects = [] // 款式导入文件 if (importStyle || importCss) { if (importStyle === 'less' || importLess) sideEffects.push(`${path}/es/${kebabCase(name)}/style/less.js`) else sideEffects.push(`${path}/es/${kebabCase(name)}/style`) } return { from: path, name: autoImport ? name : `_${name}Component`, sideEffects, }}
函数的返回值是一个对象,蕴含三个属性:组件的导入门路、导入名称、以及一个副作用列表,外面是组件的款式导入文件。
你可能会对组件的导入名称格局_${name}Component
有点纳闷,看一下Varlet
的导出形式,以Button
组件为例,它的导出文件如下:
默认导出了组件之外还通过_ButtonComponent
名称又导出了一份,而后看看@varlet/ui
整体的导出文件:
import Button, * as ButtonModule from './button'export const _ButtonComponent = ButtonModule._ButtonComponent || {}function install(app) { Button.install && app.use(Button)}export { install, Button,}export default { install, Button,}
所以_${name}Component
格局导出的就是ButtonModule._ButtonComponent
,为什么要这么做呢,为什么不间接从:
export { install, Button,}
中导入Button
呢,按理说应该也是能够的,其实是因为Varlet
有些组件默认的导出不是组件自身,比方ActionSheet
:
默认导出的是一个函数,基本不是组件自身,那么显然不能间接在模板中应用。
接下来以在Vite
中的应用为例来大抵看一下unplugin-vue-components
的实现原理。
浅析unplugin-vue-components
import components from 'unplugin-vue-components/vite'
导入的components
是createUnplugin
办法执行的返回值:
import { createUnplugin } from 'unplugin'export default createUnplugin<Options>((options = {}) => { const filter = createFilter( options.include || [/\.vue$/, /\.vue\?vue/, /\.vue\?v=/], options.exclude || [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/], ) const ctx: Context = new Context(options) const api: PublicPluginAPI = { async findComponent(name, filename) { return await ctx.findComponent(name, 'component', filename ? [filename] : []) }, stringifyImport(info) { return stringifyComponentImport(info, ctx) }, } return { api, transformInclude(id) { return filter(id) }, async transform(code, id) { if (!shouldTransform(code)) return null try { const result = await ctx.transform(code, id) ctx.generateDeclaration() return result } catch (e) { this.error(e) } }, //...s }})
unplugin是一个构建工具的对立插件零碎,也就是写一个插件,反对各种构建工具,目前反对以下这些:
createUnplugin
办法接管一个函数为参数,最初会返回一个对象,能够从这个对象中获取用于各个构建工具的插件:
传入的函数会返回一个对象,其中transformInclude
配置默认只转换.vue
文件,transform
为转换的外围办法,接管unplugin-vue-components
插件之前的其余插件解决过后的Vue
文件内容和文件门路作为参数,函数内调用了ctx.transform
办法,这个办法又调用了transformer
办法:
export default function transformer(ctx: Context, transformer: SupportedTransformer): Transformer { return async (code, id, path) => { ctx.searchGlob() const sfcPath = ctx.normalizePath(path) // 用文件内容创立一个魔术字符串 const s = new MagicString(code) // 转换组件 await transformComponent(code, transformer, s, ctx, sfcPath) // 转换指令 if (ctx.options.directives) await transformDirectives(code, transformer, s, ctx, sfcPath) // 追加一个正文内容:'/* unplugin-vue-components disabled */' s.prepend(DISABLE_COMMENT) // 将解决完后的魔术字符串从新转换成一般字符串 const result: TransformResult = { code: s.toString() } if (ctx.sourcemap) result.map = s.generateMap({ source: id, includeContent: true }) return result }}
创立了一个MagicString,而后调用了transformComponent
办法:
export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) { const results = transformer === 'vue2' ? resolveVue2(code, s) : resolveVue3(code, s) // ...}
unplugin-vue-components
同时反对Vue2
和Vue3
,咱们看一下Vue3
的转换,调用的是resolveVue3
办法:
const resolveVue3 = (code: string, s: MagicString) => { const results: ResolveResult[] = [] for (const match of code.matchAll(/_resolveComponent[0-9]*\("(.+?)"\)/g)) { const matchedName = match[1] if (match.index != null && matchedName && !matchedName.startsWith('_')) { const start = match.index const end = start + match[0].length results.push({ rawName: matchedName, replace: resolved => s.overwrite(start, end, resolved), }) } } return results}
咱们应用Vue3
官网的在线playground来看一下Vue
单文件的编译后果,如果咱们没有导入组件就在模板中援用组件,那么编译后果如下:
能够看到编译后的setup
函数返回的渲染函数中生成了const _component_MyComp = _resolveComponent("MyComp")
这行代码用来解析组件,那么后面的resolveVue3
办法里的正则/_resolveComponent[0-9]*\("(.+?)"\)/g
的含意就很显著了,就是用来匹配这个解析语句,参数就是组件的名称,所以通过这个正则会找出所有援用的组件,并返回一个对应的替换办法,回到transformComponent
办法:
export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) { // ... for (const { rawName, replace } of results) { const name = pascalCase(rawName) ctx.updateUsageMap(sfcPath, [name]) const component = await ctx.findComponent(name, 'component', [sfcPath]) // ... }}
遍历模板引入的所有组件,调用了ctx.findComponent
办法:
async findComponent(name: string, type: 'component' | 'directive', excludePaths: string[] = []): Promise<ComponentInfo | undefined> { // custom resolvers for (const resolver of this.options.resolvers) { if (resolver.type !== type) continue const result = await resolver.resolve(type === 'directive' ? name.slice(DIRECTIVE_IMPORT_PREFIX.length) : name) if (!result) continue if (typeof result === 'string') { info = { as: name, from: result, } } else { info = { as: name, ...normalizeComponetInfo(result), } } if (type === 'component') this.addCustomComponents(info) else if (type === 'directive') this.addCustomDirectives(info) return info } return undefined }
这个办法里就会调用组件库自定义的解析器,如果某个组件被胜利解析到了,那么会将后果保存起来并返回。
回到transformComponent
办法:
export default async function transformComponent(code: string, transformer: SupportedTransformer, s: MagicString, ctx: Context, sfcPath: string) { // ... let no = 0 for (const { rawName, replace } of results) { // ... if (component) { const varName = `__unplugin_components_${no}` s.prepend(`${stringifyComponentImport({ ...component, as: varName }, ctx)};\n`) no += 1 replace(varName) } }}
组件如果被解析到了,那么会调用stringifyComponentImport
办法创立导入语句并追加到文件内容的结尾,留神组件的导入名称被命名成了__unplugin_components_${no}
格局,为什么不间接应用组件本来的名字呢,笔者也不分明,可能是为了避免用户本人又导入了组件导致反复吧:
export function stringifyComponentImport({ as: name, from: path, name: importName, sideEffects }: ComponentInfo, ctx: Context) { path = getTransformedPath(path, ctx.options.importPathTransform) const imports = [ stringifyImport({ as: name, from: path, name: importName }), ] if (sideEffects) toArray(sideEffects).forEach(i => imports.push(stringifyImport(i))) return imports.join(';')}export function stringifyImport(info: ImportInfo | string) { if (typeof info === 'string') return `import '${info}'` if (!info.as) return `import '${info.from}'` else if (info.name) return `import { ${info.name} as ${info.as} } from '${info.from}'` else return `import ${info.as} from '${info.from}'`}
这个办法会依据info
的类型拼接导入语句,VarletUIResolver
解析器最初返回的是from
、name
、sideEffects
三个字段,所以调用stringifyImport
办法时会走第三个分支,以后面截图中的为例,后果如下:
import { MyComp as __unplugin_components_0 } from 'xxx'import { MyComp2 as __unplugin_components_1 } from 'xxx'
另外也能够看到副作用列表sideEffects
也被导入了,实际上就是组件的款式导入文件。
transformComponent
办法最初调用replace(varName)
办法将/_resolveComponent[0-9]*\("(.+?)"\)/
匹配到的内容改成了__unplugin_components_${no}
,还是后面截图中的为例:
const _component_MyComp = _resolveComponent("MyComp")const _component_MyComp2 = _resolveComponent("MyComp2")
被改成了:
const _component_MyComp = __unplugin_components_0const _component_MyComp2 = __unplugin_components_1
到这里Vue3
组件的导入语句就增加实现了,也能失常传递到渲染函数中进行应用,Vue2
的转换和指令的转换其实也大同小异,有趣味的能够自行浏览源码。
对于组件库的按需引入笔者之前还独自写过一篇,有趣味的也能够看一下:浅析组件库实现按需引入的几种形式。