• 第十三章 实现组件库按需引入功能


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

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

    • 经典方法:组件单独分包 + 按需导入 + 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")
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    // 转换前
    const { Button } = require("smarty-ui-vite")
    // 转换后
    const Button = require("smarty-ui-vite/button")
    
    • 1
    • 2
    • 3
    • 4

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

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

    比如:组件库只使用了 Button

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

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


    实现分包导出

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

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

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

    • 首先将组件目录由 【button】 改为 【Button】

    特别提醒:git 提交的时候注意,默认 git 修改的时候是忽略大小写的。需要修改一下 git 配置才可以提交。

    # 禁止忽略大小写
    git config core.ignorecase false
    
    • 1
    • 2
    • Button 组件入口 index.ts 默认作为插件导出。

    重构前:

    import Button from "./Button";
    
    // 导出 Button 组件
    export default Button
    
    • 1
    • 2
    • 3
    • 4

    重构后:

    import Button from "./Button";
    import { App } from "vue";
    
    // 导出Button组件
    export { Button };
    
    // 导出Vue插件
    export default {
      install(app: App) {
        app.component(Button.name, Button);
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 编写分包导出脚本

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

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

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

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

    • 修改vite.config.ts
    /// 
    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)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 读取vite配置

    文件名:scripts/build.ts

    // 读取 vite 配置
    import { config } from "../vite.config";
    import { build, InlineConfig, defineConfig, UserConfig } from "vite";
    
    // 全量打包
    build(defineConfig(config as UserConfig) as InlineConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 读取组件文件夹遍历组件库文件夹

    文件名: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) => {
          // 文件夹遍历
         });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 为每个模块定制不同的编译规则
      • 导出文件夹为 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 为每个子组件包定制一个自己的 package.json

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

    // 读取
    import Button from "smarty-ui-vite/Button"
    
    • 1
    • 2

    子包的 package.json

    {
          "name": "smarty-ui-vite/Button",
          "main": "index.umd.js",
          "module": "index.umd.js"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

     fs.outputFile(
            path.resolve(outDir, `package.json`),
            `{
              "name": "smarty-ui-vite/${name}",
              "main": "index.umd.js",
              "module": "index.umd.js",
            }`,
            `utf-8`
          );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 最终代码
    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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 安装需要的依赖

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

    pnpm i esno -D
    
    • 1

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

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

    运行代码

    pnpm build
    
    • 1

    测试按需加载

    假设页面中只使用 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 buttontitle>
        <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: `
                
    主要按钮 绿色按钮 灰色按钮 黄色按钮 红色按钮
    搜索按钮 编辑按钮 成功按钮 提示按钮 删除按钮
    `
    }).use(Button.default).mount('#app')
    script> body> html>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 启动项目
    pnpm dev
    
    • 1

    浏览器查看结果

    在这里插入图片描述

  • 相关阅读:
    海外游戏代投需要注意的
    top命令汇总
    Linux下gcc和gdb的基本使用
    刷题笔记19——优势洗牌和去重保持字典序
    9月1日,开学快乐!新的学期,新的开始!
    Python技法:实用运维脚本编写(进程/文件/目录操作)
    科研笔记第三期——一条文章带你玩转柱状图
    【目标检测】理论篇(3)YOLOv5实现
    2、Calcite 源码编译与运行
    ASM外部冗余是否可以替换磁盘
  • 原文地址:https://blog.csdn.net/qq_36362721/article/details/128010472