专栏首页Vue开发社区【干货】将Vue组件库更换为按需加载

【干货】将Vue组件库更换为按需加载

背景

我司前端团队拥有一套支撑公司业务系统的UI组件库,经过多次迭代后,组件库体积非常庞大。

组件库依赖在npm上管理,组件库以项目根目录的 index.js 作为出口导出,文件中导入了项目中所有的组件,并提供组件安装方法。

index.js

import Button from "./button";
import Table from "./table";
import MusicPlayer from "./musicPlayer";
import utils from "../utils"
import * as directive from "../directive";
import * as filters from "../filters";

const components = {
  Button,
  Table,
  MusicPlayer
}

const install = (Vue) => {
  Object.keys(components).forEach(component => Vue.use(component));
  //  此处继续完成一些服务的挂载
}

if (typeof window !== 'undefined' && window.Vue) {
  install(Vue, true);
}

export default {
  install,
  ...components
}

组件库并不导出编译完成后的依赖文件,业务系统使用时,安装依赖并导入,就能注册组件。

import JRUI from 'jr-ui';
Vue.use(JRUI);

组件库的编译是交由业务系统的编译服务顺带编译的。

即组件库项目本身不会编译,仅作为组件导出。node_module 就像一个免费的云盘,用于存储组件库代码。

因为经业务系统编译,在业务系统中。组件库代码能够和本地文件一样,直接调试。而且非常简单粗暴,并不需要做一些依赖导出的额外配置。

但也存在缺点

  1. 组件库中无法使用更为特殊的代码 vue-cli会静态编译在 node_module 引用的 .vue 文件,但不会编译 node_module 中的其他文件,一旦组件库代码存在特殊的语法扩展(JSX),或者特殊的语言(TypeScript)。此时项目启动会运行失败。
  2. 组件库中使用 webpack 的特殊变量将不起效 组件库中的 webpack 配置不会被业务系统去执行,所以组件库中的路径别名等属性无法使用
  3. 组件库依赖每次都是全量加载 index.js 本身就是全量的组件导入,所以即使业务系统只使用了部分组件, index.js 也会将所有的组件文件(图片资源,依赖)都打包进去,依赖体积总是全量大小的。

业务系统并不存在只使用一两个组件的情况,每个业务系统都需要绝大部分组件。

几乎每个项目都会使用比如 按钮,输入框,下拉选项,表格 等常见基础组件。

只有部分组件仅在少数特殊业务线使用,例如 富文本编辑器,音乐播放器

组件分类

为了解决上述问题,及完成按需引入的效果。提供两种组件导出方式,全量导出,基础导出。

将组件导出分为两种类型。基础组件,按需引入组件。

按需引入组件的评定标准为:

  1. 较少业务系统使用
  2. 组件中包含体积较大或资源文件较多的第三方依赖
  3. 未被其他组件内部引用

全量导出模式导出全部组件,基础导出仅导出基础组件。在需要使用按需引入组件时,需要自行引入对应组件。

调整为按需引入

参考 element-ui 的导出方案,组件库导出的组件依赖,要提供每个组件单独打包的依赖文件。

全量导出 index.js 文件无需改动,在 index.js 同级目录增加新文件 base.js,用于导出基础组件。

base.js

import Button from "./Button";
import Table from "./table";

const components = {
  Button,
  Table
}

const install = (Vue) => {
  Object.keys(components).forEach(component => Vue.use(component));
}

export default {
  install,
  ...components
}

修改组件库脚手架工具,增加额外打包配置。用于编译组件文件,输出编译后的依赖。

vue.config.js

const devConfig = require('./build/config.dev');
const buildConfig = require('./build/config.build');

module.exports = process.env.NODE_ENV === 'development' ? devConfig : buildConfig;

config.build.js

const fs = require('fs');
const path = require('path');
const join = path.join;
//  获取基于当前路径的目标文件
const resolve = (dir) => path.join(__dirname, '../', dir);

/**
 * @desc 大写转横杠
 * @param {*} str
 */
function upperCasetoLine(str) {
  let temp = str.replace(/[A-Z]/g, function (match) {
    return "-" + match.toLowerCase();
  });
  if (temp.slice(0, 1) === '-') {
    temp = temp.slice(1);
  }
  return temp;
}

/**
* @desc 获取组件入口
* @param {String} path
*/
function getComponentEntries(path) {
  let files = fs.readdirSync(resolve(path));
  const componentEntries = files.reduce((fileObj, item) => {
    //  文件路径
    const itemPath = join(path, item);
    //  在文件夹中
    const isDir = fs.statSync(itemPath).isDirectory();
    const [name, suffix] = item.split('.');
    //  文件中的入口文件
    if (isDir) {
      fileObj[upperCasetoLine(item)] = resolve(join(itemPath, 'index.js'))
    }
    //  文件夹外的入口文件
    else if (suffix === "js") {
      fileObj[name] = resolve(`${itemPath}`);
    }
    return fileObj
  }, {});
  return componentEntries;
}

const buildConfig = {
  //  输出文件目录
  outputDir: resolve('lib'),
  //  webpack配置
  configureWebpack: {
    //  入口文件
    entry: getComponentEntries('src/components'),
    //  输出配置
    output: {
      //  文件名称
      filename: '[name]/index.js',
      //  构建依赖类型
      libraryTarget: 'umd',
      //  库中被导出的项
      libraryExport: 'default',
      //  引用时的依赖名
      library: 'jr-ui',
    }
  },
  css: {
    sourceMap: true,
    extract: {
      filename: '[name]/style.css'
    }
  },
  chainWebpack: config => {
    config.resolve.alias
    .set("@", resolve("src"))
    .set("@assets", resolve("src/assets"))
    .set("@images", resolve("src/assets/images"))
    .set("@themes", resolve("src/themes"))
    .set("@views", resolve("src/views"))
    .set("@utils", resolve("src/utils"))
    .set("@mixins", resolve("src/mixins"))
    .set("jr-ui", resolve("src/components/index.js"));
  }
}

module.exports = buildConfig;

此时我们的 npm run build 命令,执行的便是以上这段 webpack 配置。

配置中,会寻找组件目录的所有入口文件。对每个入口文件根据设置进行编译输出到指定路径。

configureWebpack: {
  //  入口文件
  entry: getComponentEntries('src/components'),
  //  输出配置
  output: {
    //  文件名称
    filename: '[name]/index.js',
    //  输出依赖类型
    libraryTarget: 'umd',
    //  库中被导出的项
    libraryExport: 'default',
    //  引用时的依赖名
    library: 'jr-ui',
  }
},
css: {
  sourceMap: true,
  extract: {
    filename: '[name]/style.css'
  }
}
function getComponentEntries(path) {
  let files = fs.readdirSync(resolve(path));
  const componentEntries = files.reduce((fileObj, item) => {
    //  文件路径
    const itemPath = join(path, item);
    //  在文件夹中
    const isDir = fs.statSync(itemPath).isDirectory();
    const [name, suffix] = item.split('.');
    //  文件中的入口文件
    if (isDir) {
      fileObj[upperCasetoLine(item)] = resolve(join(itemPath, 'index.js'))
    }
    //  文件夹外的入口文件
    else if (suffix === "js") {
      fileObj[name] = resolve(`${itemPath}`);
    }
    return fileObj;
  }, {});
  return componentEntries;
}

项目中的组件目录为如下,配置将会将每个组件打包编译导出到 lib

components                         组件文件目录
│
│— button
│   │— button.vue                  button组件
│   └─ index.js                    button组件导出文件
│
│— input
│   │— input.vue                   input组件
│   └─ index.js                    input组件导出文件
│
│— musicPlayer
│   │— musicPlayer.vue             musicPlayer组件
│   └─ index.js                    musicPlayer组件导出文件
│
│  base.js                         基础组件的导出文件
└─ index.js                        所有组件的导出文件

lib                                编译后的文件目录
│
│— button
│   │— style.css                   button组件依赖样式
│   └─ index.js                    button组件依赖文件
│
│— input
│   │— style.css                   input组件依赖样式
│   └─ index.js                    input组件依赖文件
│
│— music-player
│   │— style.css                   musicPlayer组件依赖样式
│   └─ index.js                    musicPlayer组件依赖文件
│
│— base
│   │— style.css                   基础组件依赖样式
│   └─ index.js                    基础组件依赖文件
│
└─ index
    │— style.css                   所有组件依赖样式
    └─ index.js                    所有组件依赖文件

获取组件全部入口时,对入口名称做驼峰转横杠处理 upperCasetoLine,是因为 babel-plugin-import 在按需引入时,如组件名称为驼峰命名,路径会转换为横杠分隔。

例如业务系统引入

import { MusicPlayer } from "jr-ui"

//  转化为
var MusicPlayer = require('jr-ui/lib/music-player');
require('jr-ui/lib/music-player/style.css');

因为组件库命名约定,组件文件夹命名大小写并不以横杠隔开。但为了让 babel-plugin-import 正确运行,所以此处对每个文件的入口文件名称做了转换处理。

如不经过方法转换名称,也可以配置 babel.config.js 中的plugin-import配置 camel2DashComponentNamefalse,来禁用名称转换。

babel-plugin-import路径命名issue:https://github.com/ant-design/babel-plugin-import/issues/426

业务系统使用时

全量导出默认导出全部组件

//  全量导出
import JRUI from "jr-ui";
import "jr-ui/lib/index/index.css";

Vue.use(JRUI);

基础导出仅导出基础组件,如需要使用额外组件,需要安装 babel-plugin-import 插件且配置 babel.config.js 来完成导入语句的转换

npm i babel-plugin-import -D

业务系统babel.config.js配置

module.exports = {
  presets: ["@vue/app", ["@babel/preset-env", { "modules": false }]],
  plugins: [
    [
      "import",
      {
        "libraryName": "jr-ui",
        "style": (name) => {
          return `${name}/style.css`;
        }
      }
    ]
  ]
}
//  基础导出
import JRUI_base from "jr-ui/lib/base";
import "jr-ui/lib/base/index.css";
Vue.use(JRUI_base);

//  按需使用额外引入的组件
import { MusicPlayer } from "jr-ui";
Vue.use(MusicPlayer);

业务系统中调试组件库代码

如果仍然想调试组件库代码,在引入组件时,直接引入组件库依赖内的 components 下的组件导出文件并覆盖安装。就能调试目标组件。

import button from "jr-ui/src/components/button";
Vue.use(button);

优化效果

在组件库较大的情况下,优化效果非常明显。在使用基础组件时,体积小了一兆。而且还减少了很多组件内不必要的第三方依赖文件资源。

文章分享自微信公众号:
前端开发社区

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:uniapp社区
原始发表时间:2021-11-25
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • vue、rollup、sass、requirejs组成的vueManager

    近段时间本人一直在思考如何基于vue搭建一个中后端管理系统的通用基础前端解决方案。思考的主要问题点如下: 如何使各个子业务模块的按需加载 css预处理方案的选择...

    sam dragon
  • Vue 的打包优化之路Vue的打包优化之路

    ECharts的JSON地图文件占了很大一部分,大概有1.96MB gzip以后900KB,这一部分是没有办法做处理的。然后ECharts也应该使用按需加载

    MrTreasure
  • 【xingorg1-ui】基于vue3.0从0-1搭建组件库 (八) 组件库打包环境配置

    no-clean表示打包时不删除build文件夹(https://cli.vuejs.org/zh/config/#outputdir) 作用就是为了后边的按需...

    xing.org1^
  • 浅谈低代码平台远程组件加载方案

    低代码开发平台(LCDP)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。通过可视化进行应用程序开发的方法,使具有不同经验水平的开发人员可以...

    CRMEB商城源码
  • 《前端那些事》聊聊前端的按需加载

    前沿:按需加载是性能优化其中的一个环节,可以是图片的按需加载,也就是lazyload来实现按需加载的场景,也可以是组件库的引入,只需部分组件的使用而无需全局引入...

    树酱
  • 微信钱包中58到家首页为什么这么快

    原文标题《前后端分离和模块化-58到家微信首页重构之路》【深度前端干货】 微信钱包内的58到家全新首页已经上线,感兴趣的同学们可以在微信中打开“我的->钱包->...

    架构师之路
  • 京东风格的移动端Vue组件库NutUI2.0来啦

    移动端 Vue 组件库 NutUI 自发布以来受到了广泛的关注。据不完全统计,目前至少有30多个京东的 web 项目正在使用 NutUI 。

    京东技术
  • 《前端那些事》聊聊前端的按需加载

    树酱
  • vite + vue3 中使用按需加载

    该插件主要作用是省去每次使用一个自定义组件,或UI组件库的组件时对组件的引入。兼容不同UI组件库,需要在安装对应UI组件库的前提下,并引入对应UI组件库的 re...

    蓓蕾心晴
  • Webpack 打包优化之体积篇

    谈及如今欣欣向荣的前端圈,不仅有各类框架百花齐放,如Vue, React, Angular等等,就打包工具而言,发展也是如火如荼,百家争鸣;从早期的王者Brow...

    晚晴幽草轩轩主
  • 组件库构建过程

    最近在项目内部创建了一个vue组件库,希望通过组件库的形式,统一项目中组件的逻辑和样式,让代码的复用性更强。

    Keller
  • Vue3!ElementPlus!更加优雅的使用Icon

    这可能是目前最便捷、最合适的 Icon 使用方式了,特别是在 Vue3 中,等等,你以为它只是适用于 Vue3?不,不要被标题欺骗,它支持 Vue2/Vue3、...

    isboyjc
  • 【腾讯云前端性能优化大赛】前端首屏性能优化实战

    在现在的网络环境下,用户访问网页时,如果首屏在3S以内是可以接受的,但是如果首屏在10S以上,绝大部分用户都不会继续等待,这样就会导致用户的流失,对于个人或者企...

    xwj
  • React 如何转 Vue.js

    React 和 Vue 就像可口可乐和百事可乐,很多你可以在 React 中做的事,也同样可以在 Vue 中做。不过两者仍然有一些重要的概念上的差异,其中一些反...

    疯狂的技术宅
  • [Vue 牛刀小试]:第十六章 - 针对传统后端开发人员的前端项目框架搭建

      在之前学习 Vue 基础知识点的文章中,我们还是采用传统的方式,通过在 html 页面上引用 vue.js 这个文件,从而将 Vue 引入到我们的项目开发中...

    程序员宇说

扫码关注腾讯云开发者

领取腾讯云代金券