Vue组件库实现按需引入可以这么做

news2025/7/6 12:15:57

本文为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 } = 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在模板中扫描到某个组件时会调用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`)
        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'导入的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 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同时支持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.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解析器最后返回的是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的转换和指令的转换其实也大同小异,有兴趣的可以自行阅读源码。

关于组件库的按需引入笔者之前还单独写过一篇,有兴趣的也可以看一下:浅析组件库实现按需引入的几种方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/407640.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript对象

文章目录前言一、对象1.1什么是对象1.2为什么需要对象二、创建对象的三种方式1.利用字面量创建对象2.对象的调用2.1变量、属性、函数、方法总结3.利用new Object创建对象3.1利用构造函数创建对象4.new关键字总结前言 一、对象 1.1什么是对象 现实生活中&#xff1a;万物皆对象…

vue-在组件中使用v-model

一、使用场景 子组件想要使用父组件的值,又想去改父组件的值 二、V-Model的本质 1.给子组件的 value 传个变量 2.监听子组件的input事件,并且把传过来的值赋给父组件的变量 三、关键步骤 1. props 的使用 在自定义的 vue 文件中&#xff0c;声明父组件要向子组件传递的 pr…

左右等高布局

开篇概述 作为这个专栏的开篇&#xff0c;简单介绍一下开这个专栏的初衷。之前在团队中做代码 review 时&#xff0c;经常会发现一些不太“健壮”的 css 代码。这些不太“健壮”的 css 代码&#xff0c;当遇到一些特殊情况时&#xff0c;界面显示就有可能出现问题。其中&#…

uniapp/vue虚拟列表,数据列表渲染优化

引言 相信大家经常会遇到展示一堆数据的需求&#xff0c;几十条数据还好&#xff0c;要是几百上千条&#xff0c;甚至带上了图片。那就会卡得不行。这时候就需要按需加载。 按需加载有懒加载和虚拟列表。 懒加载&#xff1a;通过JS滚动或触底触发事件来加载更多的数据&#…

报错“Cannot read properties of null (reading ‘addEventListener‘)“

场景 控制台报错"Cannot read properties of null (reading addEventListener)" 错误原因 因为 JavaScript 中操作DOM元素的函数方法需要在 HTML 文档渲染完成后才可以使用&#xff0c;如果没有渲染完成&#xff0c;此时的 DOM 树是不完整的&#xff0c;这样在调用…

css鼠标变成小手(css中鼠标悬停是为小手)

CSS控制鼠标样式变换如何写代码呢&#xff1f; 代码&#xff1a;&#xff1a;p stylecursor: crosshair演示&#xff1a;定位指示/p如果需要将鼠标变换成帮助状态的时候。代码&#xff1a;p stylecursor: help演示&#xff1a;帮助/p当然这些只是一些常见的比较实用的代码。我们…

前端实现可拖拽流程的js框架

1 AntV X6 是 AntV 旗下的图编辑引擎&#xff0c;提供了一系列开箱即用的交互组件和简单易用的节点定制能力&#xff0c;方便我们快速搭建 DAG 图、ER 图、流程图等应用。 XFlow是基于 X6 图编辑引擎、面向 React 技术栈用户的专业图编辑应用级解决方案, 帮助您轻松开发复杂的…

前端使用print.js实现打印

前言 项目中经常会用到前端调用浏览器打印的功能&#xff0c;也经常会遇到一些问题&#xff0c;写这篇文章是为了更好的梳理一下相关内容。下面的内容基于vue。 如果需要用到前端生成二维码可以看我的这篇文章&#xff1a;在vue项目中使用qrcodesjs2生成二维码 注&#xff1a…

【学姐面试宝典】—— 前端基础篇Ⅰ(HTTP/HTML/浏览器)

前言 博主主页&#x1f449;&#x1f3fb;蜡笔雏田学代码 专栏链接&#x1f449;&#x1f3fb;【前端面试专栏】 今天开始学习前端面试题相关的知识&#xff01; 感兴趣的小伙伴一起来看看吧~&#x1f91e; 文章目录http 和 https 的基本概念http 和 https 的区别https 协议的工…

【React Router v6】这17个API,你真的都懂了吗?(建议收藏)

😇本文目录😇 Component1.` <BrowserRouter> `2.`<HashRouter>`3.`<Routes />与<Route />`4.`<Link>`5.`<NavLink>`6.`<Navigate />`7.`<Outlet>`Hooks1.`useRoutes()`2.`useNavigate()`3.`useParams()`

【微信小程序】电商移动前端API文档

文章目录一、文档更新说明1、返回值调整2、分类页图片3、订单支付接口二、路径说明三、电商登录实现方式四、API详情1、全局权限访问&#xff08;1&#xff09;主页API获取首页轮播图数据获取首页分类选项数据获取首页楼层数据&#xff08;2&#xff09;商品API商品列表搜索搜索…

pinia详细使用步骤(0基础)

Pinia简介 学vue2的都知道vuex状态管理&#xff0c;所谓状态管理&#xff0c;简单来说就是一个存储数据的地方&#xff0c;存放在Vuex中的数据在各个组件中都能访问到&#xff0c;它是Vue生态中重要的组成部分。 而pinia同理也是起到状态管理的作用&#xff0c;但是它又不完全…

TDesign小程序组件库体验

原来小程序开发有组件库选择的问题&#xff0c;可以使用WeUI或者Vant。今年腾讯开源了前端的框架TDesign&#xff0c;我也分享了两篇使用文章。 年初分享的主要是PC端的框架&#xff0c;除了有PC端的框架外&#xff0c;最近TDesign又新出了小程序的框架&#xff0c;组件比较丰…

前端vue视频vue-video-player插件总结知识点案例(带源码)

目录文档安装main.js文件全局引入视频格式组件方法切换视频清晰度基础案例效果如下完整案例1效果如下完整案例2效果如下最后文档 文档地址 选项参考 API文档 配置函数方法等 Git地址 Git地址 安装 1.vue-video-player 插件下载 npm install vue-video-player --save2.推…

vue3-admin商品管理后台项目(登录页开发和功能实现)

今天来实现vue3-admin商品管理后台项目的登录页功能 首先在pages文件夹下面添加一个login.vue文件&#xff0c;里面先写入简单的template <template><div>登录</div> </template>然后在router文件夹下面的Index.js里面编辑&#xff0c;仍然是引入页面…

【前端开发工具】VUE3 devtools安装

背景 尤雨溪在2020年9月19日晚正式发布vue3.0 one piece。此版本相较于vue2版本&#xff0c;更快、更小、更易维护、更易于原生、让开发者更轻松&#xff1b;所以学习vue3&#xff0c;对于一个前端开发者来说是一个刻不容缓的学习趋势。 学习vue3自然也离不开debug啦~~ Vue官方…

JS——正则表达式(超详细)

正则表达式概念创建正则表达式正则表达式常用方法test(字符串)search(正则表达式&#xff09;正则表达式.exec(字符串&#xff09;字符串.match(正则表达式)字符串.replace(正则表达式&#xff0c;新的内容&#xff09;断言范围类字符类字符类取反修饰符g&#xff1a;global全文…

Web前端 | HTML嵌入JS代码的三种方式

✅作者简介&#xff1a;一位材料转码农的选手&#xff0c;希望一起努力&#xff0c;一起进步&#xff01; &#x1f4c3;个人主页&#xff1a;每天都要敲代码的个人主页 &#x1f525;系列专栏&#xff1a;Web前端 目录 一&#xff1a;JavaScript概述 二&#xff1a; HTML嵌入…

如何升级Vue的版本 vue2.9.6升级到vue3.0

背景&#xff1a;电脑使用多年&#xff0c;一直使用vue 2.x版本&#xff0c;项目开发过程中由于一个模块涉及的集成模块过多&#xff0c;导致需要进行定义的变量越来越多&#xff0c;代码出现冗余&#xff0c;因此在同事的推动下&#xff0c;鉴于vue 3.x的优化&#xff0c;故对…

HTML学生个人网站作业设计:个人主页博客web网页设计制作 (HTML+CSS) (1)

&#x1f329;️ 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f482; 作者主页: 【进入主页—&#x1f680;获取更多源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;HTML5网页期末作业 (1000套…