阅前须知
- 适用人群:初中级前端,对webpack有初步了解的人
- 本文目标:
-
使用babel
-
多页面打包
-
Vue组件库打包
-
babel
Babel相当于一个翻译官,能讲ES6代码转换成ES5代码,这样我们开发过程中就不需要考虑因为使用JS新特性而产生的浏览器兼容问题。同时还可以通过插件机制根据需求来灵活扩展,十分方便。
Babel在执行编辑的时候,会先从项目根目录下.babelrc的文件读取配置,如果没有找到该文件,就会从loader的options地方读取配置。
安装
npm i babel-loader @babel/core @babel/preset-env -D
1、babel-loader是webpack与babel的桥梁
2、@babel/core的作用是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理
3、 @babel/preset-env的作用是把ES6、7、8转换成ES5
使用
// webpack.config.js
module.exports = {
...
module:{
rules:[{
test: /\.js$/,
exclude: /node_modules/, // 不包括node_modules文件夹里面的文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
}
}
// index.js
// 在入口文件写一段es6的代码
const arr = [new Promise(() => {}), new Promise(() => {})];
arr.map(item => {
console.log(item);
});
执行npx webpack命令打包
eval("var arr = [new Promise(function () {}), new Promise(function () {})];\narr.map(function (item) {\n console.log(item);\n});\n\n//# sourceURL=webpack://webpacktest/./src/index.js?");
/******/ })()
可以看到:源文件的const转换成var,箭头函数转换成普通函数,但是Promise没有被转化,因为默认的babel只支持let、const等一些基础的转换,这时候就需要@babel/polyfill,把es的新特性都装进来,以此来弥补低版本浏览器缺失的特性。
@babel/polyfill
安装
npm install --save @babel/polyfill
// 在index.js 入口文件引入
import "@babel/polyfill"
这时候再打包,会发现打包后的文件非常大,只有一行代码,但是却有400kb,这是因为polyfill默认会把所有的特性都注入,所以我们需要配置按需引入
// webpack.config.js 修改options的presets
"presets": [
[
"@babel/preset-env",
{
"targets": {
// 目标浏览器的版本号
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"corejs": 2, //新版本需要指定核心库版本
"useBuiltIns": "usage"
}
],
"@babel/preset-react"
]
修改完配置,按需引入后,重新打包,文件重新变回1kb一下,说明配置成功。
需要说明的是:
useBuiltIns是babel 7的新功能,有3个配置项:
- entry: 需要在入口文件
index.js引入import "@babel/polyfill",这时候babel就会根据你的使用情况导入需要的特性 - usage:不需要再入口文件引入,全自动检测,但是要安装
@babel/polyfill - false:这时候如果你引入了
@babel/polyfill,打包体积就会非常大(不推荐)
扩展
有时候options会写很多内容,我们可以新建一个.babelrc文件,然后把options部分移入到该文件中
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"corejs": 2, //新版本需要指定核心库版本
"useBuiltIns": "usage"
}
]
]
}
// webpack.config.js就可以把options部分去掉
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
多页面打包
在实际的项目中,可能需要配置不同的版本的页面或者应用,然后每个应用都可以当成是一个独立的SPA,可以独立部署,互不影响。
多页面打包原理
通常情况下,我们的项目只有一个main.js入口文件,同时打包输出完也是一个index.html和index.js两个静态文件。那么需要多页面打包的时候,我们可以设置entry多个入口文件,同时配置多个output、HtmlWebpackPlugin
首先我们先在src/app目录下创建3个文件夹page1/page2/page3,分别代表3个独立的单页应用,因此,每个page都要有一个main.js页面的入口文件和index.html页面的html打包逻辑。
- src/app
- page1
- main.js
- index.html
- page2
- main.js
- index.html
- page3
-
main.js
-
index.html
-
- page1
配置webpack
先看看通常的单页应用是怎样打包配置的?单页应用是通过配置entry
// webpack.config.js
module.exports = {
entry: './src/main.js', // 项目的入口文件,webpack会从main.js开始,把所有依赖的js都加载打包
output: {
path: path.resolve(__dirname, './dist'), // 项目的打包文件路径
filename: 'build.js' // 打包后的文件名
}
};
那么多页应用只需要把entry写成对象,多个入口即可。
// webpack.config.js
module.exports = {
entry: {
'page1': './src/app/page1/main.js', // 应用1
'page2': './src/app/page2/main.js', // 应用2
'page3': './src/app/page3/main.js' // 应用3
},
output: {
// 输出到哪里,必须是绝对路径
path: path.resolve(__dirname, './dist'),
filename: '[name].js',
}
}
值得注意的是: 因为多页面的index.html的模板有可能是各不相同的,因此需要配置多个HtmlWebpackPlugin
// webpack.congfig.js
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: "./src/app/page1/index.html",
filename: "page1.html",
chunks: ["page1"]
}),
new HtmlWebpackPlugin({
template: "./src/app/page2/index.html",
filename: "page2.html",
chunks: ["page2"]
}),
new HtmlWebpackPlugin({
template: "./src/app/page3/index.html",
filename: "page3.html",
chunks: ["page3"]
})
]
}
至此,完成以上步骤就可以打包出3个不同的单页应用的静态文件夹了,但是这样的配置太过笨重。因为如果有后续的需求,又要重新复制粘贴一份(老复制粘贴工程师了)。
扩展
下面我们可以写一个方法:读取app目录下的文件夹里面的main.js,有多少个文件夹就配置到entry和HtmlWebpackPlugin里面。
// utils/getApp.js
const path = require("path");
const glob = require("glob"); // npm i glob -D
const HtmlWebpackPlugin = require("html-webpack-plugin");
const getApp = () => {
const entry = {};
const htmlwebpackplugin = [];
// 分析入口文件路径:获取src/app 下面的main.js 入口文件
const entryFiles = glob.sync(path.join(__dirname, "./src/app/*/main.js"));
entryFiles.map((item, index) => {
const entryFile = entryFiles[index];
//! 过滤信息拿到入口名称
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
//! 配置htmlplugin
htmlwebpackplugin.push(
new HtmlWebpackPlugin({
template: `src/index.html`,
filename: `${pageName}.html`,
chunks: [pageName]
})
);
});
console.log(entry);
return {
entry,
htmlwebpackplugin
};
}
// webpack.config.js
const { entry, htmlwebpackplugin } = getApp();
module.exports = {
entry,
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[name]_[chunkhash:8].js"
},
plugins: [...htmlwebpackplugin]
};
扩展2
当我们开发组件库或者工具库的时候,因为polyfill是注入到全局变量的window下的,会污染全局变量,所以polyfill就不合适了。这时候就推荐使用闭包方式:@babel/plugin-transform-runtime,它不会造成全局污染。
安装
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
使用
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
Vue组件库打包
全量打包
首先在src目录下创建entry.js文件,用来引入写好的Vue组件
export { default as Button } from "./components/button";
export { default as Form } from "./components/form";
export { default as Table } from "./components/table";
...
然后在src目录下创建index.js文件,用来作为项目入口,挂载所有的组件
// 引入组件
import * as components from "./entry";
// 把所有的组件注册到Vue里面,同时暴露对象中要含有install方法用于被Vue.use的时候调用
const install = function (Vue) {
if (install.installed) return;
Object.keys(components).forEach(key => {
Vue.component(components[key].name, components[key]);
})
install.installed = true;
};
// 用于script引入
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
// 导出组件的同时,要有install方法
export * from "./entry";
export default {
install
}
上面做的事情,简单来说:就是把去组件读取出来,然后进行统一的Vue.component注册,之后暴露install方法
webpack基本配置
先把js、Vue等一些基本先配置
// webpack.config.js
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: "babel-loader"
}
},
{
test: /\.vue$/,
use: {
loader: "vue-loader"
}
}
]
},
plugins: [new VueLoaderPlugin()],
externals: { // 因为是Vue的组件库,所以默认使用者是装了Vue的,因此放到externals,这样就不会把Vue一起打包进去
vue: {
root: "Vue",
commonjs: "vue",
commonjs2: "vue",
amd: "vue"
}
}
};
webpack打包配置
const path = require("path");
module.exports = {
entry: {
main: path.resolve(__dirname, "./src/index") // 入口的打包文件
},
output: {
path: path.resolve(__dirname, "./lib"), // 打包后输出到lib文件夹
filename: "my-compon.js",
library: "myCompon",
libraryTarget: "umd" // 打包类库的发布格式
},
module: {
rules: [
{
test: /\.scss$/,
use: ["vue-style-loader", "css-loader", "sass-loader"]
}
]
}
}
最后在package.json添加打包命令
"scripts": {
"lib": "webpack --mode production --config webpack.config.js"
}
按需加载
有时候页面只是用到一两个组件,并不需要把整个组件库引进来,所以组件库都需要实现按需加载的功能。
目前业界的处理方式:
iview、ant-design-vue通过babel-plugin-import插件实现。element通过babel-plgin-component
他们本质上都是在编辑过程中,对引用路径进行替换:
import { Button } from 'components'
// 会替换成
var button = require('components/lib/button')
require('components/lib/button/style.css')
但是目前组件库的配置只打包了js文件,相应的CSS文件并没有,所以下面还要配置按照组件打包到相应的文件夹,同时还要把样式提取出来。
每个组件独立生成对应的js和css,不就是上面提及的多入口吗,这就需要我们在入口处就把组件的引用定义好
module.exports = merge(webpackBaseConfig, {
entry: {
"Button": path.resolve(__dirname, "../src/components/button/index.js"),
"Form": path.resolve(__dirname, "../src/components/form/index.js")
},
});
但是上面这么写太蠢了,每增加一个组件就要修改entry,所以我们可以写个方法来动态生成:
// utils/getComponents.js
// 安装glob npm i glob -D
const glob = require("glob");
const entry = Object.assign(
{},
glob
.sync("./src/components/**/index.js")
.map(item => {
return item.split("/")[3];
})
.reduce((acc, cur) => {
acc[`ml-${cur}`] = path.resolve(
__dirname,
`../src/components/${cur}/index.js`
);
return { ...acc };
}, {})
)
在webpack.config.js引入getComponents.js
const entrys = require(./getComponents.js)([组件目录入口]);
module.exports = merge(baseWebpackConfig, {
entry: entrys,
......
});
关于样式文件的提取,需要安装mini-css-extract-plugin,配置完生成的样式文件会单独放在配置的文件夹下
npm i mini-css-extract-plugin -D
module.exports = {
...
plugins: [
new MiniCssExtractPlugin({
filename: "styles/[name].css" // 会放在lib/styles文件夹下面
})
]
}
配置完之后我们发现,引入单个组件的时候,样式不见了,还需要另外引入该组件的样式文件
import Button from "vue-uikit/lib/Button";
import "my-compon/lib/styles/Button.css";
很显然我们不希望在引入组件的时候还要再写一行代码来引入样式,所以我们需要用户(使用组件库的项目)安装babel-plugin-compnent,(参照elementUI)
// .babelrc.js
"plugins": [
[
"component",
{
"libraryName": "my-compon", // 组件库的名字
"styleLibrary": {
"base": false, // 是否每个组件都默认引用base.css
"name": "styles" // css目录的名字
}
}
]
],
这时候主项目就可以正常使用自己打包的组件库了