JavaScript 代码为啥要编译?主要是采用 ES6 及以上语法编写的代码兼容不了老浏览器老环境。Bable 可以将其转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。Babel 通过插件让你现在就能使用新的语法,无需等待浏览器的支持。
安装 Babel
在我们安装了 @babel/cli
、@babel/core
、@babel/preset-env
和后,就可以使用 Babel 来转码了。
npm i -D @babel/cli @babel/core @babel/preset-env
转码分析
// test.js
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);
上面代码执行 npx babel test.js -o test.es5.js
后转换为:
"use strict";
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map(function (n) {
return n + 1;
});
当然,这种简单语法,通过 Bable 官网的在线工具也能做到。Bable 编译后的 JavaScript 代码不难呀,您这样想就错了。实际项目可能很多文件很多模块,有可能还用 Webpack 等工具压缩混淆过,比这复杂多了。
实例源码
比如下面一个比较简单的 Node.js 项目。
Rectangle.js 代码
// Rectangle
class Rectangle {
constructor(w, h) {
this.w = w;
this.h = h;
}
calcArea() {
return this.w * this.h;
}
calcCircumference() {
return 2 * this.w + this.h;
}
}
module.exports = Rectangle;
main.js 代码
const express = require('express');
const Rectangle = require('./Rectangle.js');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send(`The area is ${new Rectangle(10, 20).calcArea()}`);
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
})
安装 babel-loader
Webpack 借助 babel-loader
实现了 babel 的代码转化功能,使用 terser-Webpack-plugin
实行代码合并压缩混淆。前者需要额外安装.
npm i -D babel-loader
后者在 Webpack 5 中已经集成,Webpack 4 中也需要单独安装,< Webpack 4 一般用它内置的 webpack.optimize.UglifyJsPlugin
。
初步分析
Node.js 代码可以直接用 node 命令来跑,即使是生产环境部署也可以这么做。但有时为了保护代码,生产环境下使用 Webpack 编译的“安全”代码。遇到这种代码,多数开发人员可能是就是看“天书”了。如下面格式化过的代码:
(() => {
var e = {
86: e => {
function r(e) {
return r = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) {
return typeof e
} : function (e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
}, r(e)
}
function t(e, t) {
for (var o = 0; o < t.length; o++) {
var n = t[o];
n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object.defineProperty(e, (void 0, i = function (e, t) {
if ("object" !== r(e) || null === e) return e;
var o = e[Symbol.toPrimitive];
if (void 0 !== o) {
var n = o.call(e, "string");
if ("object" !== r(n)) return n;
throw new TypeError("@@toPrimitive must return a primitive value.")
}
return String(e)
}(n.key), "symbol" === r(i) ? i : String(i)), n)
}
var i
}
var o = function () {
function e(r, t) {
!function (e, r) {
if (!(e instanceof r)) throw new TypeError("Cannot call a class as a function")
}(this, e), this.w = r, this.h = t
}
var r, o;
return r = e, (o = [{
key: "calcArea", value: function () {
return this.w * this.h
}
}, {
key: "calcCircumference", value: function () {
return 2 * this.w + this.h
}
}]) && t(r.prototype, o), Object.defineProperty(r, "prototype", {writable: !1}), e
}();
e.exports = o
},
860: e => {
"use strict";
e.exports = require("express")
}
}, r = {};
function t(o) {
var n = r[o];
if (void 0 !== n) return n.exports;
var i = r[o] = {exports: {}};
return e[o](i, i.exports, t), i.exports
}
var o, n, i, u = {};
o = t(860),
n = t(86),
(i = o()).get("/", (function (e, r) {
...
})();
860
、86
、e
、t
、r
…… 一堆乱七八糟的变量,看不懂呀。别着急,慢慢分析。建议从后想前看就容易多了。
首先,注意到第 63
行 t(860)
, 先看参数 860
。它对应代码第 49~52
行,是一个对象字面量。其中 860
是键,值是一个函数,该函数导入了 express 模块,并通过 e.exports
导出。
在 Webpack中,对象e
包含应用使用的所有模块,因为我们使用的是 CommonJS,能猜到是 Webpack 对 modules 实现。e.exports
实际上就是module.exports
。
第 64
行 t(86)
, 先看参数 86
。它对应代码第 3~48
行,也是一个对象字面量。其中 86
是键,值也是一个函数,该函数返回对类 Rectangle
引用,并通过 e.exports
导出。
然后,重点看类 Rectangle
对应的 ES5 实现。注意到第 29~46
行自执行函数 o
没有?它会返回自定义类 Rectangle
,是基于 prototype
的,只是基于 Object.defineProperty
包裹了一层:
Object.defineProperty(r, "prototype", {writable: !1})
自执行函数 o
在第 45
行使用 t(r.prototype, o)
调用了工具函数 t(e, t)
。函数 t(e, t)
对应第 29~46
行代码,它 在第 16
行的 if
语句又调用了工具函数 r(e)
。至于这两个工具函数,看到文章最后又清楚了。
对照分析
如果还是云里雾里的,还需“庖丁解牛”。接下来将 Webpack 的 optimization 配置如下:
optimization: {
moduleIds: 'named', // 输出模块名
minimize: false, // 不压缩
minimizer: [new TerserPlugin({
terserOptions: {
format: {
beautify: true, // 格式化
comments: true // 保留注释
}
}
})]
}
为了对照代码理解,此处我们会非压缩压缩以输出模块名,TerserPlugin
插件设置了输出代码格式化并保留注释。
最后,再次使用 Webpack 编译,生成的代码如下:
/******/ (() => { // WebpackBootstrap
/******/ var __Webpack_modules__ = ({
/***/ "./Rectangle.js":
/***/ ((module) => {
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps)
_defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
var Rectangle = /*#__PURE__*/function () {
function Rectangle(w, h) {
_classCallCheck(this, Rectangle);
this.w = w;
this.h = h;
}
_createClass(Rectangle, [{
key: "calcArea",
value: function calcArea() {
return this.w * this.h;
}
}, {
key: "calcCircumference",
value: function calcCircumference() {
return 2 * this.w + this.h;
}
}]);
return Rectangle;
}();
module.exports = Rectangle;
/***/
}),
/***/ "express":
/***/ ((module) => {
"use strict";
module.exports = require("express");
/***/
})
/******/
});
/************************************************************************/
/******/ // The module cache
/******/ var __Webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __Webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __Webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/ var module = __Webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/
};
/******/
/******/ // Execute the module function
/******/ __Webpack_modules__[moduleId](module, module.exports, __Webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/
}
/******/
/************************************************************************/
var __Webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
var express = __Webpack_require__("express");
var Rectangle = __Webpack_require__("./Rectangle.js");
var app = express();
var port = 3000;
app.get('/', function (req, res) {
// 本生成的是concat,主机会报错,故用 + 代替
res.send('The area is ' + new Rectangle(10, 20).calcArea());
});
app.listen(port, function () {
console.log("Example app listening on port ".concat(port));
});
})();
var __Webpack_export_target__ = exports;
for (var i in __Webpack_exports__) __Webpack_export_target__[i] = __Webpack_exports__[i];
if (__Webpack_exports__.__esModule) Object.defineProperty(__Webpack_export_target__, "__esModule", { value: true });
/******/
})()
;
这段编译过的代码没有压缩混淆,和开发环境的代码差不多。可以看到,moduleIds 已经是模块名 ./Rectangle.js" 和 "express",比原来的860
、86
好理解多了。对象 e
确实对应 module
, 而函数 Rectangle
的确对应混淆过的类型 o
。
工具函数 _defineProperties
对应混淆过的函数 t
, 工具函数 _typeof
对应混淆过的函数 _typeof
。其他工具函数如 _classCallCheck
,_createClass
、_toPropertyKey
和 _toPrimitive
在本应用中只会调用一次,没有 TreeShaking 的价值。于是 Webpack 将就它们混淆成了匿名函数,这样性能会好些。
只要是用到 Webpack + Bable + Class 的应用,生成的 ES5 buldle 都会_defineProperties
、_classCallCheck
,_createClass
三个工具函数。本实例使用的是 Bable 7,如果使用 Bable 6,defineProperties
函数是放在_createClass
函数内部的。
还有一些 Webpack 相关的通用代码,比如,__Webpack_modules_
、__Webpack_module_cache__
、__Webpack_require__
、__Webpack_export_target__
。参看注释,应该能懂吧?
参考项目
访问 codesandbox 查看完整项目代码。
纯 JavaScript 项目还好,如果使用了React、Vue等框架,问题比这复杂多了。我在这里只是抛砖引玉,路漫漫呀,下次分解。
评论 (0)