从模块化到认识Babel
转载自:https://www.cnblogs.com/qcloud1001/p/10167756.html
https://blog.csdn.net/a250758092/article/details/78543440
1.模块化
模块化是指把一个复杂的系统分解到一个一个的模块。
模块化开发的优点:
(1)代码复用,让我们更方便地进行代码管理、同时也便于后面代码的修改和维护。
(2)一个单独的文件就是一个模块,是一个单独的作用域,只向外暴露特定的变量和函数。这样可以避免污染全局变量,减少变量命名冲突。
js模块化规范有:CommonJS、AMD、CMD、esm的模块系统。
1.1 AMD (现在很少用了)
AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是require.js(还有个js库:curl.js)
// 定义一个模块 define('module', ['dep'], function (dep) { return exports; }); // 导入和使用 require(['module'], function (module) { });
1.2 CommonJS 规范
是服务器端模块的规范,由nodejs推广使用。该规范的核心思想是:允许模块通过require方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。
// 导入 const moduleA = require('./moduleA');
// 导出
module.exports = moduleA.someFunc;
- require的模块第一次加载时候会被执行,导出执行结果
module.exports
。 - require的模块如果曾被加载过,再次加载时候模块内部代码不会再次被执行,直接导出首次执行的结果。
require函数是运行时执行的,所以require函数可以接收表达式,并且可以放在逻辑代码中执行。
const name = 'Tom';
const scriptName = 'tom.js';
if (name === 'Tom') {
require('./' + scriptName);
}
1.3 ES Module模块规范
在 ES6 中,使用export关键字来导出模块,使用import关键字引用模块。但是浏览器还没有完全兼容,需要使用babel转换成浏览器支持的代码。正是由于Babel的存在,前端开发者才能能够不用考虑浏览器兼容性、畅快淋漓地使用最新的JavaScript语言特性。
// 导出 export function hello() { }; export default { // ... }; // 导入 import { readFile } from 'fs'; import React from 'react';
使用import导入模块时,需要知道要加载的变量名或函数名。
在ES6中还提供了export default,为模块指定默认导出(模块的默认导出只能有一个)。对应导入默认模块import时,不需要使用大括号。
小结:在导入模块时候,CommonJS是导出值的拷贝并缓存,而在ES6 Module中是值的动态引用。
CommonJS模块是动态引入的,模块依赖关系的确认发生在代码运行时;而ES6 Module模块是静态引入的,模块的依赖关系在编译时已经可以确立。
CommonJS require
函数可以在index.js
任何地方使用,并且接受的路径参数也可以动态指定。因此,在CommonJS模块被执行前,是没有办法确定明确的依赖关系,模块的导入导出都发生在代码运行时(代码运行阶段)。
ES6 Module的导入、导出语句都是声明式的,它不支持模块路径使用表达式,并且也要求导入、导出语句位于模块的顶层作用域。因此ES6 Module是一种静态的模块结构,在ES6代码编译阶段就可以分析出模块的依赖关系。ES6 Module对比CommonJS有以下优势:
tree shaking
。通过静态分析工具在编译时候检测哪些import
进来的模块没有被实际使用过,以及模块中哪些变量、函数没有被使用,都可以在打包前先移除,减少打包体积。- 模块变量检查。JavaScript属于动态语言,不会在代码执行前检查类型错误。ES6 Module的静态模块结构有助于结合其他工具在开发或编译过程中去检查值类型是否正确。
2. Babel
Babel 是一个 JavaScript 编译器。他把最新版的javascript编译成当下可以执行的版本.
在1.3中,我们提到了由于(低版本IE)浏览器兼容问题,需要使用Babel将es6编译成es5。官方给出的定义是,将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
实际上babel转换后的代码是遵循commonJS规范的,而这个规范,浏览器(支持的是 ECMAScript)并不能识别。因此导入到浏览器中会报错,而nodeJS是commonJS的实现者,所以在babel转换后的代码是可以在node中运行的。
为了将babel生成的遵循commonJS规范的es5写法能够在浏览器上直接运行,我们就借助webpack这个打包工具来完成。【概括一下:流程是 es6->es5(commonJS规范)->浏览器可执行代码】
2.1 需要安装的依赖
-
@babel/core:babel的核心包,核心的api都在这里。
-
@babel/cli :通过命令行运行babel.
此外,babel进行代码编译时需要指定转换规则,也就是babel的plugin或者preset(一组预先设定的插件plugin)
- @babel/preset-env 将高版本es转成es5
- @babal/preset-react jsx语法转译
- plugin
- @babel/polyfill:deprecated in 7.4.0.(官方现在推荐使用core-js,并在安装时指定好大版本) 相当于一个填充,因为@babel/preset-env本身只支持转换箭头函数、结构赋值这些语法糖类的语法,而Polyfill中包含了Promise函数等新的特征。
- 当存在多个presets和多个plugins时的优先级(执行顺序):
2.2 Babel配置文件的选择
之前版本的babel都是使用.baberc
来做配置文件,babel7引入了babel.config.js
。但是它并不是.baberc
的替代品,二者根据使用的场景不同自行选择。
.babelrc
{
"presets": ["@babel/preset-flow","@babel/preset-react", "@babel/preset-typescript"],
"plugins": [...]
}
或者在package.json中添加babel配置对象
{ name: '...', version: 0.0.1, scripts: { //... } babel: { presets: ['@babel/preset-env'] } }
babel.config.js(新的) env的参数配置,https://babeljs.io/docs/en/babel-preset-env#options
module.exports = function () {
const presets = [
["env", {
"targets": { //指定要转译到哪个环境
//浏览器环境
"browsers": ["last 2 versions", "safari >= 7"],
//node环境
"node": "6.10", //"current" 使用当前版本的node
},
//是否将ES6的模块化语法转译成其他类型
//参数:"amd" | "umd" | "systemjs" | "commonjs" | false,默认为'commonjs'
"modules": 'commonjs',
//是否进行debug操作,会在控制台打印出所有插件中的log,已经插件的版本
"debug": false,
//强制开启某些模块(包含在该Preset中的),默认为[]
"include": ["transform-es2015-arrow-functions"],
//禁用某些模块,默认为[]
"exclude": ["transform-es2015-for-of"],
//babel / preset-env处理polyfill的方式。
//参数:usage | entry | false,默认为false.
"useBuiltIns": false
}]
];
// 不包含在Preset中的Plugins需要单独引入
const plugins = [ "@babel/transform-arrow-functions" ];
return {
presets,
plugins
};
}
useBuiltIns的三个参数都是什么意思呢?
-
entry:在应用程序入口导入一次core-js,多次导入可能会有全局冲突或其他问题。
-
usage:自动为每个文件添加特定的该文件所用到的polyfill。
-
false:不要为每个文件自动添加polyfill,也不将“@babel/polyfill”导入到单个polyfill。
通过babel.config.js文件中配置useBulidIns选项,可以只将我们需要的、目标浏览器中不支持的那些语法进行转义。
babel.config.js: 项目范围内的配置,放在根目录下。配置可用于node_modules文件夹。
.babelrc:文件通常用于根目录下有多个package的项目,放在packages目录下;或者放在packages的子目录下,但需要在babel.config.js文件中进行配置
babelrcRoots: [
".",
"packages/*",
],
2.3 如果想从es6一键转浏览器可以直接运行的es5, 可以利用webpack(详见参考链接2)
- 进入项目,并安装以下各个依赖
- npm install --save webpack
- npm install --save babel-loader
- npm install --save babel-core
- npm install --save babel-preset-es2015
自从babel升级6.x版本后就分成了两个插件,一个是babel-core【终端运行】(如果是node请安装babel-cli ),一个是babel-preset-es2015
安装完上述内容之后,需要设置一个.babelrc的文件放在根目录下,内容为
{
"presets": ["es2015"]
}
并且在webpack.config.js中配置babel-loader
module.exports = {
entry: "./js/main.js",
output:{
filename: 'bundle.js'
},
module: {
loaders: [{
test: /\.js$/,
loader: "babel-loader"
}]
}
}
配置完成后,就可以直接在JS文件中使用es6的语法,然后通过webpack命令打包生成即可。
2.4 运行原理:https://mp.weixin.qq.com/s/kI9nm5_hpTvGHHE61fzHNQ
- 解析阶段:包括词法分析和语法分析。完成js代码到AST(js代码--令牌流--AST)的解析工作.Babel提供@babel/parser解析代码,用到的解析器是babylon
- 词法分析:将字符串形式的代码转译成 令牌(token)流.
- 举例: n * n
[ {type: {...}, value: "n", start: 0, end: 1, loc: {...} }, {type: {...}, value: "*", start: 2, end: 3, loc: {...} }, {type: {...}, value: "n", start: 4, end: 5, loc: {...} }, ]
每个type由一组属性来进行描述
type: { label:'name', keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null },
- 举例: n * n
- 语法分析: 将令牌流转译成AST形式
- 词法分析:将字符串形式的代码转译成 令牌(token)流.
- 转译:Babel提供@babel/traverse进行AST的遍历,完成对其节点的增加、删除、替换。该方法接受的参数为AST及自定义的转译规则,返回转换后的AST.
- 生成: Babel提供@babel/generator将转换后的AST生成字符串形式的js代码,可以对是否压缩及是否删除注释进行配置,并且支持sourceMap。
补充:为什么babel会使treeshaking失效?
上文我们提到了babel的作用是将浏览器无法识别的较新的JS语法,编译从浏览器能够支持的JS语法。然而也是由于它的编译,一些我们原本看似没有副作用的代码,便转化为了(可能)有副作用的。比如我们用ES6语法定义了Person类
export class Animal { constructor ({ breed, age, sex }) { this.breed = breed this.age = age this.sex = sex } getBreed () { return this.breed } }
在经过babel编译后,得到:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var _createClass = function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0, "value" in descriptor && (descriptor.writable = !0), Object.defineProperty(target, descriptor.key, descriptor); } } return function(Constructor, protoProps, staticProps) { return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), Constructor; }; }() var Animal = function () { function Animal(_ref) { var breed = _ref.breed, age = _ref.age, sex = _ref.sex; _classCallCheck(this, Animal); this.breed = breed; this.age = age; this.sex = sex; } _createClass(Animal, [{ key: 'getBreed', value: function getBreed() { return this.breed; } }]); return Animal; }();
我们可以看到,在创建Animal的时候使用了_createClass函数,由此产生了副作用。
按照我们常规的想法(我们之前写类的方式),我们希望的编译结果可能是这样的:
var Animal = function () { function Animal() { } Animal.prototype.getBreed = function () { return this.breed }; return Anaimal; }();
那babel为什么要使用Object.defineProperty,而不是原型链的方式去编译呢?
babel有一个loose
模式的,直译的话叫做宽松模式。(不严格遵循ES6的语义,而采取更符合我们平常编写代码时的习惯去编译代码)。而在 .babelrc文件中,默认是这样的:
// .babelrc { "presets": [["env", { "loose": false }]] }
也就是说babel默认使用符合ES6真正的语义的语法进行编译。
【这里解释一下,ES6语义需要注意的点:
a. 类内部声明的方法,是不可枚举的,而通过原型链声明的方法是可以枚举的。
b. for...of
的循环是通过遍历器(Iterator
)迭代的,并非i++.】
所以,当我们开启loose模式后,即可消除babel编译带来的副作用。