预热面试季-webpack进阶篇(babel、多页面打包篇)

368 阅读5分钟

阅前须知

  • 适用人群:初中级前端,对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一下,说明配置成功。

需要说明的是:useBuiltInsbabel 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.htmlindex.js两个静态文件。那么需要多页面打包的时候,我们可以设置entry多个入口文件,同时配置多个outputHtmlWebpackPlugin

首先我们先在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

配置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,有多少个文件夹就配置到entryHtmlWebpackPlugin里面。

// 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目录的名字
        }
      }
    ]
  ],

这时候主项目就可以正常使用自己打包的组件库了