前言
之前用组件库的时候,一直不知道原来按需加载的组件库打包是不一样的,单纯的以为只要export {} ,然后import的时候解构获取就可以(获取是可以的,但是打包会全部打进去,没有真正实现按需加载)。但其实像elementui这些组件库,全量组件打包和单个组件打包都是有的。
正好工作中需要封装组件库,学习一下webpack怎么拆分打包组件实现按需加载,至于为什么使用webpack而不是rollup,只是因为webpack平时接触的多,多少会一点。并且记录下遇到的一个巨坑的问题
目标
- 全量打包
- 组件单个打包
- 测试项目中按需加载
准备工作 ------ 先把页面跑起来
1. 初始化项目
npm init
2. 下载需要的依赖包
- “vue”: “^2.6.14”, // 项目是vue2的
- “vue-loader”: “^15.10.0” // 解析vue文件
- “vue-template-compiler”: “2.6”, // 和vue版本一致不然会报错
- babel-loader // 最新语法转换成es5
- html-webpack-plugin // 生成html文件,并引入构建好的js文件
- webpack@5 // 4下载最新sass sass-loader会报错
- webpack-cli //
- webpack-dev-server
3. 新建组件包目录文件(代码放末尾1)
|--- packages
|--- components
|--- bar-rate // 某一个组件
|--- index.js
|--- index.vue
|--- style // 全局样式,组件内可以引入,不重要
|--- hs-ui.js // 所有组件的入口文件
4. 新建示例显示目录文件(代码放末尾2)
|--- example
|--- App.vue
|--- main.js
|--- index.html
|--- page
|--- index.vue // 测试组件效果
5. 配置webpack显示页面
新建webpack.dev.js
,我暂时没有用到图片,所以没有加url-loader
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
mode: "development",
entry: {
index: path.join(__dirname, "../example/", "main.js"),
},
devtool: "source-map",
devServer: {
open: false,
hot: true,
host: "0.0.0.0"
},
module: {
rules: [
{
test: /\.(scss|sass|css)$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
],
},
{
test: /\.vue$/,
use: ["vue-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../example/index.html"),
filename: "index.html",
}),
],
};
在package.json/scripts
属性中需要增加命令
"dev": "webpack-dev-server --config ./webpack/webpack.dev.js"
最后执行npm run dev
就能在本地把整个项目跑起来,在页面上显示当前书写的组件是否正常显示,后续的打包都是建立在组件代码显示没问题的基础之上的。
组件全量打包
全量打包其实就是将打包配置的入口文件指向全量引用组件的hs-ui.js
即可
因为不管是全量打包还是组件打包,都会有一些配置是相同的,所以最开始我是抽出了一些可能会相同的配置属性,但就是这样一个简单的webpack合并配置属性的操作,让我找了两个晚上的bug,走了各种各样的弯路,最后还是将组件打包去掉了merge基础属性
webpack.pro.base.js
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
mode: "production",
externals: {
vue: "vue",
echarts: "echarts",
},
plugins: [new VueLoaderPlugin()],
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(jsx?|babel|es6)$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
],
},
{
test: /\.vue$/,
use: ["vue-loader"],
},
],
},
};
webpack.pro.js
const path = require("path");
const BaseConfig = require("./webpack.pro.base");
const { merge } = require("webpack-merge");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(BaseConfig, {
entry: {
index: path.join(__dirname, "../packages/", "hs-ui.js"),
},
output: {
filename: "hs-ui.common.js",
path: path.resolve(__dirname, "../", "lib/"),
libraryTarget: "umd", // 打成umd的方式
libraryExport: "default",
},
plugins: [new CleanWebpackPlugin()],
});
在package.json/scripts
属性中增加"build:all": "webpack --config ./webpack/webpack.pro.js"
执行npm run build:all
即可生成/lib/hs-ui.common.js
组件单独打包
将每一个组件的index.js
都作为一个入口文件进行单独打包,而且每一个组件都会生成一个index.js
文件和一个index.css
文件。是之后项目中按需加载所需要的文件(和element-ui把样式都打个一个主题文件中不太一样)
const path = require("path");
// const BaseConfig = require("./webpack.pro.base");
// const { merge } = require("webpack-merge");
const fs = require("fs");
const miniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
// 获取所有的入口文件
const files = fs.readdirSync(
path.resolve(__dirname, "../packages/components/")
);
let entry = {};
for (const item of files) {
const name = item.toLowerCase();
entry[name] = path.resolve(
__dirname,
`../packages/components/${name}/`,
"index.js"
);
}
module.exports = {
mode: "production",
externals: {
vue: "vue",
echarts: "echarts",
},
entry,
output: {
path: path.resolve(__dirname, "../", "lib"),
filename: "[name]/index.js",
libraryTarget: "umd", // 打成umd的方式
libraryExport: "default",
},
module: {
rules: [
{
test: /\.(scss|sass|css)$/,
use: [
{
loader: miniCssExtractPlugin.loader,
},
"css-loader",
"sass-loader",
],
},
{
test: /\.(jsx?|babel|es6)$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
],
},
{
test: /\.vue$/,
use: ["vue-loader"],
},
],
},
plugins: [
new VueLoaderPlugin(),
new miniCssExtractPlugin({
filename: "[name]/style.css",
}),
],
};
在package.json/scripts
属性中增加
"build:com": "webpack --config ./webpack/webpack.pro.components.js"
"build": "npm run build:all && npm run build:com"
执行npm run build
即可
项目中按需引入
因为我们当前的组件库并没有上传到npm官网上,所以在另一个项目中直接install是不行的。npm给我们提供了另一种测试的方式:现在当前组件库终端执行npm link
,再在需要使用组件库的项目vscode窗口终端中执行npm link hs-ui
即npm link 组件库名(package.json里边name属性的值),这样,在项目的node_modules中就会有一个包指向我们本地的组件库文件
在项目中按需引入组件库,我们要借助一个插件babel-plugin-component
,新建babel.config.js
module.exports = {
// ......其他原有配置 //
plugins: [
[
'component',
{
libraryName: 'hs-ui',
// libDir: 'lib/packages',
camel2Dash: true,
},
],
],
}
在main.js
中增加
import { BarRate } from 'hs-ui'
Vue.use(BarRate)
以上代码插件会自动给我们变成引入对应组件的index.js和index.css,然后在项目中就可以使用了。当然测试没问题之后,就可以将组件库上传至npm官网,提供给其他项目使用。
总结
开发的时候遇到一个特别坑的问题,就是在单独打包组件的配置文件中,使用了webpack的merge,相当于在base中写了一个loader,merge之后又写了一个不一样的loader,我以为后面写的loader会覆盖通用的loader。但是它没有,打包也不报错,就是死活打不出css文件,js文件在引用时也有问题,各种报错。查找百度也是找不出具体的问题,一次机缘巧合之前,删除了merge,瞬间,整个世界的开朗了。还是自己对于基础配置的使用不熟悉导致的奇奇怪怪的问题。
代码
1. 组件包
/packages/components/bar-rate/index.vue
<template>
<div w-full border-box>
<div class="top-text">
<span>测试</span>
</div>
</div>
</template>
<script>
export default {
name: "hs-bar-rate" // 这个名字要定义好,之后项目中就是用的这个组件名,最好有统一的前缀
};
</script>
<style lang="scss" scoped>
@import "../../style/index.scss"; // 公用样式,不需要关注,也可以没有
.top-text {
width: 100%;
font-size: 16px;
}
</style>
/packages/components/bar-rate/index.js
import BarRate from "./index.vue";
BarRate.install = (Vue) => {
Vue.component(BarRate.name, BarRate);
};
export default BarRate;
/packages/hs-ui.js
const requireComponent = require.context(
// 其组件目录的相对路径
"./",
// 是否查询其子目录
true,
// 匹配基础组件文件名的正则表达式
/index.js$/
);
let componentList = [];
requireComponent.keys().forEach((fileName) => {
// 获取组件配置
let componentConfig = requireComponent(fileName);
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentList.push(componentConfig.default || componentConfig);
});
const install = (Vue) => {
// 判断是否安装过
if (install.installed) return;
// 注册所有组件
componentList.map((component) => {
Vue.use(component);
});
};
export default {
install
// 像elementui在这里又抛出了所有组件包,不知道是为什么
};
2. 示例代码(框架内页面测试)
example/index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<meta content="width=device-width,initial-scale=1.0" name="viewport" />
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
<style>
html, body{
margin:0;
height: 100%;
}
</style>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
example/App.vue
<template>
<DemoPage />
</template>
<script>
import DemoPage from "./page/index.vue";
export default {
name: "App",
components: { DemoPage },
};
</script>
example/page/index.vue
<template>
<div class="page">
<div class="box">
<hs-bar-rate />
</div>
</div>
</template>
<script>
export default {
name: "demo-page",
};
</script>
<style lang="scss" scoped>
.page {
height: 100%;
width: 100%;
background: #02111c;
display: flex;
.box {
width: 25%;
height: 200px;
border: 1px solid blue;
}
}
</style>
example/main.js
import Vue from "vue";
import App from "./App.vue";
// import hsui from "../packages/hs-ui"; // 测试未打包时的功能
// import hsui from "../lib/hs-ui.common"; // 测试打包完之后的全量包功能
// Vue.use(hsui);
// import BarRate from "../packages/components/bar-rate"; // 测试打包前,单个包引入的功能
import BarRate from "../lib/bar-rate/index"; // 测试组件分别打包生成文件功能
import "../lib/bar-rate/style.css";
Vue.use(BarRate);
new Vue({
render: (h) => h(App),
}).$mount("#app");