Vue组件库实现按需引入可以这么做
本文为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.ts
const varDirectives = ['Ripple', 'Lazy']export function VarletUIResolver(options: VarletUIResolverOptions = {}): ComponentResolver[] {return [{type: 'component',resolve: (name: string) => {const { autoImport = false } = optionsif (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 } = optionsif (!directives)returnif (!varDirectives.includes(name))returnreturn 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.ts
export 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`)elsesideEffects.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 nulltry {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.indexconst end = start + match[0].lengthresults.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 resolversfor (const resolver of this.options.resolvers) {if (resolver.type !== type)continueconst result = await resolver.resolve(type === 'directive' ? name.slice(DIRECTIVE_IMPORT_PREFIX.length) : name)if (!result)continueif (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 = 0for (const { rawName, replace } of results) {// ...if (component) {const varName = `__unplugin_components_${no}`s.prepend(`${stringifyComponentImport({ ...component, as: varName }, ctx)};\n`)no += 1replace(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}'`elsereturn `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_0
const _component_MyComp2 = __unplugin_components_1
到这里Vue3
组件的导入语句就添加完成了,也能正常传递到渲染函数中进行使用,Vue2
的转换和指令的转换其实也大同小异,有兴趣的可以自行阅读源码。
关于组件库的按需引入笔者之前还单独写过一篇,有兴趣的也可以看一下:浅析组件库实现按需引入的几种方式。
Vue组件库实现按需引入可以这么做相关推荐
- Vue组件库实现按需加载功能
文章目录 简述 示例 原理 babel-plugin-component element-ui按需引入 babel-plugin-import 组件分开打包以及全部打包 组件分开打包 组件全部打包入口 ...
- 使用vue加svg实现流程图代码_京东风格的移动端Vue组件库NutUI2.0来啦
移动端 Vue 组件库 NutUI 自发布以来受到了广泛的关注.据不完全统计,目前至少有30多个京东的 web 项目正在使用 NutUI . 经过一段时间紧锣密鼓的开发,近期,京东正式发布了 NutU ...
- 京东Vue组件库NutUI 2.0发布:将支持跨平台!
NutUI 是一套来自京东用户体验设计部(JDC)前端开发部的移动端 Vue 组件库,NutUI 1.0 版本于 2018 年发布.据不完全统计,目前在京东至少有30多个 web 项目正在使用 Nut ...
- Vant 1.0 发布:轻量、可靠的移动端 Vue 组件库
Vant 是有赞前端团队维护的移动端 Vue 组件库,提供了一整套 UI 基础组件和业务组件.通过 Vant 可以快速搭建出风格统一的页面,提升开发效率. Vant 一.关于 1.0 距离 Vant ...
- 十多款优秀的Vue组件库介绍
十多款优秀的Vue组件库介绍 1. iView UI组件库 iView 是一套基于 Vue.js 的开源 UI 组件库,主要服务于 PC 界面的中后台产品.iView的组件还是比较齐全的,更新也很快, ...
- GitChat · 前端 | Vue 组件库实践和设计
来自 GitChat 作者:周志祥 更多IT技术分享,尽在微信公众号:GitChat技术杂谈 前言 现在前端的快速发展,已经让组件这个模式变的格外重要.对于市面上的组件库,虽然能满足大部分的项目,但是 ...
- 最好的Vue组件库之Vuetify的入坑指南(持续更新中)
目录 安装Vuetify 文档结构 快速入门 特性 样式和动画 首先先声明,个人不是什么很牛逼的大佬,只是想向那些想入坑Vuetify的前端新手或者嫌文档太长不知如何入手的人提供一些浅显的建议而已,能 ...
- vue组件库介绍以及组件库Element UI 的使用
组件库 前言 这篇文章介绍vue组件库! 介绍什么是组件库以及Element UI组件库的使用! 看完不会你打我.哈哈哈,开玩笑的,不多说,上刺刀!! 1. 什么是 vue 组件库 在实际开发中,前端 ...
- vant官网-vant ui 首页-移动端Vue组件库
Vant 是有赞前端团队开源的移动端vue组件库,适用于手机端h5页面. 鉴于百度搜索不到vant官方网址,分享一下vant组件库官网地址,方便新手使用 vant官网地址https://vant-co ...
最新文章
- Windows中将文件压缩成linux支持的tar.gz格式的压缩包
- php正则重复匹配,php – 用于匹配任何长度的所有重复子串的正则表达式
- 新建VHDL的Vivado工程
- boost::geometry::strategy::vincenty用法的测试程序
- requestURI的组成部分
- c++中.dll与.lib文件的生成与使用的详解
- 微信JS-SDK实现分享功能
- CV2 puttext不能显示中文问题
- java正则表达式 ascii_Java——正则表达式
- 基本功:SQL 多表联合查询的几种方式
- 如何用“向上管理”搞垮一个团队?
- Windows环境变量配置问题
- java 分布式同步锁_java编程进阶之路:回归锁的本质,探索分布式锁之源头
- SQL 高效运行注意事项(一)
- r语言列表添加元素_技术贴 | R语言:geom_smooth在散点图中添加多条回归直线
- 三种免费批量下载QQ空间相册方法-2018.05.20亲测有效
- MAC中SPSS无法打开数据
- python2代码转换为python3
- 国外lead,广告联盟常见的任务类型和操作方法
- MATLAB使用GPU加速计算