SourceMap
当我们使用 webpack 进行打包时,打包后的代码是和原代码有一定差异的,比如:代码会进行压缩,新旧语法的转换,TypeScript 转为 JavaScript,由于两份代码的不一致这就会导致调试(debug)很困难(因为出错信息显示的位置是打包后的代码,而不是原代码),解决调试这种不一致代码的方案就是 source map。
source map 可以将已转换的代码,映射到原始的源文件(简单说,source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置),使浏览器可以通过 source map 文件和已转换的代码文件生成原文件并在调试器中显示原文件。
使用 Source Map
使用 Source Map 分为两步:
1. 生成 Source Map 文件(在 webpack 可以通过配置来生成 source map 文件)
2. 在打包后的代码最下方添加一个注释,这个注释指向 Source Map 文件//# sourceMappingURL=common.bundle.js.map
完成上面两个步骤后,浏览器会根据填写的注释找到 source map 文件,并根据 source map 文件还原出原代码,使开发者方便进行调试。
在 Chrome 浏览器中,可以在 控制台(F12) -> 设置 中找到 source map 的开关(默认是打开的),如下图:
在 webpack 使用 source-map
在 webpack 中通过配置 devtool 属性可以生成 source-map,devtool 字段的配置项非常多,使用不同的配置产生的 source-map 会有一定差异,打包时也会有性能差异,需要对不同的情况选中不同的配置。
// webpack.config.jsmodule.exports = {devtool: 'source-map',};
[inline-|hidden-|eval-] [nosources-] [cheap-[module-]] source-map
devtool 的配置可以在上面中选项中组合使用,组合规则如下:
- [inline-|hidden-|eval-]: 三选一
- [nosources-]: 可选值
- [cheap-[module-]]:cheap 可以,module可以配合cheap使用,也可以配合使用
- source-map:可以搭配其他配置使用,也可以单独使用
devtool 还可以配置为 false,false 不会生成 source map。
文件结构
为了查看不同配置产生的结果,定义的文件结构如下:
// index.js 入口文件import add from './utils/index';console.log(add(10, 20));
// utils/index.jsconst add = (a, b) => {return a + b;}module.exports = add;
eval 效果
eval 是 development 环境下的默认值,他也不会生成 source-map, 但会将每个模块打包为 eval 代码,在 eval 后面增加 source-map 注释,这个注释只能定位到具体文件,没有更多的细节。
打包后的文件如下:
浏览器 sources 显示效果如下:
在上图中可以看到并没有生成源文件,当我们修改代码让其产生错误时,只能定位到对应的打包后的文件:
修改代码,产生错误:
// index.js 入口文件import add from './utils/index';console.log(add1(10, 20) // add1 不存在);
source-map 效果
生成了 source-map 文件,打包后的代码最下方自动添加了对应的注释:
使用 source-map 生成的效果就和原文件相同了:
eval-source-map 效果
eval 是会生成 eval 函数,然后在 eval 中添加 source-map 注释,而 source-map 可以生成 source-map 文件,两者结合起来就是生成 source-map 将 source-map 放在 eval 函数中,不给生成 source-map 的是 DataUrl。
浏览器显示效果和 source-map 相同,只不过是将 source-map 内容以 DataURI 放在了代码中。
inline-source-map 效果
inline-source-map 也是将 source-map 内容以 DataURI 的方式放在了代码中,不过不会生成 eval 函数,而是之间放在了代码的最底部。
浏览器显示效果和 source-map 相同。
cheap-source-map 效果
cheap-source-map 也可以生成 source-map 文件,但是他没有列映射,对比如下图:
cheap-module-source-map 效果
cheap-module-source-map 和 cheap-source-map 效果类似,但是如果其他 loader 对原代码进行处理(比如 babel)后,还是能生成原代码进行调试,而 cheap-source-map 生成的是 loader 处理后的代码。
修改文件结构:
// index.js 入口文件const { add, abc } = require('./utils/index');console.log(add2(10, 20));
// utils/index.jsconst add = (a, b) => {return a + b;}module.exports = {add,abc: '23112',};
基于上面的文件结构,增加 Babel 后 cheap-source-map 打包后的效果就出了问题(因为生成的 source-map 指向的是 Babel 转换后的代码),产生的效果如下:
使用 cheap-module-source-map 效果正常:
hidden-source-map 和 nosources-source-map 效果
hidden-souce-map:
可以生成 source-map 文件,但是不会自动在代码中添加指向该文件的注释,所以说如果想让 source-map 起作用需要手动添加注释。
nosources-source-map:
可以生成 source-map 文件,但是只有错误提示信息,不会生成原文件。
最佳实践
开发阶段和测试阶段推荐使用 source-map 或 cheap-module-source-map。
生产环境不使用 source-map (避免反编译,源码暴露)。
Babel
Babel 是前端开发不可缺少的工具之一,因为在开发中想使用 ES6+ 的语法、TypeScript、JSX 等都需要 Babel 进行转换(将 ES6+ 代码转为 ES5 的代码适配不同浏览器,将 TypeScript 转为 JavaScript 代码等)。
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中,如:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性
- 源码转换
Babel 和 webpack 都是微内核架构,它的核心非常小只包含主要的代码,大部分功能需要扩展插件来实现。
Babel 在命令行中使用
Babel 可以之间在命令行中使用,不和 webpack 等构建工具配和使用。
首先需要下载 babel 的核心包和 cli:
npm i @babel/core @babel/cli -D
通过 npx 命令运行:
npx babel 原文件/原文件夹 --out-dir 数据的文件夹
代码转换
因为 Babel 只包含核心功能,所以需要如代码转换功能需要下载对应插件:
安装 @babel/preset-env 预设:
npm i @babel/preset-env -D
命令行执行:
npx babel 原文件/原文件夹 --out-dir 数据的文件夹 --presets=@babel/preset-env
在 webpack 中使用 Babel
首先需要安装 babel 依赖和 babel-loader:
npm i @babel/core babel-loader -D
接下来需要下载 babel 对应的插件,但是对应的插件有很多,一个个安装非常麻烦,所以 Babel 提供了一套预设,预设会根据指定的目标环境(如浏览器版本)加载出需要插件列表并传递给 Babel。
注意:预设会加载 browserslist 的配置查看目标环境,也可以单独配置 targets 属(targets 会覆盖 browserslist)。
下载 @babel/preset-env 预设:
npm i @babel/preset-env -D
配置 webpack:
module.exports = {module: {rules: [{test: /\.js$/,use: [{loader: 'babel-loader',options: {presets: ['@babel/preset-env',// [// // 这里也可以写成一个数组, 目的是为了传递给预设参数// '@babel/preset-env',// {// targets: '> 0.25%, not dead',// ...配置参数// }// ],],},},],},],},}
抽离配置文件
Babel 的配置可以抽离为单独的文件,这样就不用把配置写到 webpack 里面,webpack 中直接使用 babel-loader 即可:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
}
],
},
}
Babel 提供了两种配置文件的编写:
- babel.config.json(或 .js、.cjs、.mjs)
- .babelrc.json (或 .babelrc、.js、.cjs、.mjs)
两种方式的区别在于: .babelrc 是早期使用的配置方式,但是对于配置多包管理时比较麻烦,而 babel.config.json 可以直接作为多包管理的子包更加推荐使用。
创建 babel.config.js 文件:
// babel.config.js
module.exports = {
presets: [
'@babel/preset-env',
],
}
Stage-X 的 preset
在 Babel7 之前,经常会有下面的 preset 配置方式,但是 Babel 7 开始就不推荐这种方式,推荐使用 preset-env。
// babel.config.js
module.exports = {
presets: [
'stage-0'
],
}
Polyfill
PolyFill 可以理解成一个补丁,它可以帮助我们更好的使用 JavaScript,比如:我们想使用 Promise、Generator 等新 API 的时候,这时有可能有些浏览器还没有支持它们,PolyFill 就会帮助我们用以前代码的方式去实现这些 API,就像打上一个补丁一样,这时就能使用这些新特性了。
使用 Polyfill
在 Babel7.4.0 之前使用 @babel/polyfill,但是现在已经不在推荐使用,现在需要单独引入 core-js、regenerator-runtime 来完成 polufill:
npm i core-js regenerator-runtime
因为下载的第三方包可能已经做过 polyfill 了,所以我们不能再对第三方包做一次 polyfil 避免冲突,所以在使用 babel-loader 的时候需要排除 node_module:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/, // 正则, 排除 nide_module 文件夹
},
],
},
}
安装好上面的依赖包之后,配置一下 presets:
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3, // 注意:默认使用 2 版本, 如果下载的是其他版本必须配置 corejs 否则打包报错
},
]
],
}
上面配置属性 useBuiltIns 有三个常见的属性:
- false : 不使用 polyfill
- usage : 会自动根据原代码中出现的语言特性,配置所需要的 polyfill,这样可以确保打包后的 polyfill 数量最小化,打包后文件会相对小一些。
- entry : 会根据 browserslist 配置的环境,对目标环境不支持的特性进行 polyfill ,原代码中没有用到的特性也会进行打包,所以打包后的文件会大一些。
指定 useBuiltIns: entry 需要在入口文件手动引入下面两个包:
// index.js 入口文件
import "core-js/stable";
import "regenerator-runtime/runtime";
注意:corejs 配置默认使用 2 版本, 如果下载的是其他版本必须配置 corejs 为对应版本否则打包报错。
Plugin-transform-runtime 插件
在使用 polyfill 时默认的效果是全局的,如果想要编写一个工具时,工具中使用全局的 polyfill 可能会污染使用者的代码,这时可以使用 Plugin-transform-runtime 这个插件来完成 polyfill。
安装插件:
npm i @babel/plugin-transform-runtime -D
修改配置:
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
// 使用这个插件就不用在这配置 polyfill 了
],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
]
],
}
React 的 JSX 语法支持
在编写 React 代码时使用的是 JSX 语法,JSX 可以通过 Babel 来转换,Babel 提供了对应的预设配置:
npm i @babel/preset-react -D
修改配置:
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
[
useBuiltIns: 'usage',
corejs: 3,
],
],
[
'@babel/preset-react',
],
],
}
TypeScript 的编译
当我们使用 TypeScript 进行项目开发时,通过 tsc 命令编译 TS 文件太过麻烦,我们可以直接通过 webpack 来配置 ts-loader 来对项目文件进行打包:
npm i ts-loader typescript -D
修改配置:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
exclude: /node_modules/, // 排除 node_modules
},
],
},
}
注:在使用 ts-loader 之前需要先通过 tsc --init 命令生成 tsconfig.json 配置文件。
使用 Babel 编译 TypeScript 代码
使用 Babel 也可以编译 TypeScript 代码,只需要下载 Babel 的预设即可:
npm i @babel/preset-typescript -D
修改 webpack 配置:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'babel-loader
},
exclude: /node_modules/, // 排除 node_modules
},
],
},
};
修改 babel.config.js 配置:
// babel.config.js
module.exports = {
presets: [
...,
[
'@babel/preset-typescript'
],
],
};
两种方式的选择
ts-loader 的优点是在编译过程中如果存在 ts 语法错误会使编译失败,缺点是不能进行 polyfill。
babel-loader 的优点使可以进行 polyfill,缺点是在编译过程的语法错误不会导致编译失败。
所以一个最佳实践是:
使用 babel 来进行代码的转换,使用 tsc 命令来对代码进行检测,也就是说先通过 tsc 命令检测代码是否存在问题,如果不存在问题再进行打包。
配置 script 脚本,添加 tsc 检测:
// package.json
{
"scripts": {
"check": "tsc --notEmit",
"build": "npm run check & webpack --config ./webpack.config.js --progess"
}
}
// --notEmit 关闭 tsc 的文件输出, 只做 typescript 校验

