本文为Varlet组件库源码主题阅读系列第七篇,读完本篇,可以了解到如何通过unplugin-vue-components插件来为你的组件库实现按需引入。

手动引入

前面的文章中我们介绍过Varlet组件库的打包流程,最后会打包成几种格式,其中modulecommonjs格式都不会把所有组件的代码打包到同一个文件,而是保留着各个组件的独立,每个组件都导出作为一个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在模板中扫描到某个组件时会调用typecomponentresolve,扫描到指令时会调用typedirectiveresolve,如果解析成功,那么就会自动添加导入语句。

当扫描到的组件名以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'导入的componentscreateUnplugin方法执行的返回值:

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同时支持Vue2Vue3,我们看一下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解析器最后返回的是fromnamesideEffects三个字段,所以调用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组件库实现按需引入可以这么做相关推荐

  1. Vue组件库实现按需加载功能

    文章目录 简述 示例 原理 babel-plugin-component element-ui按需引入 babel-plugin-import 组件分开打包以及全部打包 组件分开打包 组件全部打包入口 ...

  2. 使用vue加svg实现流程图代码_京东风格的移动端Vue组件库NutUI2.0来啦

    移动端 Vue 组件库 NutUI 自发布以来受到了广泛的关注.据不完全统计,目前至少有30多个京东的 web 项目正在使用 NutUI . 经过一段时间紧锣密鼓的开发,近期,京东正式发布了 NutU ...

  3. 京东Vue组件库NutUI 2.0发布:将支持跨平台!

    NutUI 是一套来自京东用户体验设计部(JDC)前端开发部的移动端 Vue 组件库,NutUI 1.0 版本于 2018 年发布.据不完全统计,目前在京东至少有30多个 web 项目正在使用 Nut ...

  4. Vant 1.0 发布:轻量、可靠的移动端 Vue 组件库

    Vant 是有赞前端团队维护的移动端 Vue 组件库,提供了一整套 UI 基础组件和业务组件.通过 Vant 可以快速搭建出风格统一的页面,提升开发效率. Vant 一.关于 1.0 距离 Vant ...

  5. 十多款优秀的Vue组件库介绍

    十多款优秀的Vue组件库介绍 1. iView UI组件库 iView 是一套基于 Vue.js 的开源 UI 组件库,主要服务于 PC 界面的中后台产品.iView的组件还是比较齐全的,更新也很快, ...

  6. GitChat · 前端 | Vue 组件库实践和设计

    来自 GitChat 作者:周志祥 更多IT技术分享,尽在微信公众号:GitChat技术杂谈 前言 现在前端的快速发展,已经让组件这个模式变的格外重要.对于市面上的组件库,虽然能满足大部分的项目,但是 ...

  7. 最好的Vue组件库之Vuetify的入坑指南(持续更新中)

    目录 安装Vuetify 文档结构 快速入门 特性 样式和动画 首先先声明,个人不是什么很牛逼的大佬,只是想向那些想入坑Vuetify的前端新手或者嫌文档太长不知如何入手的人提供一些浅显的建议而已,能 ...

  8. vue组件库介绍以及组件库Element UI 的使用

    组件库 前言 这篇文章介绍vue组件库! 介绍什么是组件库以及Element UI组件库的使用! 看完不会你打我.哈哈哈,开玩笑的,不多说,上刺刀!! 1. 什么是 vue 组件库 在实际开发中,前端 ...

  9. vant官网-vant ui 首页-移动端Vue组件库

    Vant 是有赞前端团队开源的移动端vue组件库,适用于手机端h5页面. 鉴于百度搜索不到vant官方网址,分享一下vant组件库官网地址,方便新手使用 vant官网地址https://vant-co ...

最新文章

  1. Windows中将文件压缩成linux支持的tar.gz格式的压缩包
  2. php正则重复匹配,php – 用于匹配任何长度的所有重复子串的正则表达式
  3. 新建VHDL的Vivado工程
  4. boost::geometry::strategy::vincenty用法的测试程序
  5. requestURI的组成部分
  6. c++中.dll与.lib文件的生成与使用的详解
  7. 微信JS-SDK实现分享功能
  8. CV2 puttext不能显示中文问题
  9. java正则表达式 ascii_Java——正则表达式
  10. 基本功:SQL 多表联合查询的几种方式
  11. 如何用“向上管理”搞垮一个团队?
  12. Windows环境变量配置问题
  13. java 分布式同步锁_java编程进阶之路:回归锁的本质,探索分布式锁之源头
  14. SQL 高效运行注意事项(一)
  15. r语言列表添加元素_技术贴 | R语言:geom_smooth在散点图中添加多条回归直线
  16. 三种免费批量下载QQ空间相册方法-2018.05.20亲测有效
  17. MAC中SPSS无法打开数据
  18. python2代码转换为python3
  19. 国外lead,广告联盟常见的任务类型和操作方法
  20. MATLAB使用GPU加速计算

热门文章

  1. C# 连接MYSQL指南,附带增删改查操作代码
  2. 安卓模拟器 arm linux,让x86的android模拟器能模拟arm架构系统
  3. ST-Link的SWD接口的接线方式
  4. C++的lib文件到底是什么
  5. pta 6-7 使用函数求最大公约数 (10 分)
  6. 图像匹配技术简单介绍
  7. L1-070 吃火锅 (15 分)
  8. freetype使用文泉驿显示及保存图片
  9. 【Python学习笔记】36:抓取去哪儿网的旅游产品数据
  10. 什么是十二时辰养生法