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 的开关(默认是打开的),如下图:
image.png

在 webpack 使用 source-map

在 webpack 中通过配置 devtool 属性可以生成 source-map,devtool 字段的配置项非常多,使用不同的配置产生的 source-map 会有一定差异,打包时也会有性能差异,需要对不同的情况选中不同的配置。

  1. // webpack.config.js
  2. module.exports = {
  3. devtool: 'source-map',
  4. };

[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。

文件结构

为了查看不同配置产生的结果,定义的文件结构如下:

  1. // index.js 入口文件
  2. import add from './utils/index';
  3. console.log(
  4. add(10, 20)
  5. );
  1. // utils/index.js
  2. const add = (a, b) => {
  3. return a + b;
  4. }
  5. module.exports = add;

eval 效果

eval 是 development 环境下的默认值,他也不会生成 source-map, 但会将每个模块打包为 eval 代码,在 eval 后面增加 source-map 注释,这个注释只能定位到具体文件,没有更多的细节。

打包后的文件如下:
image.png
浏览器 sources 显示效果如下:
image.png
在上图中可以看到并没有生成源文件,当我们修改代码让其产生错误时,只能定位到对应的打包后的文件:
修改代码,产生错误:

  1. // index.js 入口文件
  2. import add from './utils/index';
  3. console.log(
  4. add1(10, 20) // add1 不存在
  5. );

浏览器报错信息如下:
image.png
image.png

source-map 效果

生成了 source-map 文件,打包后的代码最下方自动添加了对应的注释:
image.png
使用 source-map 生成的效果就和原文件相同了:
image.png

eval-source-map 效果

eval 是会生成 eval 函数,然后在 eval 中添加 source-map 注释,而 source-map 可以生成 source-map 文件,两者结合起来就是生成 source-map 将 source-map 放在 eval 函数中,不给生成 source-map 的是 DataUrl。
image.png
浏览器显示效果和 source-map 相同,只不过是将 source-map 内容以 DataURI 放在了代码中。

inline-source-map 效果

inline-source-map 也是将 source-map 内容以 DataURI 的方式放在了代码中,不过不会生成 eval 函数,而是之间放在了代码的最底部。
image.png
浏览器显示效果和 source-map 相同。

cheap-source-map 效果

cheap-source-map 也可以生成 source-map 文件,但是他没有列映射,对比如下图:
image.png

cheap-module-source-map 效果

cheap-module-source-map 和 cheap-source-map 效果类似,但是如果其他 loader 对原代码进行处理(比如 babel)后,还是能生成原代码进行调试,而 cheap-source-map 生成的是 loader 处理后的代码。

修改文件结构:

  1. // index.js 入口文件
  2. const { add, abc } = require('./utils/index');
  3. console.log(add2(10, 20));
  1. // utils/index.js
  2. const add = (a, b) => {
  3. return a + b;
  4. }
  5. module.exports = {
  6. add,
  7. abc: '23112',
  8. };

基于上面的文件结构,增加 Babel 后 cheap-source-map 打包后的效果就出了问题(因为生成的 source-map 指向的是 Babel 转换后的代码),产生的效果如下:
image.png
使用 cheap-module-source-map 效果正常:
image.png

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 都是微内核架构,它的核心非常小只包含主要的代码,大部分功能需要扩展插件来实现。
p.png

Babel 在命令行中使用

Babel 可以之间在命令行中使用,不和 webpack 等构建工具配和使用。

首先需要下载 babel 的核心包和 cli:

  1. npm i @babel/core @babel/cli -D

通过 npx 命令运行:

  1. npx babel 原文件/原文件夹 --out-dir 数据的文件夹

代码转换

因为 Babel 只包含核心功能,所以需要如代码转换功能需要下载对应插件:

安装 @babel/preset-env 预设:

  1. npm i @babel/preset-env -D

命令行执行:

  1. npx babel 原文件/原文件夹 --out-dir 数据的文件夹 --presets=@babel/preset-env

在 webpack 中使用 Babel

首先需要安装 babel 依赖和 babel-loader:

  1. npm i @babel/core babel-loader -D

接下来需要下载 babel 对应的插件,但是对应的插件有很多,一个个安装非常麻烦,所以 Babel 提供了一套预设,预设会根据指定的目标环境(如浏览器版本)加载出需要插件列表并传递给 Babel。

注意:预设会加载 browserslist 的配置查看目标环境,也可以单独配置 targets 属(targets 会覆盖 browserslist)。

下载 @babel/preset-env 预设:

  1. npm i @babel/preset-env -D

配置 webpack:

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.js$/,
  6. use: [
  7. {
  8. loader: 'babel-loader',
  9. options: {
  10. presets: [
  11. '@babel/preset-env',
  12. // [
  13. // // 这里也可以写成一个数组, 目的是为了传递给预设参数
  14. // '@babel/preset-env',
  15. // {
  16. // targets: '> 0.25%, not dead',
  17. // ...配置参数
  18. // }
  19. // ],
  20. ],
  21. },
  22. },
  23. ],
  24. },
  25. ],
  26. },
  27. }

抽离配置文件

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,
      },
    ]
  ],
}

指定不同 core-js 版本还需要额外下载一些依赖包:
image.png

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 校验