本文介绍了将Vue组件库更换为按需加载的方法步骤,分享给大家,具体如下:按需加载DEMO仓库地址
按需加载DEMO仓库地址背景
背景背景我司前端团队拥有一套支撑公司业务系统的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 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);

import JRUI from 'jr-ui';
Vue.use(JRUI);
组件库的编译是交由业务系统的编译服务顺带编译的。
即组件库项目本身不会编译,仅作为组件导出。node_module 就像一个免费的云盘,用于存储组件库代码。因为经业务系统编译,在业务系统中。组件库代码能够和本地文件一样,直接调试。而且非常简单粗暴,并不需要做一些依赖导出的额外配置。
但也存在缺点

组件库中无法使用更为特殊的代码
组件库中无法使用更为特殊的代码vue-cli会静态编译在 node_module 引用的 .vue 文件,但不会编译 node_module 中的其他文件,一旦组件库代码存在特殊的语法扩展(JSX),或者特殊的语言(TypeScript)。此时项目启动会运行失败。

组件库中使用 webpack 的特殊变量将不起效
组件库中使用 webpack 的特殊变量将不起效组件库中的 webpack 配置不会被业务系统去执行,所以组件库中的路径别名等属性无法使用

组件库依赖每次都是全量加载
组件库依赖每次都是全量加载index.js 本身就是全量的组件导入,所以即使业务系统只使用了部分组件, index.js 也会将所有的组件文件(图片资源,依赖)都打包进去,依赖体积总是全量大小的。业务系统并不存在只使用一两个组件的情况,每个业务系统都需要绝大部分组件。
几乎每个项目都会使用比如 按钮,输入框,下拉选项,表格 等常见基础组件。
只有部分组件仅在少数特殊业务线使用,例如 富文本编辑器,音乐播放器。组件分类
组件分类组件分类为了解决上述问题,及完成按需引入的效果。提供两种组件导出方式,全量导出,基础导出。
将组件导出分为两种类型。基础组件,按需引入组件。
按需引入组件的评定标准为:

较少业务系统使用

组件中包含体积较大或资源文件较多的第三方依赖

未被其他组件内部引用
较少业务系统使用组件中包含体积较大或资源文件较多的第三方依赖未被其他组件内部引用全量导出模式导出全部组件,基础导出仅导出基础组件。在需要使用按需引入组件时,需要自行引入对应组件。
调整为按需引入
调整为按需引入调整为按需引入参考 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
}


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;


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;
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'

}
}

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;
}


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
所有组件依赖文件

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');


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配置 camel2DashComponentName 为 false,来禁用名称转换。
babel-plugin-import路径命名issue
babel-plugin-import路径命名issue业务系统使用时
业务系统使用时业务系统使用时全量导出默认导出全部组件

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

Vue.use(JRUI);


// 全量导出
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

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`;

}

}

]
]
}

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);


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);

import button from "jr-ui/src/components/button";
Vue.use(button);
优化效果
优化效果优化效果在组件库较大的情况下,优化效果非常明显。在使用基础组件时,体积小了一兆。而且还减少了很多组件内不必要的第三方依赖文件资源。案例仓库地址,如有疑问和错误的地方,欢迎大家提问或指出。
祝你有个快乐的劳动节假期 :)
Have a nice day.
案例仓库地址参考资料
参考资料参考资料vue-cli执行解析
babel-plugin-importvue-cli执行解析babel-plugin-import