前言
在前端开发过程中,我们可能会遇到一些js的兼容性
问题,例如:语法兼容
、api兼容
。同时我们可能想做一些语言增强
,例如使用:jsx
,ts
。大多数情况下此类问题上升不到普通开发场景下,因为脚手架可能已经解决了相关问题,本文主要了解解决此类问题原理和宏观思想。
一、解决兼容性问题
我们平时可能会使用新发布的api或语法,有可能在一些老旧环境下不受支持,该如何优雅解决呢?
1.api兼容性问题
-
使用nvm降低node版本,模拟老旧环境。
控制台执行:
nvm use 9.11.2
-
使用ES6的api
flatMap
处理数组创建index.js写入如下代码:
let arr = [1,2].flatMap(x => [x, x * 2]);
执行结果:TypeError: [1,2].flatMap is not a function
-
api兼容的原理认知
我们将使用三方库来兼容当前环境,本质上其实就是在Array.prototype
上添加一个名为flatMap
的方法。添加代码:
Array.prototype.flatMap = function(callback) { //... }; let arr = [1,2].flatMap(x => [x, x * 2]);
类似于这种做法或思想被称之为
polyfill
,有道的翻译结果:“一种用于衣物、床具等的聚酯填充材料,使这些物品更加温暖舒适”。我觉得可以简单理解为当前环境下没有这个api,我们通过一些手段将其填充(polyfill)
了进去。 -
兼容环境配置
认识了大致原理后,接下来使用core-js实现polyfill
控制台执行:
npm install core-js
修改代码:
require("core-js/modules/es.array.flat-map"); let arr = [1,2].flatMap(x => [x, x * 2]); console.log(arr)//[ 1, 2, 2, 4 ]
结合上下文我们就能认识到其实core-js做了什么事,本质上来说就是将所有的api在其
内部实现
,我们在编码过程中直接引入对应的api,进而不借助当前运行环境
提供的api,从而解决api兼容问题。
2.语法兼容性问题
语法兼容的问题相比api兼容更为复杂一些,其核心思想就是语法转换,详细说来,就是在保证运行结果
不变的前提下,将当前运行环境不支持的语法用当前环境支持的语法去替换
,下面对async-await
语法进行转换。
-
安装语法转换库
控制台执行:
npm install regenerator
-
准备待转换代码
创建index.js写入:
async function func(){ await Promise.resolve(); }
-
语法转换
将刚才的代码以字符串形式转入regenerator.compile方法中进行转换。
创建compoler.js写入:
let regenerator = require("regenerator"); //将待转换的代码以字符串形式转入regenerator.compile方法 let result = regenerator.compile(` async function func(){ await Promise.resolve(); }`, { includeRuntime: true, } ) console.log(result.code)//转换后的代码
-
写一个自动转换工具
我们已经了解到了语法转换的基本原理,接下来开发一个自动转换语法的小工具吧。
修改compoler.js:
let regenerator = require("regenerator"); let fs = require("fs"); let path = require("path"); //读取待转换代码 let source = fs.readFileSync(path.join(__dirname, "./index.js"), "utf-8"); //使用regenerator.compile进行转换 let result = regenerator.compile(source, { includeRuntime: true, } ) //将结果写入target.js fs.writeFileSync(path.join(__dirname, "./target.js"), result.code)
执行后就会在当前目录自动创建target.js:
二、初识babel
上面我们解决的两种兼容问题,究其根本实质都是在做代码转换
,但在实际开发中,如果每转化一次就进行一次上面的操作,这种做法效率太过低下
。有没有能整合上面那些操作的工具(代码集成转换工具
)呢?它就是babel。
1.安装babel
控制台执行:
npm install -D @babel/core @babel/cli
@babel/core
主要进行代码转换
,@babel/cli
主要是使用户可以通过命令行
方式去调用@babel/core执行转换
2.准备待转换代码
创建index.js写入:
let family = {
grandfather: {
father: {
son1: {
name:"a"
},
son2: {
name:"b"
}
}
},
}
console.log(family?.grandfather?.father?.son1?.name)
console.log(family?.grandfather?.father?.son2?.name)
console.log(family?.grandfather?.father?.son3?.name)
3.配置控制台命令并转换
修改当前目录下的package.json:
...
"scripts": {
"compile":"babel ./index.js -o ./target.js"
},
...
控制台执行:
npm run compile
此时再看看当前文件目录,已经多出来target.js,你会发现代码好像只是被复制了一遍,并无变化,不过我们确实进行了转换,原因在于我们在转换过程中没有告诉babel要具体使用什么转换插件。
4.加入转换插件
安装一个专门转换链式调用的插件,控制台执行:
npm install -D @babel/plugin-transform-optional-chaining
创建babel配置文件babel.config.js
写入:
module.exports = {
plugins:[
"@babel/plugin-transform-optional-chaining"
]
}
5.再次转换
控制台执行:
npm run compile
再次看看target.js:
var _family$grandfather, _family$grandfather2, _family$grandfather3;
let family = {
grandfather: {
father: {
son1: {
name: "a"
},
son2: {
name: "b"
}
}
}
};
console.log(family === null || family === void 0 || (_family$grandfather = family.grandfather) === null || _family$grandfather === void 0 || (_family$grandfather = _family$grandfather.father) === null || _family$grandfather === void 0 || (_family$grandfather = _family$grandfather.son1) === null || _family$grandfather === void 0 ? void 0 : _family$grandfather.name);
console.log(family === null || family === void 0 || (_family$grandfather2 = family.grandfather) === null || _family$grandfather2 === void 0 || (_family$grandfather2 = _family$grandfather2.father) === null || _family$grandfather2 === void 0 || (_family$grandfather2 = _family$grandfather2.son2) === null || _family$grandfather2 === void 0 ? void 0 : _family$grandfather2.name);
console.log(family === null || family === void 0 || (_family$grandfather3 = family.grandfather) === null || _family$grandfather3 === void 0 || (_family$grandfather3 = _family$grandfather3.father) === null || _family$grandfather3 === void 0 || (_family$grandfather3 = _family$grandfather3.son3) === null || _family$grandfather3 === void 0 ? void 0 : _family$grandfather3.name);
6.使用预设
经过上面的配置,我们使用babel转换了链式运算符,如果需要转换更多语法呢,如此一个个操作仍然繁杂,因此我们使用babel的预设
,预设可以简单理解为一堆插件的集合,开箱即用。
常见的babel预设:typescript预设、jsx预设、基础预设等。
-
安装基础预设
cmd执行:
npm install -D @babel/preset-env
-
修改babel.config.js
module.exports = { presets:[//配置预设 ["@babel/preset-env",{ targets:{//需要兼容的浏览器版本 edge:"17", chrome:"67" }, useBuiltIns:"usage",//按需导入 corejs:"3.34.0"//安装的corejs的版本 }] ] }
-
更新代码
修改index.js:
async function func(){ let family = { grandfather: { father: { son1: { name:"a" }, son2: { name:"b" } } }, } return Promise.resolve(family); } func().then(res=>{ console.log(res?.grandfather?.father?.son1?.name) })
3.代码转换
控制台执行:
npm run compile
再看看target.js:
"use strict"; require("core-js/modules/es.promise.js"); async function func() { let family = { grandfather: { father: { son1: { name: "a" }, son2: { name: "b" } } } }; return Promise.resolve(family); } func().then(res => { var _res$grandfather; console.log(res === null || res === void 0 || (_res$grandfather = res.grandfather) === null || _res$grandfather === void 0 || (_res$grandfather = _res$grandfather.father) === null || _res$grandfather === void 0 || (_res$grandfather = _res$grandfather.son1) === null || _res$grandfather === void 0 ? void 0 : _res$grandfather.name); });
总结
感谢你读到这里