将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 就像一个免费的云盘,用于存储组件库代码。
因为经业务系统编译,在业务系统中。组件库代码能够和本地文件一样,直接调试。而且非常简单粗暴,并不需要做一些依赖导出的额外配置。
但也存在缺点
- 组件库中无法使用更为特殊的代码
vue-cli会静态编译在 node_module 引用的 .vue 文件,但不会编译 node_module 中的其他文件,一旦组件库代码存在特殊的语法扩展(JSX),或者特殊的语言(TypeScript)。此时项目启动会运行失败。
- 组件库中使用 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 }
修改组件库脚手架工具,增加额外打包配置。用于编译组件文件,输出编译后的依赖。
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配置 camel2DashComponentName 为 false,来禁用名称转换。
业务系统使用时
全量导出默认导出全部组件
// 全量导出 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);
优化效果
在组件库较大的情况下,优化效果非常明显。在使用基础组件时,体积小了一兆。而且还减少了很多组件内不必要的第三方依赖文件资源。
案例仓库地址,如有疑问和错误的地方,欢迎大家提问或指出。
祝你有个快乐的劳动节假期 :)
Have a nice day.
参考资料