搭建Vue3组件库:第十三章 实现组件库按需引入功能

简介: 本章介绍组件库如何实现按需引入。

组件库会包含几十甚至上百个组件,但是应用的时候往往只使用其中的一部分。这个时候如果全部引入到项目中,就会使输出产物体积变大。按需加载的支持是组件库中必须考虑的问题。

目前组件的按需引入会分成两个方法:

  • 经典方法:组件单独分包 + 按需导入 + babel-plugin-component ( 自动化按需引入);
  • 次时代方法:ESModule + Treeshaking + 自动按需 importunplugin-vue-components 自动化配置)。

分包与树摇(Treeshaking)

传统的解决方案就是将组件库分包导出,比如将组件库分为 ListButtonCard,用到哪个加载哪个,简单粗暴。这样写有两个弊端:

  • 需要了解软件包内部构造 例: import "ui/xxx" or import "ui/package/xxx"
  • 需要不断手工调整组件加载预注册。
// 全部导入
const SmartyUI = require("smarty-ui-vite")

// 单独导入
const Button = require("smarty-ui-vite/button")

好在后面有 babel-plugin-component,解决了需要了解软件包构造的问题。当然你需要按照约定规则导出软件包。

// 转换前
const { Button } = require("smarty-ui-vite")
// 转换后
const Button = require("smarty-ui-vite/button")

随着时代的发展,esmodule 已经成为了前端开发的主流。esmodule 带来好处是静态编译,也就是说,在编译阶段就可以判断需要导入哪些包。

这样就给 Treeshaking 提供了可能。Treeshaking 是一种通过语法分析去除无用代码的方法。目前,Treeshaking 逐渐成为了构建工具的标配,RollupVite新版本的 Webpack 都支持了这个功能。

比如:组件库只使用了 Button

import { Button } from 'smarty-ui-vite'

使用 ES 模块并且只引用了 Button,编译器会自动将其他组件的代码去掉。


实现分包导出

分包导出相当于将组件库形成无数各子软件包,软件包必须满足一下要求:

  • 文件名即组件名;
  • 独立的 package.json 定义,包含 esmumd 的入口定义;
  • 每个组件必须以 Vue 插件形式进行加载;
  • 每个软件包还需要有单独的 css 导出。
  1. 重构代码结构

首先需要在原有代码上进行重构:

  • 首先将组件目录由 【button】 改为 【Button】
特别提醒:git 提交的时候注意,默认 git 修改的时候是忽略大小写的。需要修改一下 git 配置才可以提交。
# 禁止忽略大小写
git config core.ignorecase false
  • Button 组件入口 index.ts 默认作为插件导出。

重构前:

import Button from "./Button";

// 导出 Button 组件
export default Button

重构后:

import Button from "./Button";
import { App } from "vue";

// 导出Button组件
export { Button };

// 导出Vue插件
export default {
  install(app: App) {
    app.component(Button.name, Button);
  },
};
  1. 编写分包导出脚本

默认导出方式是通过配置 vite.config.tsbuild 属性完成。但是在分包导出的时候需要每个组件都分别配置自己的配置文件,而且需要由程序自动读取组件文件夹,根据文件夹的名字遍历打包,还需要为每个子组件包配上一个 package.json 文件。

新建一个 scripts/build.ts 文件。

首先需要学会的是如何使用代码让 vite 打包。

导入 vite.config.ts中的配置,然后调用 vitebuild api 打包。

  • 修改vite.config.ts
/// <reference types="vitest" />
import { defineConfig,UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// 引入重构后的unocss配置
import UnoCSS from './config/unocss'

const rollupOptions = {
  external: ['vue', 'vue-router'],
  output: {
    globals: {
      vue: 'Vue',
    },
  },
}

export const config =  {

    resolve: {
      alias: {
        vue: 'vue/dist/vue.esm-bundler.js', 
      },
    },
    plugins: [
      vue(), // VUE插件
      vueJsx({}), // JSX 插件
  
      UnoCSS(), // 这是重构后的unocss配置
  
      // 添加UnoCSS插件
      // Unocss({
      //   presets: [presetUno(), presetAttributify(), presetIcons()],
      // }),
    ],
  
    // 添加库模式配置
    build: {
      rollupOptions,
      cssCodeSplit: true,   // 追加 css代码分割
      minify: "terser",
      sourcemap: true, // 输出单独的source文件
      reportCompressedSize: true, // 生成压缩大小报告
      lib: {
        entry: './src/entry.ts',
        name: 'SmartyUI',
        fileName: 'smarty-ui',
        // 导出模块格式
        formats: ['es', 'umd', 'iife'],
      },
      outDir: "./dist"
    },
    test: {
      // enable jest-like global test APIs
      globals: true,
      // simulate DOM with happy-dom
      // (requires installing happy-dom as a peer dependency)
      environment: 'happy-dom',
      // 支持tsx组件,很关键
      transformMode: {
        web: [/.[tj]sx$/]
      }
    }
  
  }

export default defineConfig(config as UserConfig)
  • 读取vite配置

文件名:scripts/build.ts

// 读取 vite 配置
import { config } from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";

// 全量打包
build(defineConfig(config as UserConfig) as InlineConfig);
  • 读取组件文件夹遍历组件库文件夹

文件名:scripts/build.ts

const srcDir = path.resolve(__dirname, "../src/");
  fs.readdirSync(srcDir)
    .filter((name) => {
      // 过滤文件只保留包含index.ts
      const componentDir = path.resolve(srcDir, name);
      const isDir = fs.lstatSync(componentDir).isDirectory();
      return isDir && fs.readdirSync(componentDir).includes("index.ts");
    })
    .forEach(async (name) => {
      // 文件夹遍历
     });
  • 为每个模块定制不同的编译规则

    • 导出文件夹为 dist/ <组件名>/ 例: dist/Button
    • 导出模块名为: index.es.jsindex.umd.js
    • 导出模块名为: <组件名> iffe 中绑定到全局的名字
const outDir = path.resolve(config.build.outDir, name);
  const custom = {
    lib: {
      entry: path.resolve(srcDir, name),
      name, // 导出模块名
      fileName: `index`,
      formats: [`esm`, `umd`],
    },
    outDir,
  };

  Object.assign(config.build, custom);
  await build(defineConfig(config as UserConfig) as InlineConfig);
  • 为每个子组件包定制一个自己的 package.json

最后还需要为每个子组件包定制一个自己的 package.json。因为根据 npm 软件包规则,当你 import 子组件包的时候,会根据子包中的 package.json 文件找到对应的模块。

// 读取
import Button from "smarty-ui-vite/Button"

子包的 package.json

{
      "name": "smarty-ui-vite/Button",
      "main": "index.umd.js",
      "module": "index.umd.js"
}

生成 package.json 使用模版字符串实现。

 fs.outputFile(
        path.resolve(outDir, `package.json`),
        `{
          "name": "smarty-ui-vite/${name}",
          "main": "index.umd.js",
          "module": "index.umd.js",
        }`,
        `utf-8`
      );
  • 最终代码
import fs from "fs-extra";
import path from "path";
// 读取 vite 配置
import {config} from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";

const buildAll = async () => {
  
    // 全量打包
    await build(defineConfig(config as UserConfig) as InlineConfig);
    // await build(defineConfig({}))
  
    const srcDir = path.resolve(__dirname, "../src/");
    fs.readdirSync(srcDir)
      .filter((name) => {
        // 只要目录不要文件,且里面包含index.ts
        const componentDir = path.resolve(srcDir, name);
        const isDir = fs.lstatSync(componentDir).isDirectory();
        return isDir && fs.readdirSync(componentDir).includes("index.ts");
      })
      .forEach(async (name) => {
        const outDir = path.resolve(config.build.outDir, name);
        const custom = {
          lib: {
            entry: path.resolve(srcDir, name),
            name, // 导出模块名
            fileName: `index`,
            formats: [`es`, `umd`],
          },
          outDir,
        };
  
        Object.assign(config.build, custom);
        await build(defineConfig(config as UserConfig) as InlineConfig);
  
        fs.outputFile(
          path.resolve(outDir, `package.json`),
          `{
            "name": "smarty-ui-vite/${name}",
            "main": "index.umd.js",
            "module": "index.umd.js"
          }`,
          `utf-8`
        );
      });
  };

  buildAll();
  • 安装需要的依赖

由于脚本是使用typescript编写的所以需要使用 esno 运行。

pnpm i esno -D

代码中还使用的fs的功能所以需要安装fs-extra

pnpm i fs-extra -D
  • package.json中添加脚本
"scripts": {
    "build": "pnpm build:components",
    "build:all": "vite build",
    "build:components": "esno ./scripts/build.ts",
    }

运行代码

pnpm build

测试按需加载

假设页面中只使用 Button 按钮,那么只调用Button子包中的 js、css 就可以了。

文件名:demo/iffe/test_button.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test button</title>
    <link rel="stylesheet" href="../../dist/Button/assets/index.cb9ba4f4.css">
    <script src="../../node_modules/vue/dist/vue.global.js"></script>
    <script src="../../dist/Button/index.iife.js"></script>
</head>
<body>
    <div id="app"></div>
    <script>
        const { createApp } = Vue
        console.log('vue', Vue)
        console.log('SmartyUI', Button)
        createApp({
            template: `
            <div style="margin-bottom:20px;">
                <SButton color="blue">主要按钮</SButton>
                <SButton color="green">绿色按钮</SButton>
                <SButton color="gray">灰色按钮</SButton>
                <SButton color="yellow">黄色按钮</SButton>
                <SButton color="red">红色按钮</SButton>
            </div>
           
            <div style="margin-bottom:20px;">
                <SButton color="blue"  icon="search">搜索按钮</SButton>
                <SButton color="green"  icon="edit">编辑按钮</SButton>
                <SButton color="gray"  icon="check">成功按钮</SButton>
                <SButton color="yellow"  icon="message">提示按钮</SButton>
                <SButton color="red"  icon="delete">删除按钮</SButton>
            </div>
            <div style="margin-bottom:20px;">
                <SButton color="blue"  icon="search"></SButton>
                <SButton color="green"  icon="edit"></SButton>
                <SButton color="gray" icon="check"></SButton>
                <SButton color="yellow"  icon="message"></SButton>
                <SButton color="red"  icon="delete"></SButton>
            </div>
        `}).use(Button.default).mount('#app')
    
    </script>
</body>
</html>
  • 启动项目
pnpm dev

浏览器查看结果

在这里插入图片描述

相关文章
|
1天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
1天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
1天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
4天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
14 2
|
4天前
|
JavaScript API
Vue3快速上手简介
Vue3快速上手简介
16 2
|
4天前
|
API
vue3知识点:reactive函数
vue3知识点:reactive函数
12 1
|
4天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
11 0
|
2天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
2天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
20 9
|
1天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。