上一节中我们简单实现了webpack的打包功能,并且对webpack的一些基本配置、打包命令以及不同模式下打包的区别做了一个简单的了解,也了解了下控制台输出信息的含义。接下来的几节我们一起来了解下,针对于不同类型的资源,webpack是如何进行编译打包的,在打包期间又需要做一些什么处理。

js兼容性处理

在上一节中,我们写了一段简单的js代码,用来做打包示例,并对js的打包有了初步的了解:

const add = (x, y) => x + y
console.log(add(1, 2))

我们截取打包后的代码:

eval("const add = (x, y) => x + y;\n\nconsole.log(add(1, 2));\n\n\n//# sourceURL=webpack:///./src/js/index.js?");

我们可以看到,在打包出来的代码中,add 的定义依旧是使用的 const,而不是兼容性最好的 var。现阶段,在我们的日常开发过程中基本上使用的都是ES6语法,但是一些版本较低的浏览器并不能完全兼容ES6语法,此时我们就要对js代码做兼容性处理。

要对js做兼容性处理,这里我们需要用到两个工具:babel-loader 以及 @babel/core。我们先来安装下这两个工具:

yarn add babel-loader @babel/core

安装完成后要怎么使用呢?这里我们需要使用到webpack五个基本概念中的 loader。在上一篇配置的基础上,我们加入 loader 的配置。loader 的相关配置写到配置文件的 module 中:

// 引入node的path模块
const { resolve } = require('path')

module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 文件输出配置
    output: {
    	// 输出的文件名称
        // 配置[name]则取的是入口文件的名称
    	filename: 'js/[name].js',
        // 输出的文件路径
        // 这里是指输出到根目录下的build目录下,如果对__dirname不是很了解的小伙伴可以去nodejs官网了解一下
        path: resolve(__dirname, 'build')
    },
    // 用于配置loader
    module: {
      rules: [
      	{
          // 匹配.js文件
          test: /\.js$/,
          // 需要忽略掉node_modules,不然会对其中的内容也做检查, 会降低打包的性能
          exclude: /node_modules/, 
          // 使用babel-loader
          loader: 'babel-loader',
          // loader的相关配置
          options: {}
        }
      ]
    },
    // 用于配置插件信息
    plugins: [],
    // 启用开发模式
    mode: 'development'
}

配置完成后,我们运行 yarn build:dev 命令,我们会发现结果跟之前是一样的,ES6的代码并没有被转换。这是因为我们还没有“告诉” babel-loader 要怎么去做兼容性处理。那具体该怎么去处理js的兼容性问题呢?我们接着往下看。

处理js兼容的三种方式

针对js的兼容性处理主要有三种方式,我们依次来了解一下。

方法一:基本兼容性处理

基本兼容性处理是指可以处理一些基本的js兼容性问题,比如js中的const/let定义的转换,箭头函数的转换等等。此时我们需要用到一个工具:@babel/preset-env。这个工具可以帮助我们处理一些基本的js兼容性问题。

yarn add -D @babel/preset-env

安装完成后,我们在 babel-loaderoptions 中加入以下配置:

/* ... */
module: {
  rules: [
    {
      // 匹配.js文件
      test: /\.js$/,
      // 需要忽略掉node_modules,不然会对其中的内容也做检查, 会降低打包的性能
      exclude: /node_modules/, 
      // 使用babel-loader
      loader: 'babel-loader',
      // loader的相关配置
      options: {
        presets: ['@babel/preset-env']
      }
    }
  ]
},
/* ... */

配置完成后 ,我面再去执行刚才的打包命令, 然后截取出我们所写的代码:

eval("var add = function add(x, y) {\n  return x + y;\n};\n\nconsole.log(add(1, 2));\n\n//# sourceURL=webpack:///./src/js/index.js?");

此时我们可以发现,代码中的 const 声明已经被转换成了 var, 而原先的 箭头函数 也被转换成了普通的函数形式。说明loader已经生效了。

在介绍这种兼容性处理方式的时候有提到过此种方式只能处理一些基本的js问题。如果我们在文件中加入一些稍微复杂的东西,比如 Promise, 然后执行打包会发生什么呢?

const add = (x, y) => x + y;
console.log(add(1, 2));

const promiseHandle = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('完成');
  }, 1000)
})
console.log(promiseHandle);

执行打包命令后,我们再来看下我们所写的代码:

eval("var add = function add(x, y) {\n  return x + y;\n};\n\nconsole.log(add(1, 2));\nvar promiseHandle = new Promise(function (resolv

我们可以看到,类似于 const 以及 箭头函数 的定义都被转换成了ES5的语法,但是 Promise 的内容并没有被转换,而且代码中也没有其他对 Promise 进行处理的方法。此时要怎么处理呢?莫慌,我们来看第二种js的兼容性处理方式。

方法二:@babel/polyfill

如题,我们需要引入另外一种工具:@babel/polyfill。这种兼容方法比较全面,它可以对 全部js 做兼容性处理。

yarn add -D @babel/polyfill

这个工具使用比较简单,安装完成后,只需在需要使用的js文件中引入即可。我们在刚才的js文件基础上,引入这个包。

import '@babel/polyfill'

const add = (x, y) => x + y;
console.log(add(1, 2));

const promiseHandle = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('完成');
  }, 1000)
})
console.log(promiseHandle);

再次执行打包命令,此时我们打开打包之后的js文件,会发现js文件中引入了大量的polyfill来对js兼容性的处理,其中就有对Promise的兼容处理(由于内容太长,此处就不贴代码了,建议小伙伴们动手试一下~)。此种方式可以解决我们第一种兼容性处理中的问题。

但是,使用这种方式又会引发另外一个问题。我们来看下打包后的文件输出:

不难发现,由于引入了大量的polyfill,导致我们原先仅仅只有几k的文件,膨胀到了511k, 因为引入的polyfill囊括了所有的兼容性处理,这显然不是我们愿意看到的。而且事实上我们也用不到这么多的兼容性处理。那我们又该如何处理这个问题呢?我们来看下第三种兼容性处理方案。

方法三:core-js

说白了,要解决方法二中的弊端,核心思路就是 按需引入。这个时候,我们需要使用到 code-js。其实在方法二中用到的 @babel/polyfill中也有它的影子, 有兴趣的小伙伴们可以去打包生成的文件中找找看~这里我们单独安装一份 core-js:

yarn add -D core-js

想要使用它,我们需要对刚才的 babel-loader 配置再进行一些改造:

/* ... */
module: {
  rules: [
    {
      // 匹配.js文件
      test: /\.js$/,
      // 需要忽略掉node_modules,不然会对其中的内容也做检查, 会降低打包的性能
      exclude: /node_modules/, 
      // 使用babel-loader
      loader: 'babel-loader',
      // loader的相关配置
      options: {
        "presets": [
          [
            "@babel/preset-env",
            {
              // 指定按需加载
              "useBuiltIns": "usage",
              // 指定core-js的版本
              "corejs": {
                "version": 3
              },
              // 指定兼容的浏览器版本
              "targets": {
                "chrome": "60",
                "firefox": "60",
                "ie": "9",
                "safari": "10",
                "edge": "17"
              }
            }
          ]
        ]
      }
    }
  ]
},
/* ... */

此时,我们指定了按需加载,指定了coreJs的版本是3,指定了需要兼容的具体的浏览器的版本。

注意:使用这种方式的时候,需要把第二种方式中引入的@babel/polyfill注释掉,两者不可共用!!

然后我们再次进行打包:

再次打包后我们可以发现,Promise的兼容性处理会被引入进来,并且文件大小也缩小至105k, 相较于第二种方式,显然此种方式更佳~如果有小伙伴觉得这样配置显得代码有些冗余,不够优雅,我们也可将 presets 中配置的内容移动到根目录的 .babelrc 文件中:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        // 指定按需加载
        "useBuiltIns": "usage",
        // 指定core-js的版本
        "corejs": {
          "version": 3
        },
        // 指定兼容的浏览器版本
        "targets": {
          "chrome": "60",
          "firefox": "60",
          "ie": "9",
          "safari": "10",
          "edge": "17"
        }
      }
    ]
  ]
}

行文至此,js的打包方式以及处理兼容的几种方式都已经介绍完毕。当然loader的配置不止这些,在日后的开发过程中,小伙伴们可以根据项目需要去查询对应loader的文档,来定制化适合自己项目的配置。

处理完js的兼容性问题,那么开发团队在日常开发中的js风格统一化又该怎么去处理?一些容易犯的低级错误又该怎么去避免呢?下一章节我们就来介绍处理这些问题的方法~