接下来介绍一个打包编译过程中一个极为重要的工具--babel。
细心的朋友可以知道,在之前打包编译测试都是使用简单的ES5特性,
并没有使用过ES6(ES2015+)特性(import除外)
这是因为webpack本身不会处理代码中的ES6(ES2015+)特性,所以也就没有使用。
先来做一个测试
在 /src/index.js 文件使用部分ES6(ES2015+),查看打包编译代码会发现webpack并没有处理ES6(ES2015+)特性。
自从ES6(ES2015+)时代来临后,前端才具有了飞速发展。ES6(ES2015+)各种特性也给开发人员带来了便利。
毫不客气的说,没有人再想写ES5代码了。
但是,前端代码的执行环境(浏览器)是由用户决定的,如果用户一直使用旧版本浏览器,那么新特性就无法运行在用户浏览器中。
这时候就需要一种工具:将代码使用的ES6(ES2015+)特性转换为ES5特性
这个工具就叫做:babel
?? ? webpack作为一个打包器。为babel提供了扩展支持。
?? ES6是ES2015+所有版本统称 有的文章会写成ES7、ES8。但其实都是ES6。
? 上面代码使用到了ES6的 Promise类型、块级声明(const)、箭头函数、for-of语法、数组API、await属性,不了解ES6的朋友可以学习阮一峰老师的ES6入门教程
ES6来临后,前端开启了百花绽放的时代。从而也导致了ES6转ES5的工作并不仅仅局限于JS语言的原始特性。
例如:Typescript、JSX语法等等。
这些都可以使用babel进行处理。
babel的设计思想也与webpack一致:提供核心引擎 + 插件化的模式管理
babel提供了一个核心引擎库:@babel/core 和 扩展插件库配置。
babel 其实并不是webpack一个扩展插件,它是一个独立的工具。可以进行单独配置、运行。
babel提供了一个@babel/cli库,与webpack-cli库一样,允许命令行直接运行babel
{
"scripts": {
"build": "babel src -d lib"
}
}
在此就不介绍@babel/cli这一块的内容了,有兴趣的朋友可以去官网学习
??? babel作为一个独立工具,理论可以配置在所有打包器中。
babel作为一个独立的工具,那么肯定不能直接配置在webpack中。
那么想要babel执行在webpack,就必须提供一个适配器,来桥接两个库。
而这个适配器就是babel-loader。
babel-loader在webpack执行时拦截需要转换文件,将文件先交给babel进行转换,然后再传回webpack执行接下来的操作。
而babel-loader只是调用了@babel/core库中的API。最后执行的还是@babel/core引擎
下面先安装babel-loader和@babel/core
yarn add -D babel-loader@8.2.2 @babel/core@7.13.1
然后在webpack.config.js中配置所有的js文件都使用babel-loader进行转换。
{
module:{
rules:[
{
// 所有的.js文件都走babel-loader
test:/\.js$/,
include: path.join(config.root,'src'),
loader: "babel-loader",
}
]
},
}
? babel@6.X版本时,核心引擎库名为babel-core。从babel@7.X版本之后,官方对库名称做了统一的修改,官方提供的包都以@babel/冠名,所以babel-core和@babel/core实际上是一个库 。有兴趣朋友可以在NPM中对比下两个包的版本 :@babel/core、babel-core
?后面会陆续加入其它文件执行babel-loader。例如:.ts、.jsx
但是目前依然无法转换ES6(ES2015+)代码。因为只添加了引擎(@babel/core),并没有添加具体转换库。
先来介绍一下@babel/preset-env库,来完成部分转换功能。
@babel/preset-env是babel 预设的一个plugin
yarn add -D @babel/preset-env@7.13.5
在配置loader时,可以设置当前loader使用的属性和依赖库。babel-loader具有一个presets属性来依赖的预设插件(preset)
{
module:{
rules:[
{
// 所有的.js文件都走babel-loader
test:/\.js$/,
include: path.join(config.root,'src'),
loader: "babel-loader",
options: {
presets:[
"@babel/preset-env",
]
}
}
]
}
}
?? presets的执行是从后往前执行的,官方说为了确保向后兼容
? presets配置可以设置短名称,
此时执行yarn build
操作后生成的代码就会处理部分ES6(ES2015+)
生成代码中可以看到:await、for-of、const 这些ES6代码被转换了。
? 代码中的那堆 case 语句,是await ES5的写法。await 本质只是一个 将异步同步化的状态机。不熟悉 await 机制的朋友可以忽略,只需知道代码为await语法ES5写法即可。
但细心的朋友可以发现,并不是所有的ES6特性被转换了。
还有部分ES6特性并没有被转换(promise、includes、filter),并且代码被一个箭头函数包裹着。
代码被箭头函数包裹这个问题稍后在解决。
先来了解下为什么有的ES6特性没有被转换。
? @babel/preset-env取代了preset-es20系列的预设插件(preset)
目前生成代码还无法在浏览器运行,缺少regeneratorRuntime,这个稍后再说
思考一个问题:刚才被转换的ES6特性与未被转换的ES6特性有何不同。
答案是被转换的ES6特性是Syntax(语法),而未被转换的则是:API(类型、函数)
babel处理ES6特性时将Syntax(语法)和API(类型、函数)进行了分开处理。
为什么要这样做呢?
原因是两者本质的不同:Syntax(语法)是一个语言本身客观存在的事实,而API(类型、函数),则只是对一系列操作的封装
当执行环境不支持某Syntax(语法)时,那么就只能使用其它Syntax(语法)进行替换。
而执行环境中不存在某API(类型、函数)时,可以编写自定义API(类型、函数)进行替换。
? JS中Syntax(语法)错误提示是:Uncaught SyntaxError;API(类型、函数)错误提示是:Uncaught ReferenceError。
@babel/preset-env只是babel提供处理Syntax(语法)的预设插件(preset)
至于API(类型、函数)的处理,则是由其它插件处理,这个插件俗称:垫片、腻子。
在处理API(类型、函数)之前,先介绍下babel配置文件。
刚才在配置@babel/preset-env时,直接配置在了babel-loader中presets属性。
除了babel-loader,babel还支持其它方式配置
@babel/core支持在package.json文件设置
package.json文件babel属性设置babel 插件
@babel/core执行时会尝试读取此属性。
"babel": {
"presets": [
"@babel/preset-env"
],
"plugins": [
]
}
babel支持使用配置文件设置。
这种方式与webpack.config.js文件一样,使用.约定文件名称设置。@babel/core执行时会尝试读取.约定文件。
约定文件名称 可以为 babel.config.js 或 .babelrc.json 。 较为常用的是 .babelrc.json 。不过一般都会省略后缀, 名称叫做 .babelrc
package.json形式和配置文件形式 只能选择一种形式设置。如果同时存在会直接报错。
babel-loader配置方式优先级高于其他两种方式
在使用plugin/preset时,可以设置属性。
不过参数形式有些奇葩。
plugin/preset与参数存在于一个数组内,第一个为plugin/preset,第二个为属性对象
{
"presets": [
["@babel/preset-env", {
"targets": "defaults"
}]
],
"plugins": [
]
}
??? 以下会使用配置文件方式,所以一定要把babel-loader中的设置删除掉。否则会因为优先级问题而失效。:我就因为这个疏漏曾经被耽误了一天
在转换API(类型、函数)时要进行测试。
而开发人员基本上使用的都是新版浏览器,所以需要具有一个不支持ES6API(类型、函数)的浏览器。
一般ES6的新特性,都已经不再支持IE浏览器了。所以IE浏览器是一个天然的测试对象。
例如ES6Promise类型,就不再支持IE浏览器
win 10系统携带的IE浏览器版本一般都为IE11。IE浏览器支持对版本进行修改IE浏览器
F12-开发者模式--仿真--文档模式 可以修改IE浏览器版本,在这里使用的版本为IE9
在刚才打包编译时,发现生成的代码使用了一个箭头函数包裹。
这个箭头函数函数怀疑是打包时webpack搞得鬼,具体原因没排查,在这里只介绍下处理方案。
在package.json文件中添加browserslist属性,设置打包代码支持IE9浏览器。
"browserslist": [
"ie 9"
]
? browserslist属性是browserslist库提供的一个属性,browserslist是提供浏览器版本支持的库。多个库中都依赖了browserslist。 browserslist库详情在下一篇介绍。
此时使用yarn build
执行打包编译,生成代码就不再由箭头函数包裹
介绍下关于之前打包代码缺少 regeneratorRuntime() 问题。
regeneratorRuntime() 是由regenerator-runtime库提供的,
regenerator-runtime库是一个转换ES6中 generator函数、await函数 功能的库。babel直接使用此库处理两种函数。
很多文章介绍时regenerator-runtime都与core-js一起介绍。所以在此也将这两个库放在一起介绍。
处理ES6 API(类型、函数)的解决方案在上面介绍过。
当执行环境中不存在某API(类型、函数)时,可以使用自定义API(类型、函数)进行替代。
而core-js库就是一个自定义的API(类型、函数)库。也就是俗称的腻子
core-js是 个人开源项目,并不属于任何公司。
babel直接使用了core-js进行处理API(类型、函数)
core-js截至到编写文章时的最新版本为@3.9.0
core-js的@3.X与@2.X两个大版本间具有巨大的差异性,以至于影响到了babel。不过目前基本都是使用core-js@3.X版本。
? core-js开发者目前在开发core-js@4.X版本。可能到时候配置又会具有大变化。
关于babel的文章中,有很多都会介绍@babel/polyfill。
@babel/polyfill库其实就是babel对core-js和regenerator-runtime的封装库。
不过在babel官网,这个库已经被弃用了。babel@7.4.0版本之后就建议直接使用core-js和regenerator-runtime
上面那段话的大致意思为:@babel@7.4.0开始,@babel/polyfill会被弃用,直接使用core-js和regenerator-runtime。
下面那段话的大致意思为:babel具有一个polyfill包含了core-js和regenerator-runtime。
??? 关于@babel/polyfill库被弃用的原因好像是因为:core-js@3.X版本和core-js@2.X版本的巨大差异 导致@babel/polyfill无法过渡适配。
yarn add regenerator-runtime@0.13.7 core-js@3.9.0 // 安装在dependencies
直接使用core-js和regenerator-runtime需要在代码中手动引用。babel当然也支持配置,慢慢来
index.js文件引用。
??
此时执行yarn build
打包 编译生成代码中会看到好多引用代码。这些都是core-js处理ES6 API(类型、函数)的垫片
例如promise类型,就可以在编译生成后的代码中找到core-js自定义的实现方式。
这时候使用IE9运行代码可以运行成功,也就是说ES6 API(类型、函数)被成功替代了。
刚才加入core-js和regenerator-runtime后打包运行,可以知道ES6 API(类型、函数)被成功替代了。
但其实这里还具有一个非常严重的问题,那就是文件大小。
可以看到打包生成的文件现在高达428K。虽然打包代码压缩,但也不应该这个大小
在代码中仅写了两个函数。那么原因大概是引入core-js和regenerator-runtime导致。
core-js是ES6 API(类型、函数)的垫片。
core-js本身并不知道你使用哪些ES6 API(类型、函数),而babel默认情况会将所有的垫片引入,
也就造成了这个恐怖的文件大小
前端对于文件大小非常敏感,文件大小直接影响到网站的加载速度。所以必须要做到按需加载垫片 (仅加载需要使用的垫片)
不同项目对浏览器支持版本需求不一样。
babel处理ES6 API(类型、函数)垫片时的按需加载垫片具有三种含义
?浏览器支持版本需求 取决于项目的使用用户,例如有的项目只是公司管理项目,无须兼容老版本浏览器
babel中@babel/preset-env提供了两种按需加载配置方案:按照浏览器版本加载(1)和按照浏览器版本+代码中使用加载(3)
@babel/preset-env 属性配置
按需加载垫片中有一个浏览器版本加载的含义,想要实现浏览器版本加载那就必须设置浏览器版本,
babel提供了两种设置浏览器版本的方案:
browserslist
browserslist方案在刚才处理函数包裹代码时使用到了,设置在package.json中的browserslist属性
"browserslist": [
"ie 9"
]
browserslist是一个提供浏览器版本的一个库,提供了多种配置规则,好多库都使用到了browserslist,例如:babel。
browserslist属性是Array,允许设置多个浏览器版本。例如ie 9,便是支持IE9浏览器。
还可以设置范围版本,例如大于Chrome75版本。
"browserslist": [
"Chrome > 75"
]
在这里只使用这两种规则测试,browserslist会在下一篇介绍
targets
targets属性是babel自身提供浏览器版本设置,配置在@babel/preset-env属性中
targets属性类型为 String、Object;支持browserslist格式规则。
targets属性的优先级高于browserslist。
{
"presets": [
["@babel/preset-env",{
"targets": "chrome > 75",
}]
],
"plugins": [
]
}
{
"presets": [
["@babel/preset-env",{
"targets": {
"chrome": "58",
"ie": "11"
}]
],
"plugins": [
]
}
推荐使用browserslist设置,也就是package.json中browserslist属性。
因为browserslist库已经被社区高度认可。好多库都依赖了browserslist,使用browserslist库可以做到:配置统一管理,利于项目维护
?:?? 浏览器版本设置也会影响Syntax(语法)的转换。 指定的浏览器版本支持的Syntax(语法)不会被转换ES5
在介绍按需加载垫片之前再说一个@babel/preset-env属性:corejs
corejs属性是babel@7.4.0时加入的,用于设置加载core-js版本。
corejs设置的类型为: String、Object。
{
"presets": [
["@babel/preset-env",{
"corejs": {
"version": "3.9",
"proposals":true
}
}]
],
"plugins": [
]
}
?? corejs属性只有在启用按需加载垫片(useBuiltIns设置为entry、usage才有效。
按需加载垫片是由@babel/preset-env库提供的useBuiltIns属性设置。
useBuiltIns属性可以设置三个属性值:
false
不启用按需加载垫片功能,全部加载core-js垫片。此值为默认值。
entry
按照浏览器版本加载垫片。
{
"presets": [
["@babel/preset-env",{
"useBuiltIns": "entry",
"corejs": {
"version": "3.9",
"proposals":true
}
}]
],
"plugins": [
]
}
browserslist属性为 Chrome > 75 时 打包出来的文件大小就会小很多
"browserslist": [
"Chrome > 75"
]
可以看到,此时文件大小与刚才是天壤之别。因为浏览器设置的为 Chrome > 75 ,几乎支持全部新特性
可以看到打包生成代码中没有提供filter垫片,并且 await 语法都没有转换。这些特性在新版Chrome都提供了。
如果将browserslist属性设置为 ie 9
那么文件大小依然会很大。因为ES6 新特性都不支持IE 9
"browserslist": [
"ie 9"
]
usage
刚才使用entry属性值实现了按照浏览器版本加载垫片的功能。
不过并不能算是我们需要的真正按需加载垫片。
useBuiltIns属性的usage值提供了理论上真正的按需加载:浏览器版本+代码中使用
{
"presets": [
["@babel/preset-env",{
"useBuiltIns": "usage",
"corejs": {
"version": "3.9",
"proposals":true
}
}]
],
"plugins": [
]
}
在使用usage属性值时,就不需要手动引用core-js和regenerator-runtime库了
babel会自动加载。
此时哪怕设置ie 9。打包文件大小也不会像entry时那么大了。
"browserslist": [
"ie 9"
]
而在Chrome > 75的情况下,代码都不需要进行处理了
"browserslist": [
"Chrome > 75"
]
entry、usage有话说
@babel/preset-env配置项中有一个modules。
modules属性表示是否将ES modules转换为指定模块类型处理。
modules属性值具有:amd、systemjs、umd、commonjs、cjs、auto、false。
默认属性值为auto:默认情况下,使用ES modules来进行处理,但是会受到其它plugin的modules属性影响。
推荐使用ES modules,将属性值设置为false
因为ES6 modules 可以进行tree-shaking优化
{
"presets": [
[
"@babel/preset-env",
{
"modules":false
}
]
]
}
@babel/preset-env还有一些别的属性,在此就不赘述。有兴趣的朋友可以查询官网。
babel处理ES6特性时,还提供了一个解决全局污染的垫片库:@babel/plugin-transform-runtime
@babel/plugin-transform-runtime也是一个经常被使用到的库。
在日常开发中都应该遵守的一个原则:避免全局污染。
全局污染是一件极为可怕的问题。在协同、代码运行时会出现不可预知的问题。
@babel/plugin-transform-runtime库就是将代码使用到的ES6 API(类型、函数)名称转换为自定义名称,从而避免污染运行环境自身API。
? @babel/plugin-transform-runtime与usage属性值一样:按照浏览器版本+代码中使用加载垫片
开发第三方库,强烈建议使用@babel/plugin-transform-runtime
@babel/plugin-transform-runtime库依赖了一个@babel/runtime-corejs3或@babel/runtime-corejs2库。
??? @babel/runtime-corejs3对应的core-js@3.X @babel/runtime-corejs2对应的core-js@2.X
@babel/runtime-corejs3是babel提供的core-js封装库,内部做了一些处理,具体可以参考这篇文章。不过此文章是基于@babel/runtime-corejs2版本,与@babel/runtime-corejs3具有一定差异。
yarn add -D @babel/plugin-transform-runtime@7.13.7 @babel/runtime-corejs3@7.13.7
?? 使用@babel/plugin-transform-runtime时,就不需要安装core-js和regenerator-runtime ,@babel/runtime-corejs3中会依赖这两个库
.babelrc文件中使用@babel/plugin-transform-runtime配置替代@babel/preset-env中配置。
不过注意的是@babel/plugin-transform-runtime属性中corejs.version不再是字符串,而是2、3。 因为加载的是@babel/runtime-corejs[3/2]
{
"presets": [
[
"@babel/preset-env",
{
// 移除useBuiltIns设置
// "targets": "chrome > 75",
// "useBuiltIns": "usage",
// "corejs": {
// "version": "3.9",
// "proposals":true
// }
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
}
}
]
]
}
配置完毕后,不再需要任何引用就可以进行打包生成。
"browserslist": [
"ie 9"
]
在IE9环境yarn build
。
可以看到使用的ES6-API已经被转换为另外的API了,所以并不会再污染全局代码。至于打包的大小,并没有多大
至于在Chrome > 75的打包结果,有兴趣的朋友可以自行测试。
在使用babel库时,发现有两种类型:
配置时也是不同属性:
{
"presets": [
],
"plugins": [
]
}
preset的中文翻译为:预置。其实也就是babel提供的预置插件库,其本质也都是plugin
???
{
"name": "my-cli",
"version": "1.0.0",
"main": "index.js",
"author": "mowenjinzhao<yanzhangshuai@126.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "7.13.1",
"@babel/plugin-transform-runtime": "7.13.7",
"@babel/preset-env": "7.13.5",
"@babel/runtime-corejs3": "7.13.7",
"babel-loader": "8.2.2",
"clean-webpack-plugin": "3.0.0",
"html-webpack-plugin": "5.2.0",
"webpack": "5.24.0",
"webpack-cli": "4.5.0"
},
"dependencies": {
"jquery": "3.5.1",
},
"scripts": {
"start": "webpack --mode=development --config webpack.config.js",
"build": "webpack --mode=production --config webpack.config.js"
},
"browserslist": [
"ie 9",
"Chrome > 75"
]
}
const path = require('path')
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const config = {
root: path.join(__dirname, './'),
}
const modules = {
// 入口文件
// 字符串形式
entry: path.join(config.root, 'src/index.js'),
// 对象形式
// entry:{
// 'index': path.join(config.root, 'src/index.js'),
// },
// 输出文件
// 字符串形式
// output:path.join(config.root, './dist/[name].js')
//对象形式
output: {
// 输出文件的目录地址
path: path.join(config.root, 'dist'),
// 输出文件名称,contenthash代表一种缓存,只有文件更改才会更新hash值,重新打包
filename: '[name]_[contenthash].js'
},
//devtool:false, //'eval'
module:{
rules:[
{
// 所有的.js文件都走babel-loader
test:/\.js$/,
include: path.join(config.root,'src'),
loader: "babel-loader"
}
]
},
optimization: {
minimize: false,
minimizer: [
new TerserPlugin({
// 指定压缩的文件
include: /\.js(\?.*)?$/i,
// 排除压缩的文件
// exclude:/\.js(\?.*)?$/i,
// 是否启用多线程运行,默认为true,开启,默认并发数量为os.cpus()-1
// 可以设置为false(不使用多线程)或者数值(并发数量)
parallel: true,
// 可以设置一个function,使用其它压缩插件覆盖默认的压缩插件,默认为undefined,
minify: undefined,
// 是否将代码注释提取到一个单独的文件。
// 属性值:Boolean | String | RegExp | Function<(node, comment) -> Boolean|Object> | Object
// 默认为true, 只提取/^\**!|@preserve|@license|@cc_on/i注释
// 感觉没什么特殊情况直接设置为false即可
extractComments: false,
// 压缩时的选项设置
terserOptions: {
// 是否保留原始函数名称,true代表保留,false即保留
// 此属性对使用Function.prototype.name
// 默认为false
keep_fnames: false,
// 是否保留原始类名称
keep_classnames: false,
// format和output是同一个属性值,,名称不一致,output不建议使用了,被放弃
// 指定压缩格式。例如是否保留*注释*,是否始终为*if*、*for*等设置大括号。
format: {
comments: false,
},
output: undefined,
// 是否支持IE8,默认不支持
ie8: false,
compress: {
// 是否使用默认配置项,这个属性当只启用指定某些选项时可以设置为false
defaults: false,
// 是否移除无法访问的代码
dead_code: false,
// 是否优化只使用一次的变量
collapse_vars: true,
warnings: true,
// 是否删除所有 console.*语句,默认为false,这个可以在线上设置为true
drop_console: false,
// 是否删除所有debugger语句,默认为true
drop_debugger: true,
// 移除指定func,这个属性假定函数没有任何副作用,可以使用此属性移除所有指定func
// pure_funcs: ['console.log'], //移除console
},
},
})
]
},
plugins: [
new HtmlWebpackPlugin({
// HTML的标题,
// template的title优先级大于当前数据
title: 'my-cli',
// 输出的html文件名称
filename: 'index.html',
// 本地HTML模板文件地址
template: path.join(config.root, 'src/index.html'),
// 引用JS文件的目录路径
publicPath: './',
// 引用JS文件的位置
// true或者body将打包后的js脚本放入body元素下,head则将脚本放到中
// 默认为true
inject: 'body',
// 加载js方式,值为defer/blocking
// 默认为blocking, 如果设置了defer,则在js引用标签上加上此属性,进行异步加载
scriptLoading: 'blocking',
// 是否进行缓存,默认为true,在开发环境可以设置成false
cache: false,
// 添加mate属性
meta: {}
}),
new CleanWebpackPlugin({
// 是否假装删除文件
// 如果为false则代表真实删除,如果为true,则代表不删除
dry: false,
// 是否将删除日志打印到控制台 默认为false
verbose: true,
// 允许保留本次打包的文件
// true为允许,false为不允许,保留本次打包结果,也就是会删除本次打包的文件
// 默认为true
protectWebpackAssets: true,
// 每次打包之前删除匹配的文件
cleanOnceBeforeBuildPatterns: ['**/*'],
// 每次打包之后删除匹配的文件
cleanAfterEveryBuildPatterns:["*.js"],
}),
new webpack.DefinePlugin({ "global_a": JSON.stringify("我是一个打包配置的全局变量") }),
],
resolve: {
alias:{
// 设置路径别名
'@': path.join(config.root, 'src') ,
'~': path.join(config.root, './src/assets') ,
},
// 可互忽略的后缀
extensions:['.js', '.json'],
// 默认读取的文件名
mainFiles:['index', 'main'],
}
}
// 使用node.js的导出,将配置进行导出
module.exports = modules
{
"presets": [
[
"@babel/preset-env",
{
"modules":false
// 移除useBuiltIns设置
// "targets": "chrome > 75",
// "useBuiltIns": "usage",
// "corejs": {
// "version": 3,
// "proposals":true
// }
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
}
}
]
]
}