首先,配置babel-loader

babel-loader使用babel来转译js,例如把es6转为es5,即对代码进行polyfills(垫片),让旧浏览器也能被“垫平”到与新浏览器一样好的兼容性支持能力。

  1. 安装相关依赖npm i babel-loader @babel/core @babel/preset-env -D

    babel-loader: 让babel能够在webpack里工作;
    @babel/core:babel核心模块;
    @babel/preset-env:转换 ES2015+的babel预设(一组babel插件集合)。

  2. 在module.rules中,给js资源配置babel-loader,并添加预设。

    module.exports = {
            // ...
            module: {
                rules: [
                    {
                        test: /\.js$/,
                        exclude: /node_modules/, // 排除依赖里的代码
                        use: {
                            loader: 'babel-loader',
                            // loader参数分为:1.babel库的参数 2.babel-loader的参数
                            // 可把babel库的参数抽离到配置文件babel.config.js中
                            options: {
                                // babel库的参数:预设,一组插件集合
                                // @babel/preset-react: 编译react jsx语法预设
                                // @babel/preset-typescript: 编译ts预设
                                presets: ['@babel/preset-env']
                                // 常用的babel-loader的参数
                                // 开启babel编译缓存
                                // 默认缓存目录node_modules/.cache/babel-loader
                                cacheDirectory: true, 
                                // 缓存文件不要压缩
                  			    cacheCompression: false, 
                            }
                        }
                    }
                ]
            },
        }
    
  3. 配置目标浏览器。

    // package.json
    {
    	// 其它package配置省略
    	"browserslist": [
       		"ie 11" // 目标浏览器为ie 11
    	]
    }
    

配置完这里,已经可以转换像箭头函数之类的语法了,但有一些语法的转换需要进一步优化:

async转译之regeneratorRuntim函数

安装相对旧版本的babel搭配@babel/preset-env使用时,如果转译了代码中的async、await语法,会在浏览器报regeneratorRuntime is not definded的错误,这时需要配@babel/plugin-transform-runtime插件

regeneratorRuntime是babel生成的一个用来兼容async/await语法的函数。

babel@7+转换语法时,会把regeneratorRuntime函数和辅助函数,以内联方式重复注入到每处需要用到的代码中,不利于代码复用。

@babel/plugin-transform-runtime插件默认可将@babel/runtime模块引入项目,@babel/runtime包含babel辅助函数、regeneratorRuntime函数,经过babel转换的代码需要用到这些函数时,可共用它们:

  1. 在上一步的基础上,安装项目依赖npm i @babel/runtime

  2. 安装开发依赖npm i @babel/plugin-transform-runtime -D

  3. 配置module.rules

    module.exports = {
            // ...
            module: {
                rules: [
                    {
                        test: /\.js$/,
                        exclude: /node_modules/, // 排除依赖里的代码
                        use: {
                            loader: 'babel-loader',
                            // loader参数
                            options: {
                                presets: ['@babel/preset-env'],
                                // 记得安装项目依赖@babel/runtime
                                plugins: ['@babel/plugin-transform-runtime'] 
                            }
                        }
                    }
                ]
            },
        }
    

这里,项目中的新语法基本已经可以完美转译了,但是较新的api依旧可能有问题,如Promise、WeakMap等,在ie11中仍无法使用。

@babel/polyfill与core-js

js兼容性转换分为两种:

  • 语法类的转译,如const、箭头函数、可选链操作符,babel称为syntax类型(通过babel插件转换)
  • api类的覆写,如Promise、WeakMap、startsWith、includes等原生api,称为built-in类型(需要在业务代码里插入兼容代码,如Promise的es5实现)

上一步只实现了syntax的处理,built-in的还没处理:

@babel/polyfillcore-js依赖功能上是一样的:用来解决built-in类型的覆写(垫平),它包含了es6+新增的原生api的实现。

通俗来讲就是,用es5实现了es6+的一些新的api,代码里引入后,它们可以修改Object等原生对象的prototype,以实现api的扩展。

因为@babel/polyfill或core-js都是要被打包到最终代码里的,所以应该安装为项目依赖,而非开发依赖

// 入口文件 
// 必须在有兼容性代码执行前执行polyfill代码,所以一般在入口文件引入
import '@babel/polyfill';
// import 'core-js'; // 或者使用core-js
console.log('dfdfdf'.replaceAll('d','e'));

使用core-js替代@babel/polyfill

引入@babel/polyfill后,代码体积会变大,因为里面包含了全部兼容性代码。它被babel@7淘汰,而改用core-jscore-js把不同的兼容语法进行了精细划分,可按需引入部分兼容性代码。

core-js既可以全量兼容,也可以只兼容指定的语法,还可以做到无副作用兼容(不修改原生对象的prototype)

// 官方示例
import 'core-js/actual'; // 全部兼容

/* 只导入用到了的新语法
import 'core-js/actual/promise'; 
import 'core-js/actual/set';
import 'core-js/actual/iterator';
import 'core-js/actual/array/from';
import 'core-js/actual/array/flat-map';
import 'core-js/actual/structured-clone';
*/ 

// 被兼容的新语法↓
Promise.resolve(42).then(it => console.log(it)); // => 42

Array.from(new Set([1, 2, 3]).union(new Set([3, 4, 5]))); // => [1, 2, 3, 4, 5]

[1, 2].flatMap(it => [it, it]); // => [1, 1, 2, 2]

(function * (i) { while (true) yield i++; })(1)
  .drop(1).take(5)
  .filter(it => it % 2)
  .map(it => it ** 2)
  .toArray(); // => [9, 25]

structuredClone(new Set([1, 2, 3])); // => new Set([1, 2, 3])

不修改全局的对象:

import Promise from 'core-js-pure/actual/promise';
import Set from 'core-js-pure/actual/set';
import Iterator from 'core-js-pure/actual/iterator';
import from from 'core-js-pure/actual/array/from';
import flatMap from 'core-js-pure/actual/array/flat-map';
import structuredClone from 'core-js-pure/actual/structured-clone';

Promise.resolve(42).then(it => console.log(it)); // => 42

from(new Set([1, 2, 3]).union(new Set([3, 4, 5]))); // => [1, 2, 3, 4, 5]

flatMap([1, 2], it => [it, it]); // => [1, 1, 2, 2]

Iterator.from(function * (i) { while (true) yield i++; }(1))
  .drop(1).take(5)
  .filter(it => it % 2)
  .map(it => it ** 2)
  .toArray(); // => [9, 25]

structuredClone(new Set([1, 2, 3])); // => new Set([1, 2, 3])

到这里,就已经可以在项目里处理所有的兼容性代码了,但是我们可以进一步配置,让built-in类型(新api)无需像上面这样手动在代码里引入,而是像syntax类型那样,被babel-loader自动识别,并处理。

如果使用上面这种方式引入兼容性代码的弊端还有:

  • 打包体积过大,可能把目标浏览器不需要的兼容性api也导入进去了。
  • 需要手动引入,如果目标浏览器修改了,还要可能要修改代码,可维护性差。
  • syntax和built-in两种兼容内容管理混乱,前者用预设自动处理,后者手动处理。

下面的介绍就是,为了解决这些问题:

webpack项目中配置polyfills(完整)

core-js可灵活引入只需要兼容的那部分兼容性代码,例如可以细化到是否全量引入Promise上的兼容性api。如果需要人工引入会很麻烦。

@babel/preset-env预设、@babel/plugin-transform-runtime插件都附加自动引入core-js代码的功能。(开启哪个看项目而定)。

区别:

  • @babel/preset-env会修改全局对象上的prototype,适合业务系统。
  • @babel/plugin-transform-runtime则不会(无副作用),适合开发第三方库。

首先,安装依赖,并配置browserslist来指定兼容的浏览器。

@babel/plugin-transform-runtime需要依赖@babel/runtime-corejs3插件,@babel/runtime-corejs3包含了上面@babel/runtime插件的功能,还可以用来引入core-js

 npm i babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime -D

 npm i core-js@3 @babel/runtime-corejs3 @babel/runtime
// package.json
{
	// 其它package配置省略
	"browserslist": [
   		"ie 11" // 目标浏览器为ie 11
	]
}
  • 方式一:使用@babel/preset-env预设,处理新语法+引入core-js:配置预设参数useBuiltIns(是否兼容built-in类型),决定如何引入core-js模块。然后,配置corejs参数为对应core-js的版本号;同时使用@babel/plugin-transform-runtime插件处理辅助函数

    // webpack.config.js
    module.exports = {
        // ...
        module: {
            rules: [
                {
                    test: /.js$/i,
                    exclude: /node_module/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env',
                                    {
                                        useBuiltIns: 'usage',
                                        // targets: ['> 1%, last 1 version'],
                                        corejs: '3.30.2'
                                    }
                                ]
                            ],
                            // 这里只要@babel/runtime,不需要@babel/runtime-corejs3
                            plugins: ['@babel/plugin-transform-runtime']
                        }
                    }
                }
            ]
        }
    }
    
    // useBuiltIns取值:false、usage、entry
    // false: 不处理core-js(默认值)
    // usage、entry: 针对目标浏览器情况,引入需要垫平的部分代码,也就是说,
    // 如果只要兼容promise,目标浏览器为chorme或ie引入的core-js是不同的。
    
    // 它们的区别:
    
    // usage: 自动识别项目中用到的新api,自动按需引入core-js
    
    // entry: 需要手动在业务代码里引入core-js的指定兼容模块,
    // entry和纯手动自己引入区别在于,会根据目标浏览器情况,剔除不需要的部分
    /*
      entry模式下,需要在入口文件添加导入core-js的语句:
      如果没有添加,entry模式下就不会做任何built-in处理
      
      兼容目标浏览器所有ES6+ API,包括提案阶段
      import 'core-js';
      或者,只兼容针对目标浏览器,promise部分的API
      import 'core-js/es/promise'; 
    */
    
    
  • 方式二:使用@babel/plugin-transform-runtime插件处理辅助函数+引入core-js,只需要配置corejs参数即可;同时使用@babel/preset-env预设,处理新语法。

    这种方式会自动识别项目中用到的新api,进行代码转换,类似方式一的usage方式。区别在于,引入core-js的方式是,上面说的:不修改全局的对象。

    // webpack.config.js
    module.exports = {
        // ...
        module: {
            rules: [
                {
                    test: /.js$/i,
                    exclude: /node_module/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                '@babel/preset-env' // 只处理语法
                            ],
                            plugins: [
                                ["@babel/plugin-transform-runtime", 
                                    {
                                        // @babel/runtime-corejs3包含@babel/runtime的功能
                                        // 自动识别项目中用到的新api,进行代码转换
                                        corejs: {
                                            version: 3, // 需要安装 @babel/runtime-corejs3
                                            proposals: true // 使用提案阶段语法
                                        }
                                    }]
                            ]
                        }
                    }
                }
            ]
        }
    }
    

需要注意的是,因为babel-loader配置exclude属性,排除掉了第三方依赖node_modules中的代码,这些第三方依赖可能存在不兼容ie11的代码,且不会被babel-loader转译。

另外,项目中,最好还是把options里的babel参数放到项目根目录的babel.config.js中:

// babel.config.js
module.exports = {
	presets: [
       '@babel/preset-env' // 只处理语法
    ],
    plugins: [
        ["@babel/plugin-transform-runtime", 
            {
                // @babel/runtime-corejs3包含@babel/runtime的功能
                corejs: {
                    version: 3, // 需要安装 @babel/runtime-corejs3
                    proposals: true // 使用提案阶段语法
                }
            }]
    ]
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐