实现组件库按需引入功能

Posted 天界程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现组件库按需引入功能相关的知识,希望对你有一定的参考价值。

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

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

  • 经典方法:组件单独分包 + 按需导入 + 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

浏览器查看结果

以上是关于实现组件库按需引入功能的主要内容,如果未能解决你的问题,请参考以下文章

babel-plugin-import 实现按需引入

组件库按需加载 借助babel-plugin-import实现

[转] 组件库按需加载 借助babel-plugin-import实现

vue通过组件,按需引入echarts 柱状图 (全网最简单写法)

vue通过组件,按需引入echarts 柱状图 (全网最简单写法)

React UI组件库——如何快速实现antd的按需引入和自定义主题