转载自:https://www.cnblogs.com/qcloud1001/p/10167756.html
https://blog.csdn.net/a250758092/article/details/78543440
1.模块化
模块化是指把一个复杂的系统分解到一个一个的模块。
模块化开发的优点:
(1)代码复用,让我们更方便地进行代码管理、同时也便于后面代码的修改和维护。
(2)一个单独的文件就是一个模块,是一个单独的作用域,只向外暴露特定的变量和函数。这样可以避免污染全局变量,减少变量命名冲突。
js模块化规范有:CommonJS、AMD、CMD、ES6的模块系统。
1.1 CommonJS 规范
是服务器端模块的规范,由nodejs推广使用。该规范的核心思想是:允许模块通过require方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口。
// 导出 module.exports = moduleA.someFunc; // 导入 const moduleA = require('./moduleA');
1.2 AMD
AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是require.js(还有个js库:curl.js)
// 定义一个模块 define('module', ['dep'], function (dep) { return exports; }); // 导入和使用 require(['module'], function (module) { });
1.3 ES6模块化
ES6在语言的层面上实现了模块化。浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
在 ES6 中,使用export关键字来导出模块,使用import关键字引用模块。但是浏览器还没有完全兼容,需要使用babel转换成浏览器支持的代码。正是由于Babel的存在,前端开发者才能能够不用考虑浏览器兼容性、畅快淋漓地使用最新的JavaScript语言特性。
// 导出 export function hello() { }; export default { // ... }; // 导入 import { readFile } from 'fs'; import React from 'react';
使用import导入模块时,需要知道要加载的变量名或函数名。
在ES6中还提供了export default,为模块指定默认输出。对应导入模块import时,不需要使用大括号。
2. Babel
在1.3中,我们提到了由于浏览器兼容问题,需要使用Babel将es6转成es5。官方给出的定义是,将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。从中,我们可以看出,Babel的主要功能是“翻译”。
实际上babel转换后的代码是遵循commonJS规范的,而这个规范,浏览器(支持的是 ECMAScript)并不能识别。因此导入到浏览器中会报错,而nodeJS是commonJS的实现者,所以在babel转换后的代码是可以在node中运行的。
为了将babel生成的遵循commonJS规范的es5写法能够在浏览器上直接运行,我们就借助webpack这个打包工具来完成。【概括一下:流程是 es6->es5(commonJS规范)->浏览器可执行代码】
Babel中的Preset:即一组预先设定的插件(plugins)【嗯,所以它们之间是集合和元素的关系】可以使用官方提供的,也可以自己创建。通过babel.config.js文件中配置useBulidIns选项,可以只将我们需要的、目标浏览器中不支持的那些语法进行转义。
2.1 Babel配置文件的选择
之前版本的babel都是使用.baberc
来做配置文件,babel7引入了babel.config.js
。但是它并不是.baberc
的替代品,二者根据使用的场景不同自行选择。
.babelrc
{ "presets": ["@babel/preset-flow","@babel/preset-react", "@babel/preset-typescript"], "plugins": [...] }
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: 项目范围内的配置,放在根目录下。配置可用于node_modules文件夹。
.babelrc:文件通常用于根目录下有多个package的项目,放在packages目录下;或者放在packages的子目录下,但需要在babel.config.js文件中进行配置
babelrcRoots: [ ".", "packages/*", ],
2.2 需要安装的依赖
-
@babel/core:babel的核心包,核心的api都在这里。
-
@babel/cli :通过命令行运行babel.
- @babel/polyfill:包含所有新的JS语法特征。相当于一个填充,因为babel本身只支持转换箭头函数、结构赋值这些语法糖类的语法,而Polyfill中包含了Promise函数等新的特征。【注意】babel/polyfill安装时是--save而不是--save-dev
-
@babel/preset-env:指定的一组babel plugins.
当存在多个presets和多个plugins时的优先级:
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命令打包生成即可。
补充:为什么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编译带来的副作用。