前言
新建的项目发现在手机钉钉的浏览器中打开是空白的,其他浏览器正常,虽然一开始定位是浏览器兼容问题,但是使用 Vconsole 也打印不出问题。后面一步一步定位到了 main.js 使用了一些 ES2020 的语法,但是 babel 没有转换到钉钉支持的语法,在钉钉的浏览器上运行不了,估计报错被打包工具劫获了。
第一次遇到兼容问题,以前都是使用的默认或者别人配好的,这次也算初步了解到了 babel 等兼容性工具。可通过 navigator.appVersion,navigator.userAgent 等获取目标浏览器的内核,当使用到一些新语法时,注意它的兼容性问题,查看支持的内核版本,并使用 babel 这些兼容工具转换语法以兼容低版本浏览器。
借此也系统学习一下Babel:
了解Babel
官方文档 :www.babeljs.cn/docs
Babel 是一个 JavaScript 编译器
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
- 更多参考资料!(请查看这些 视频 以获得启发)
js
// Babel 接收到的输入是: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
Babel原理核心步骤
- 解析(Parsing): 词法分析(Lexical Analysis):将源代码字符串分割成tokens数组。每个token是一个对象,包含类型和值(例如,标识符、关键字、操作符等)。
- **语法分析(Syntax Analysis):**将tokens数组转换成抽象语法树(AST)。AST是一个树形结构,表示代码的结构。
- 转换(Transformation): 在这个阶段,Babel 会遍历AST,并应用一系列插件来进行代码转换。这些插件可以添加、删除或修改AST中的节点。例如,将ES6的箭头函数转换成ES5的函数表达式,或将ES6的模块导入导出语句转换成CommonJS的require和exports语句。
- 生成(Code Generation): 将转换后的AST转换回代码字符串。这个过程涉及到生成源代码中的空格、缩进和换行符等,以保持代码的可读性。
- 源码映射(Source Maps): Babel 还会生成源码映射(source maps),这是一个信息文件,可以将编译后的代码映射回原始源代码。这样,在调试时,开发者可以直接查看原始代码,而不是编译后的代码。
browserslistrc
在根目录一般会有一个browserslistrc文件,browserslist 配置通常用于前端工具链中的多个地方,包括 Babel、Autoprefixer、PostCSS、Stylelint 等。这些工具会根据您在 browserslist 中指定的浏览器版本范围来决定如何转换或优化代码。例如,Babel 会根据 browserslist 配置来决定哪些 JavaScript 新特性需要被转换成旧浏览器兼容的代码。Autoprefixer 会根据同样的配置来决定哪些 CSS 属性需要添加浏览器前缀。 在项目根目录中创建一个名为 .browserslistrc 的文件,或者将 browserslist 配置放在 package.json 文件中的 browserslist 字段中,这样相关的工具就能够读取这些配置,并根据它们来处理代码。
markdown
> 1%
last 2 versions
not dead
not ie 11
这些配置的含义如下:
1%:选择全球使用率大于1%的浏览器。 last 2 versions:选择每个浏览器的最后两个版本。 not dead:排除已停止维护或更新的浏览器。 not ie 11:排除Internet Explorer 11浏览器。
也可在package.json配置
json
{
"browserslist": "> 1%,last 2 versions, not dead , not ie 11"
}
Babel配置
vue-cli生成的项目中,会有一个babel.config.js
文件,可在此配置Babel
js
{
"presets": [],
"plugins": []
}
presets
预设是一组插件的集合,用于描述你的代码需要转换成哪种版本的 JavaScript。以下是一些常见的预设:
@vue/cli-plugin-babel/preset
是 Vue CLI 项目中默认使用的一个 Babel 预设,它包含了一套适用于 Vue.js 项目的 Babel 插件集合。这个预设是为了简化 Babel 配置,确保 Vue CLI 生成的项目能够正确地编译 Vue 组件和 JavaScript 代码。 当你使用 Vue CLI 创建一个新项目时,Vue CLI 会自动添加这个预设到你的 babel.config.js 文件中。这个预设通常包含了如下功能:
- 解析 Vue 单文件组件(.vue 文件)。
- 转换 JavaScript 新特性,使其兼容不支持这些特性的浏览器。
- 提供对 TypeScript 的支持(如果项目中使用了 TypeScript)。
- 可能还包括其他与 Vue.js 相关的优化和特性支持。
@babel/preset-env
这个预设允许你使用最新的 JavaScript 语法,而无需担心兼容性问题。它会根据你支持的目标环境自动确定你需要的 Babel 插件和 polyfills。
原则上你写vue,用vuecli会自动将@vue/cli-plugin-babel/preset添加,它是包含了@babel/preset-env 的,除非你有特殊的需求,否则不需要在配置中再次添加 @babel/preset-env。
这里详细介绍一下它的配置项,因为这个preset很常用。
js
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
// 指定core-js版本
corejs: 3,
// 指定兼容性到浏览器的哪个版本, 官方推荐使用根目录下的browserslistrc文件替换targets,同时配置使用targets
targets: {
chrome: '60',
firefox: '60',
ie: '9',
safari: '50',
edge: '17'
}
}
]
],
targets
指定要支持的环境。可以是一个对象,也可以是一个字符串(如 .browserslistrc 文件的内容)。
js
{
"targets": {
"chrome": "58",
"ie": "11"
}
}
useBuiltIns
根据你的target指定如何处理 polyfills。有三个选项: "entry ": 在你的入口文件中手动导入 core-js 和 regenerator-runtime,@babel/preset-env 会根据 targets 自动导入所需的 polyfills。 "usage "(推荐): 自动检测代码中需要哪些 polyfills,并只导入那些必要的 polyfills。 "false": 不自动导入 polyfills,你需要手动导入。
corejs
指定要使用的 core-js 版本。
modules
指定如何转换模块。默认值是 "auto",它会根据你使用的环境自动选择是否转换模块。其他选项包括 "amd", "umd", "systemjs", "commonjs", "cjs", "false" 等。
其它
debug: 启用 debug 模式,输出更多关于插件的信息。这对于调试很有用。
include: 指定要包含的插件列表。
exclude: 指定要排除的插件列表。
loose: 启用松散模式,允许插件生成更简洁的代码,但可能不完全遵循规范。
spec: 启用严格模式,尽可能遵循规范。
shippedProposals: 启用实验性特性。
forceAllTransforms: 即使目标环境已经支持某个特性,也强制进行转换。
configPath: 指定 Babel 配置文件的路径。
ignoreBrowserslistConfig: 忽略 .babelrc 或 package.json 中的 browserslist 配置。
@babel/preset-react
如果你在项目中使用 React,这个预设包含了所有需要的插件来转换 JSX 和 React 插件。
@babel/preset-typescript
如果你使用 TypeScript,这个预设可以帮助你将 TypeScript 代码转换为 JavaScript。
plugins
插件是 Babel 转译过程中的单个转换规则,用于实现特定的功能。你可以根据项目的需求添加相应的插件。以下是一些常用的插件:
@babel/plugin-proposal-class-properties:允许你使用类属性语法。 @babel/plugin-proposal-decorators:允许你使用装饰器。 @babel/plugin-transform-runtime:用于重用 Babel 注入的帮助代码,以减少代码体积。
例子:
babel-plugin-import
一个用于按需加载模块的 Babel 插件,尤其适用于像 ant-design-vue 这样的 UI 库。这个插件的配置应该包含在一个数组中,数组的第一个元素是插件名称,第二个元素是一个对象,包含了插件的配置,第三个元素是一个标识符.
json
plugins: [
[
'import',
{
libraryName: 'ant-design-vue',
libraryDirectory: 'es',
style: true
},
'ant-design-vue'
],
[
// 使用 import时 'antd' 可以代替 'ant-design-vue'
'import',
{
libraryName: 'antd',
customName: name =>
`ant-design-vue/es/${name.replace(
/[A-Z]/g,
(match, index) => (index === 0 ? '' : '-') + match.toLowerCase()
)}`,
style: true
},
'antd'
]
]
这个插件的配置项如下: libraryName: 指定要按需加载的库的名称,在这个例子中是 ant-design-vue。
libraryDirectory: 指定库的入口目录,这里使用 es,意味着会从 es 目录下加载 ES Module 格式的文件。
customName: 这是一个自定义函数,它允许您自定义导入的组件的路径。函数 name 参数代表要导入的组件的名称。这个函数将组件名称中的大写字母转换为连字符分隔的小写字母,并构造出正确的 ES 模块路径。例如,如果您导入了 Button 组件,它会将其转换为 ant-design-vue/es/button。
style: 这个选项设置为 true 表示会加载库对应的样式文件。对于 ant-design-vue,这意味着当您导入组件时,相关的样式文件也会被自动导入。
例如,当您在 Vue 组件中导入 ant-design-vue 的按钮组件时:
js
import { Button } from 'ant-design-vue';
Babel 插件会自动将它转换为:
js
import Button from 'ant-design-vue/es/button';
import 'ant-design-vue/es/button/style/css';
实现一个Babel插件将所有的console.log换成业务的myCollect方法
tip:Webpack 插件主要用于处理构建过程的其他方面,如资产管理、环境变量注入、打包优化等。对于代码转换,通常使用加载器(如 babel-loader)与相应的编译工具(如 Babel)配合使用。
这里只使用Babel插件来简单实现将所有的console.log换成我自己写的一个myCollect方法,学习一下babel插件的写法。它会检查myCollect是否已经被导入,如果没有,它会自动添加一个导入语句:
实现
js
// my-babel-plugin.js
module.exports = function (babel) {
const { types: t } = babel;
return {
name: "console-log-to-mycollect",
visitor: {
Program: {
enter(path, state) {
// 检查是否已经导入了myCollect
let hasImport = false;
path.traverse({
ImportDeclaration(importPath) {
if (importPath.node.source.value === 'path-to-mycollect') {
hasImport = true;
importPath.stop();
}
},
});
// 如果没有导入,添加导入语句
if (!hasImport) {
path.unshiftContainer(
'body',
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier('myCollect'))],
t.stringLiteral('path-to-mycollect') // 这里写MyCollect方法的路径
)
);
}
},
},
CallExpression(path, state) {
if (
t.isMemberExpression(path.node.callee) &&
t.isIdentifier(path.node.callee.object, { name: "console" }) &&
t.isIdentifier(path.node.callee.property, { name: "log" })
) {
// 替换为 myCollect 方法调用
path.replaceWith(
t.callExpression(t.identifier('myCollect'), args.map(arg => arg.node))
);
}
},
},
};
};
这个插件定义了一个访问者,它会遍历所有的调用表达式(CallExpression)。如果调用表达式的 callee 是一个成员表达式,并且对象是 console,属性是 log,那么它就是一个 console.log 调用。在这种情况下,插件会将替换成myCollect
使用
Babel配置文件上的plugins加上即可
js
module.exports = {
plugins: [
// 写入插件的路径,如果是同一目录下
"./my-babel-plugin.js",
],
};
处理浏览器兼容问题tips
-
特性检测:使用现代的JavaScript特性检测方法来确定浏览器是否支持特定的API或功能。例如,您可以使用 window.Promise 来检查浏览器是否支持Promise对象。
jsif (window.Promise) { // 浏览器支持Promise } else { // 浏览器不支持Promise }
-
用户代理检测:尽管不推荐,但在某些情况下,您可能需要使用用户代理字符串(navigator.userAgent)。使用正则表达式或专门的库(如 ua-parser-js)来解析用户代理字符串。
js// 使用正则表达式进行简单的用户代理检测 const userAgent = navigator.userAgent; const isChrome = /Chrome/i.test(userAgent); const isFirefox = /Firefox/i.test(userAgent);
-
客户端_HINTs:这是一种新的API,允许开发者从浏览器中获取更多信息,例如用户的首选语言、设备类型等。
js// 获取用户的语言偏好 navigator.languages.forEach((language) => { console.log(language); });
-
Modernizr:这是一个流行的库,用于检测用户浏览器对新HTML5和CSS3特性的支持情况。
-
Can I Use:这是一个在线资源,提供了详细的浏览器兼容性信息,可以帮助开发者了解不同浏览器对各种Web技术的支持情况。
-
Babel 和 Polyfills:如果您正在使用像Babel这样的转译器,您可以使用polyfills来填补旧浏览器中的功能缺口,而不必依赖于特性检测。
-
CSS @supports:在CSS中,您可以使用@supports规则来检测浏览器是否支持特定的CSS特性。
css@supports (display: grid) { .element { display: grid; } }