为什么要处理ES6语法呢?当我们使用新语法来写代码,一些浏览器如果不支持那么就会报错,导致用户体验非常糟糕。我们使用babel来将ES6语法处理成ES5语法,就可以解决这个问题。
Babel官网{:target=”_blank”}
1.安装Babel
yarn add babel-loader @babel/core
webpack.config.js
安装@babel/preset-env
yarn add @babel/preset-env
.babelrc
{
“presets”: [“@babel/preset-env”]
}
src/index.js
package.json
{
“scripts”: {
“dev”: “npx webpack-dev-server –mode=development –colors”,
“dist”: “npx webpack –mode=production”,
// “./src/index.js”:
/!******!
! ./src/index.js !
*******/
/! no static exports found */
/**/ (function(module, exports) {
eval(“var arr = [new Promise(function () {}), new Promise(function () {})];\narr.map(function (val) {\n console.log(val);\n});\n\n//# sourceURL=webpack:///./src/index.js?”);
/***/ })
我们看到ES6语法被转换成了ES5语法,但是只转换了一部分,比如在低版本浏览中还是识别不了Promise、Map这样的语法。
2.兼容低版本浏览器
安装babel/polyfill
yarn add @babel/polyfill
src/index.js
arr.map(val => {
console.log(val)
})
运行webpack
yarn run build
从webpack打包输出的信息中可以看到,Promise和Map方法@babel/polyfill自己帮我们做了实现。可是dist/mian.js文件突然增大到941kb,原因就是
main.js文件将整个polyfill都打包进来。我们想要的其实在main.js中用到的方法帮助我们实现,如果没有用到,就不需要打包进main.js。
3.精简代码
.babelrc
{
“presets”: [
[
“@babel/preset-env”,
4.设置浏览器版本
.babelrc
{
“presets”: [
[
“@babel/preset-env”,
运行webpack
yarn run build
5.插件开发
上面教程如果我们开发项目,完全够用了。但是当我们开发插件时就会有问题,因为上面是将promis等新特性的实现通过全局变量来注入,会污染全局环境。而且写代码还要在js前面添加import “@babel/polyfill”。所以小菜建议下面这种方式
安装pluginTransformRuntime
yarn add @babel/plugin-transform-runtime
yarn add @babel/runtime
yarn add @babel/runtime-corejs2
.babelrc
{
“presets”: [
[
“@babel/preset-env”,
{
“targets”: {
“chrome”: “67”
},
“useBuiltIns”: “usage”
}
]
],
// “./src/index.js”:
/!******!
! ./src/index.js !
*******/
/! no exports provided */
/**/ (function(module, webpack_exports, webpack_require) {
“use strict”;
eval(“webpack_require.r(webpack_exports);\n/* harmony import / var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0__ = webpack_require(/! @babel/runtime-corejs2/core-js/promise / "./node_modules/@babel/runtime-corejs2/core-js/promise.js");\n/ harmony import / var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0_default = /*#__PURE/webpack_require.n(babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0);\n\nconst arr = [new _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0_default.a(() => {}), new _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0__default.a(() => {})];\narr.map(val => {\n console.log(val);\n});\n\n//# sourceURL=webpack:///./src/index.js?”);
/***/ })
从打包的代码我们可以看到,向Promise和Map都帮助我们实现了。
js模块化进程的起因
现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。当一个项目开发的越来越复杂的时候,你会遇到一些问题:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。
JavaScript发展的越来越快,超过了它产生时候的自我定位。这时候js模块化就出现了。
什么是模块化
模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分成许多部分单独开发,然后再组装起来,每一个部分即为模块。当使用模块化开发的时候可以避免刚刚的问题,并且让开发的效率变高,以及方便后期的维护。
js模块化进程
一、早期:script标签
这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中。
缺点:
1.污染全局作用域
2.只能按script标签书写顺序加载
3.文件依赖关系靠开发者主观解决
二、发展一:CommonJS规范
允许模块通过require方法来同步加载(同步意味阻塞)所要依赖的其他模块,然后通过module.exports来导出需要暴露的接口。
// module add.js
module.exports = function add (a, b) { return a + b; }
// main.js
var {add} = require(‘./math’);
console.log(‘1 + 2 = ‘ + add(1,2);
CommonJS 是以在浏览器环境之外构建JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。
三、发展二:AMD/CMD
(1)AMD
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出(异步模块定义)。
AMD标准中定义了以下两个API:
require([module], callback);
define(id, [depends], callback);
require接口用来加载一系列模块,define接口用来定义并暴露一个模块。
define(['./a', './b'], function(a, b) {
// 依赖必须一开始就写好
a.add1()
...
b.add2()
...
})
优点:
1、适合在浏览器环境中异步加载模块 2、可以并行加载多个模块
(2)CMD
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。(在CommomJS和AMD基础上提出)
define(function (requie, exports, module) {
//依赖可以就近书写
var a = require(‘./a’);
a.add1();
…
if (status) {
var b = requie(‘./b’);
b.add2();
}
});
优点:
1、依赖就近,延迟执行 2、可以很容易在服务器中运行
(3)AMD 和 CMD 的区别
AMD和CMD起来很相似,但是还是有一些细微的差别:
1、对于依赖的模块,AMD是提前执行,CMD是延迟执行。
2、AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require。
3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
四、发展三:ES6模块化
EcmaScript6 标准增加了JavaScript语言层面的模块体系定义。
在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。
// module math.jsx
export default class Math extends React.Component{}
// main.js
import Math from “./Math”;
目前很少JS引擎能直接支持 ES6 标准,因此 Babel 的做法实际上是将不被支持的import翻译成目前已被支持的require。
ES6详解八:模块(Module)
基本用法
命名导出(named exports)
可以直接在任何变量或者函数前面加上一个 export 关键字,就可以将它导出。
这种写法非常简洁,和平时几乎没有区别,唯一的区别就是在需要导出的地方加上一个 export 关键字。
比如:
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
然后在另一个文件中这样引用:
import { square, diag } from ‘lib’;
console.log(square(11)); // 121
console.log(diag(4, 3));
你可能会注意到这个奇怪的语法 { square, diag } 不就是前面讲过的 destructing吗。所以你会以为还可以这样写:
import lib from ‘lib’;
square = lib.square;
但是其实这样是错的,因为 import { square, diag } from ‘lib’; 是import的特有语法,并不是 destructing 语法,所以其实import的时候并不是直接把整个模块以对象的形式引入的。
如果你希望能通过 lib.square 的形式来写,你应该这样导入:
import * as lib from ‘lib’;
square = lib.square;
不过值得注意的一点是,如果你直接用babel编译,执行是会报错的。因为 babel 并不会完全编译 modules,他只是把 ES6 的modules语法编译成了 CMD 的语法,所以还需要用 browserify 之类的工具再次编译一遍。
如果你发现 browserify 找不到 lib,可以改成 from ‘./lib’ 试试。
默认导出
大家会发现上面的写法比较麻烦,因为必须要指定一个名字。其实很多时候一个模块只导出了一个变量,根本没必要指定一个名字。
还有一种用法叫默认导出,就是指定一个变量作为默认值导出:
//—— myFunc.js ——
export default function () { … };
//—— main1.js ——
import myFunc from ‘myFunc’;
myFunc();
默认导出的时候不需要指定一个变量名,它默认就是文件名。
这里的区别不仅仅是不用写名字,而是 导出的默认值就是模块本身,而不是模块下面的一个属性,即是 import myFunc from ‘myFunc’; 而不是 import {myFunc} from ‘myFunc’;
命名导出结合默认导出
默认导出同样可以结合命名导出来使用:
export default function (obj) {
…
};
export function each(obj, iterator, context) {
…
}
export { each as forEach };
上面的代码导出了一个默认的函数,然后由导出了两个命名函数,我们可以这样导入:
import _, { each } from ‘underscore’;
注意这个逗号语法,分割了默认导出和命名导出
其实这个默认导出只是一个特殊的名字叫 default,你也可以就直接用他的名字,把它当做命名导出来用,下面两种写法是等价的:
import { default as foo } from ‘lib’;
import foo from ‘lib’;
同样的,你也可以通过显示指定 default 名字来做默认导出, 下面两种写法是一样的:
//—— module1.js ——
export default 123;
//—— module2.js ——
const D = 123;
export { D as default };
仅支持静态导入导出
ES6规范只支持静态的导入和导出,也就是必须要在编译时就能确定,在运行时才能确定的是不行的,比如下面的代码就是不对的:
//动态导入
var mylib;
if (Math.random()) {
mylib = require(‘foo’);
} else {
mylib = require(‘bar’);
}
//动态导出
if (Math.random()) {
exports.baz = …;
}
为什么要这么做,主要是两点:
性能,在编译阶段即完成所有模块导入,如果在运行时进行会降低速度
更好的检查错误,比如对变量类型进行检查
各种导入和导出方式总结
总结一下,ES6提供了如下几种导入方式:
// Default exports and named exports
import theDefault, { named1, named2 } from ‘src/mylib’;
import theDefault from ‘src/mylib’;
import { named1, named2 } from ‘src/mylib’;
// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from ‘src/mylib’;
// Importing the module as an object
// (with one property per named export)
import * as mylib from ‘src/mylib’;
// Only load the module, don’t import anything
import ‘src/mylib’;
如下几种导出方式:
//命名导出
export var myVar1 = …;
export let myVar2 = …;
export const MY_CONST = …;
export function myFunc() {
…
}
export function* myGeneratorFunc() {
…
}
export class MyClass {
…
}
// default 导出
export default 123;
export default function (x) {
return x
};
export default x => x;
export default class {
constructor(x, y) {
this.x = x;
this.y = y;
}
};
//也可以自己列出所有导出内容
const MY_CONST = …;
function myFunc() {
…
}
export { MY_CONST, myFunc };
//或者在导出的时候给他们改个名字
export { MY_CONST as THE_CONST, myFunc as theFunc };
//还可以导出从其他地方导入的模块
export * from ‘src/other_module’;
export { foo, bar } from ‘src/other_module’;
export { foo as myFoo, bar } from ‘src/other_module’;
浅谈webpack打包原理
模块化机制
webpack并不强制你使用某种模块化方案,而是通过兼容所有模块化方案让你无痛接入项目。有了webpack,你可以随意选择你喜欢的模块化方案,至于怎么处理模块之间的依赖关系及如何按需打包,webpack会帮你处理好的。
关于模块化的一些内容,可以看看我之前的文章:js的模块化进程
核心思想:
一切皆模块:
正如js文件可以是一个“模块(module)”一样,其他的(如css、image或html)文件也可视作模 块。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。这意味着我们可以将事物(业务)分割成更小的易于管理的片段,从而达到重复利用等的目的。
按需加载:
传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。但是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会导致应用一直处于加载中状态。因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载。
文件管理
每个文件都是一个资源,可以用require/import导入js
每个入口文件会把自己所依赖(即require)的资源全部打包在一起,一个资源多次引用的话,只会打包一份
对于多个入口的情况,其实就是分别独立的执行单个入口情况,每个入口文件不相干(可用CommonsChunkPlugin优化)
打包原理
把所有依赖打包成一个bundle.js文件,通过代码分割成单元片段并按需加载。
如图,entry.js是入口文件,调用了util1.js和util2.js,而util1.js又调用了util2.js。
打包后的bundle.js例子
/**/ ([
/* 0 / //模块id
/**/ function(module, exports, webpack_require) {
__webpack_require__(1); //require资源文件id
__webpack_require__(2);
// },
/ 1 /
// function(module, exports, webpack_require) {
//util1.js文件
webpack_require(2);
var util1=1;
exports.util1=util1;
// },
/ 2 /
// function(module, exports) {
//util2.js文件
var util2=1;
exports.util2=util2;
// }
…
…
/***/ ]);
bundle.js是以模块 id 为记号,通过函数把各个文件依赖封装达到分割效果,如上代码 id 为 0 表示 entry 模块需要的依赖, 1 表示 util1模块需要的依赖
require资源文件 id 表示该文件需要加载的各个模块,如上代码_webpack_require__(1) 表示 util1.js 模块,webpack_require(2) 表示 util2.js 模块
exports.util1=util1 模块化的体现,输出该模块
ECMAScript6 实现了很多强大的新特性,借助 ES6 我们能用更加优雅的方式完成许多强大的功能。只是鉴于许多老版本的浏览器尚未支持 ES6 语法,需要在使用之前转换为 ES5 语法,以使其兼容更多的浏览器。而完成这些转换工作的就是 Babel 了。
Babel 本质就是一个 JavaScript 编译器,通过:
将 JavaScript 源代码解析成抽象语法树(AST);
将源代码的 AST 结果一系列转换生成目标代码的 AST;
将目标代码的 AST 转换成 JavaScript 代码。
就可以完成 ES6 代码到 ES5 代码的转换,当然转换的过程会很复杂,我们在这里先了解一下基本的原理。想深入了解的同学可以通过开发自己的 Babel Plugin,熟悉 AST 的操作流程。
Babel 本身的安装使用是很简单的,针对我们当前的应用,我们可以通过如下过程实现:
安装 babel-core 包:cnpm i –save-dev babel-core;
新建一个 Babel 测试文件 babelTest.js 并使用 babel-core 转换 ES6 代码(我们使用了 ES6 的箭头函数):
babelTest.js
图片描述
安装上一步中使用的 babel-preset-env 和 babel-preset-stage-0 包:cnpm i –save-dev babel-preset-env babel-preset-stage-0;(babel-preset-env 是一个主流的 Babel 插件数组;Stage-X 是实验阶段的 Presets,)
TC39 将提案分为以下几个阶段:
Stage 0 - 稻草人: 只是一个想法,可能是 babel 插件。
Stage 1 - 提案: 初步尝试。
Stage 2 - 初稿: 完成初步规范。
Stage 3 - 候选: 完成规范和浏览器初步实现。
Stage 4 - 完成: 将被添加到下一年度发布。
在当前目录执行 babelTest.js 文件:node babelTest.js,控制台输出信息:
图片描述
可以看到我们的 ES6 箭头函数被转换为了 ES5的 ‘(function() {})’。
了解了 Babel 的基本工作原理,现在让我们用 ES6 的新特性,稍微改写一下我们的 index.js:
index.js
图片描述
在这里我们用了 ES6 的模板字符串和箭头函数。模板字符串通过用反引号(`)标识字符串,可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
在没有配置 Babel 的情况下,我们通过 webpack 命令打包,可以发现生成的 test.js 文件还是用的 ES6 语法:
test.js
图片描述
注:截图部分是 test.js 的最后参数部分。
下面让我们看一下如何将 Babel 结合到 webpack 中,来实现 ES6 代码到 ES5 代码的转换。
四、webpack + Babel 构建 ES6 开发平台
既然要将 webpack 和 Babel 结合在一起,就需要在两者之间建立一条纽带,而通过 webpack 的 loaders 就可以生成这条纽带,现在让我们修改我们的 webpack.config.js 配置文件:
webpack.config.js
图片描述
可以看到我们在这里加了一段 module.rules 配置项,rules 数组里的每一项就是一条 loader 使用规则,loader 用于对不同类型文件的源代码进行转换,可以使你在 import 或”加载”模块时预处理文件。
现在我们配置的第一条规则,就是针对以 .js 结尾的文件使用 babel-loader。由于现在我们的项目中还不存在 babel-loader,让我们先通过 cnpm 安装该模块: cnpm i –save-dev babel-loader。
现在我们已经准备好了 webpack、webpack 和 Babel 的纽带,接下来就需要准备 Babel 的相关配置了。
在上一节已经介绍了单独使用 Babel 的实现方法,但是在 webpack 中一般情况下我们不会主动调用 babel-core 解析 ES6 代码,而是通过 babel-loader 在 webpack 编译过程中自动解析 ES6 代码。那么现在的问题就是在上一节使用 babel-core 的过程中,我们使用了 env 和 stage-0 两个 preset,现在不使用 babel-core 了,这两个 preset 又应该在哪里配置?
这里我们有两种方式可以实现上述配置:
使用 Babel 提供的方法:通过 Babel 目录下的 .babelrc 配置文件完成配置(直接创建 .babelrc 文件可能会有命名规范的问题,可以通过 VS Code、Sublime 等编辑器创建该文件)。这里的 .babelrc 就类似于 webpack.config.js 的作用,只是 .babelrc 文件是在 babel-loader 执行的过程中使用的。
.babelrc
图片描述
使用 webpack 提供的方法:在 webpack.config.js 的 module.rules 规则中,我们还可以通过使用 loader 语法配置 Babel 的 presets:
webpack.config.js
图片描述
我们采用第一种方式配置 .babelrc 文件,项目目录结构如下:
图片描述
现在让我们在当前目录下执行 webpack 命令并查看生成的 test.js 文件,可以发现我们的 index.js 已经被转换成 ES5 代码了:
https://webpack.js.org/