概述

webpack-dev-server默认会将构建结果输出到dist, 可以通过loader将开发环境的源代码转换成能够在生产环境中运行的代码。这是一种进步,但是这种进步的同时也就意味着我们在实际生产环境当中运行的代码与我们开发阶段所编写的代码之间会有很大的差异。

那在这种情况下如果我们需要去调试我们的应用,又或是我们运行应用的过程中出现了意料之外的错误,那我们将无从下手。

这是因为我们无论是调试还是报错,那他都是基于转换过后的代码来运行的。那Source Map就是解决这一类问题最好的一个办法。

他的名字就已经表述了他的作用叫做源代码地图,那他就是用来映射我们转换过后的代码与源代码的关系,那一段转换过后的代码呢我们通过转换过程当中生成的这个source map文件就可以逆向得到源代码。

那目前很多第三方的库再去发布的文件当中都会有一个.map后缀的source map文件,那例如jQuery,我们可以打开jQuery的source map文件然后看一下,这是一个JSON格式的文件,为了更容易阅读我们格式化一下。

那这个JSON文件里面记录的就是我们转换过后的代码与转换之前代码之间的映射关系。主要有这么几个属性我们简单来看一下。

首先就是version这个属性指的是我们当前这个文件所使用的的source map标准的版本。

然后就是source属性,那这个属性记录的就是我们转换之前源文件的名称。那因为很有可能是多个文件合并,转换成了一个文件,所以说这里的属性是一个数组。

再然后就是names属性,这个属性指的是我们的源代码当中使用的一些成员名称,我们都知道,在压缩代码时我们会将开发阶段编写的那些有意义的变量去替换为一些简短的字符从而去压缩我们整体代码的体积。那这个属性中记录的就是我们原始对应的那些名称。

最后是mappings属性, 那这个属性其实是我们整个source map文件的核心属性。那他是一个base64-vl编码的字符串。那这个字符串他记录的信息就是我们转换过后代码当中的字符与我们转换之前所对应的映射关系。

{
    "version": 3,
    "file": "jquery.min.js",
    "sources": [
        "jquery.js"
    ],
    "names": [
        "window",
        "undefined",
        "readyList",
        "rootjQuery",
        "core_strundefined",
        "location",
        "document",
        "docElem",
        "documentElement",
        "_jQuery",
        "jQuery",
        "_$",
        "$",
        "class2type"
        ...
    ],
    "mappings": ";;;CAaA,SAAWA,EAAQC,GAOnB,GAECC,GAGAC,EAIAC,QAA2BH,GAG3BI,EAAWL,EAAOK,SAClBC,EAAWN,EAAOM,SAClBC,EAAUD,EAASE,gBAGnBC,EAAUT,EAAOU,OAGjBC,EAAKX,EAAOY,EAGZC,KAGAC,
    ...
    "
}

有了这样一个文件过后一般我们会在转换过后的代码当中通过添加一行注释的方式来去引入这个source map文件。

不过呢,source map这个特性只是帮助开发者更容易去调试和定位错误的,所以说他对生产环境其实没有什么太大的意义。

在最新版的jQuery当中他已经去除了引用source map的注释,我们这里想要去尝试的话需要手动的添加回来。这里我们在jQuery.min.js这个文件当中最后一行我们去添加一个这样的注释。这个注释有一个特定的格式。

就是以#开头,然后source mapping url = 我们jQuery的map文件

//# sourceMappingURL = jquery-3.4.1.min.map

那这样的话,我们在浏览器当中如果说打开了开发人员工具的话,那开发人员工具去加载到的这个js文件最后有这么一行注释他就会自动去请求这个source map文件。

然后根据这个文件的内容逆向解析出来我们对应的源代码。以便于我们的调试,同时因为有了映射的关系所以说我们源代码当中如果说出现了错误那也就很容易能定位到源代码当中对应的位置了。

那我们这里回到浏览器。然后我们打开开发人员工具找到source面板,那在source面板我们就能看到转换之前的jquery源代码。

他也可以显示在里面,那我们这里还可以在我们的代码当中添加一个断点,然后我们刷新页面。这个时候我们就可以单步去调试整个jquery的源代码。

那此时调试过程当中使用的就不再是我们之前所使用的的压缩过后的代码,而是真正意义上的源代码,这就是source map的一个作用。

那这里简单总结一下,source map的作用解决的就是我们在前端方向引入了构建编译之类的概念过后导致我们前端编写的源代码与运行的代码之间不一样所产生的那些调试的问题。

配置source map

webpack可以为我们的打包结果生成对应的source map文件,那用法上也非常简单,不过他提供了很多不同的模式。那这就导致大部分的初学者可能会比较懵。

那接下来我们一起来研究一下,webpack中如何去配置使用source map以及他几种不同模式之间的差异,那我们回到配置文件当中。

这哭我们需要使用的一个配置属性叫做devTool 这个属性就是用来配置我们开发过程中的辅助工具,也就是与source map 相关的一些功能配置。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
    },
    devtool: 'source-map',
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html'
        }),
        new CopyWebpackPlugin([
            'public/**'
        ])
    ]
}

这里我们可以直接将这个属性设置为source-map, 然后我们打开命令行终端,运行webpack打包。

打包完成过后我们打开所生成的dist目录, 可以发现在这个目录中生成了一个对应的bundle.js.map文件。而且我们打开bundle.js找到这个文件的最后那这个文件的最后也通过注释的方式引入了这个source-map文件。

/******/ (function(modules) { // webpackBootstrap
/******/ })
/************************************************************************/
/******/ ([
/******/ ]);
//# sourceMappingURL=bundle.js.map

这里我们再回到命令行终端,运行服务打开浏览器查看。此时如果js存在错误,我们就可以通过console中的提示直接定位错误所在的源代码当中的位置。当然了,如果你需要去调试源代码的时候,这里我们也就可以直接去调试我们所编写的源代码了。

如果说你只是需要去使用source map的话,那这里就已经可以实现了。但是呢,如果你只是这么去使用的话,那实际的效果呢就会差的比较远。

那为什么这么说呢,因为截止到目前,webpack对source map的风格支持,支持了很多种(12)种。也就是说他有很多实现方式。那每种方式所生成的source map 效果以及生成source map的速度,都是不一样的。

很简单也很明显的一个道理就是效果最好的,一般他的生成速度也就会最慢。而速度最快的,一般生成出来的这个source map文件也就没有什么效果。那具体哪种方式才是最好或者说最适合我们的,那这里我们还需要继续去探索。

eval模式下的Source Map

可以使用source-map这个值来支持很多其他的模式, 那具体的我们可以参考文档当中有一个不同模式之间的一个对比表。

这张表中分别从初次构建速度,监视模式,重新打包速度还有就是是否适合在生产环境中使用以及所生成的source map的质量这四个维度去对比了这些不同方式之间的一些差异。

那表格当中的对比呢可能不够鲜明。所以说接下来我们配合表格中的介绍,然后通过具体的尝试来去体会这些不同模式间的差异。从而找到适合自己的最佳实践。

首先我们来看一个叫做eval的模式。

eval他是js当中的一个函数,他可以用来去运行我们字符串当中的js代码,这里我们可以尝试一下。

eval('console.log(123)');

默认情况下这段代码他会运行在一个临时的虚拟机环境中。我们可以通过source url来去声明这段代码所处的文件路径,我们这里再来尝试执行以下。不过我们在这段js代码字符串当中去添加一段注释内容。

就是//# sourceURL=./foo/bar 然后我们执行,此时呢我们这段代他所运行的这个环境呢就是./foo/bar.js。

这也就意味着,意味着我们可以通过sourceURL来去改变我们通过eval执行的这段代码所处的这样一个环境的名称,其实他还是运行在虚拟机环境中。只不过他告诉了执行引擎。这段代码所属的这个文件路径。这只是一个标识而已。

eval('console.log(123) //# sourceURL=./foo/bar.js');

了解了这样一个特点过后我们回到配置文件当中,这里我们将devtool属性设置为eval,也就是使用eval模式,然后我们回到命令行终端。再次运行打包。

那打包完成过后呢,我们运行一下这个应用。然后回到浏览器,刷新一下页面。

那此时根据控制台的提示,我们就能找到这个错误所出现的文件,但是当我们打开这个文件,我们看到的却是打包过后的模块代码。

那这是为什么呢,因为在这种模式下,他会将我们每个模块所转换过后的代码都放在eval函数当中去执行,并且在这个eval函数执行的字符串最后通过sourceURL的方式去说明所对应的文件路径。

那这样的话,浏览器在通过eval去执行这段代码的时候就知道这段代码所对应的源代码是哪一个文件。那从而去实现我们定位错误所出现的文件,只能取定位文件。

那这种模式下他不会去生成source map文件,也就是说,它实际上跟source-map没有什么太大的关系,所以说他的构建速度也就是最快的。但是呢他的效果也就很简单,他只能定位我们源代码文件的名称。而不知道具体的行列信息。

不同devtool之间的差异。

对比不同模式的source map之间的差异。这里我们使用一个新的项目来去同时创建出不同模式下的打包结果。来去通过具体的试验来去横向对比他们之间的差异。

我们回到项目中,目前我们的项目中只有两个js模块,其中main.js当中故意加入了一个运行时的错误。

import createHeading from './heading.js'

const heading = createHeading();

document.body.append(heading);

console.log('main.js running');

console.log111('main.js running');

我们打开webpack的配置文件,在这个文件中我们这里定义一个数组,数组中的每一个成员就是devtool配置取值的一种,然后我们这里导出了一个配置对象。

const allModes = [
    'eval',
    'cheap-eval-source-map',
    'cheap-module-eval-source-map',
    'eval-source-map',
    'cheap-module-source-map',
    'inline-cheap-source-map',
    'inline-cheap-module-source-map',
    'source-map',
    'inlie-source-map',
    'hidden-source-map',
    'nosource-source-map',
]

webpack的配置对象可以是一个数组,数组中给的每个元素就是一个单独的打包配置。这样一来我们就可以在一次打包过程中同时去执行多个打包任务。

例如我们这里导出一个数组,然后我们再数组当中添加两个打包配置。我们将它的entry属性,也就是打包的入口都设置为src/main.js,不过他们的输出文件的文件名是不同的。

module.exports = [
    {
        entry: './src/main.js',
        output: {
            filename: 'a.js'
        }
    },
    {
        entry: './src/main.js',
        output: {
            filename: 'b.js'
        }
    }
]

完成以后我们进行打包,这一次打包过程就会有两个子任务进行工作,而我们dist文件夹中生成的结果也就是两个文件。

那了解了webpack的这种用法之后我们就回到配置文件当中。删除刚刚的尝试代码,这里我们通过遍历,去为每一种模式单独去创建一个打包配置,那这样的话我们就可以在一次打包当中同时去生成所有模式下的不同结果。这样的话比我们一个一个去试验要更快一些,也会更明显一些。

每个配置项之中我们先去定义devtool属性,这个属性的值就是我们刚刚遍历的这个名称。然后我们将mode设置为none,那这样就确保webpack内部不会去做额外的处理,那紧接着我们再来设置这个任务的打包入口,以及他输出文件的名称。这里我们将输出文件的名称设置为js目录下,以模式名称命名的一个js文件。

为什么这个文件要放在js目录中,继续往下看。

再下面我们为这个js模块配置一个babel-loader,这里配置babel-loader的目的是接下来我们对比当中能够辨别其中一类模式的差异。

最后我们再来配置一个html-webpack-plugin, 也就是为我们每一个打包任务去生成一个html文件。通过前面的了解我们知道,html-webpack-plugin可以为我们生成使用我们打包结果的html,待会我们就是通过这些html在浏览器当中去尝试这些不同的打包结果。

const HtmlWebpackPlugin = require('html-webpack-plugin');

const allModes = [
    'eval',
    'cheap-eval-source-map',
    'cheap-module-eval-source-map',
    'eval-source-map',
    'cheap-module-source-map',
    'inline-cheap-source-map',
    'inline-cheap-module-source-map',
    'source-map',
    'inlie-source-map',
    'hidden-source-map',
    'nosource-source-map',
];

module.exports = allModes.map(item => {
    return {
        mode: 'none',
        devtool: item,
        entry: './src/main.js',
        output: {
            filename: `js/${item}.js`,
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                filename: `${item}.html`
            })
        ]
    }
})

配置完成过后我们再次打开命令行终端。运行打包。此时这个打包过程就会生成所有模式下的打包结果,然后我们再启动服务,打开浏览器,那此时我们就能在页面中看到所有不同模式下的html,如果刚刚我们没有把js文件单独输出到一个目录的话,这里的文件就会非常的多,我们看起来就不会那么的容易。

有了这样一个页面过后我们接下来就可以在这个里面去单独对比不同模式之间的差异了。

模式对比

有了这些不同模式下的打包结果过后,那接下来我们就可以一个一个仔细去对比了,我们这里先看几个比较典型的模式, 然后找出他们之间的关系。

  1. 首先我们看下eval模式。

这个模式刚刚我们已经单独看过了,他就是将我们的模块代码放到eval函数当中去执行。并且通过sourceURL去标注我们这个模块文件的路径。这种模式下并没有生成对用的source-map他只能定位我们是哪一个文件出了错误。

  1. 接下来我们看下eval-source-map模式。

那这个模式同样也是使用eval函数去执行模块代码,不过这里有所不同的是他除了可以帮我们定位我们错误出现的文件。还可以定位到具体的行和列信息,因为在这种模式下,相比于eval他生成了source-map。

  1. cheap-eval-source-map

这个模式的名字我们差不多就可以推断出一些信息,他其实就是在刚刚eval-source-map的基础之上加了一个cheap,也就是廉价的,那用计算机行业经常说的一个词就是阉割版的source-map。

为什么这么说呢,因为他虽然也生成了source-map,但这种模式下的source-map他只能帮我们定位到行,而没有列的信息。那也就是少了一点效果。少了一点效果所以生成速度自然也就会快很多。

  1. cheap-module-eval-source-map

根据这个名字我们慢慢就发现了,webpack这些模式的名字好像不是乱起的。他好像有某种规律,这里其实就是刚刚的模式基础之上多了一个module,在这种模式下的特点可能乍一看不会那么明显。因为他也就只能定位到行。

那我们把刚刚cheap-eval-source-map的模式仔细进行一个对比。通过对比会发现,cheap-module-eval-source-map定位源代码跟我们编写的源代码是一模一样的。而cheap-eval-source-map他显示的是我们经过babel转换过后的结果。

那这样的话这两者之间的差异也就出来了。这也就是为什么之前配置的时候会给js文件单独配一个loader的原因。因为带有module这种模式下他解析出来的源代码是没有经过loader加工的我们真正手写的那些源代码。而不带module是加工过后的一个结果。

如果我们想要和我们真正手写代码一样的源代码的话,那我们就需要选择cheap-module-eval-source-map这种模式。

了解了以上这些模式过后,基本上就算是通盘了解了所有的模式,因为其他的模式无外乎就是把这几个特点再次排列组合罢了。例如下面的。cheap-source-map

  1. cheap-source-map

没有eval,也就爱意味着没有用eval的方式去执行模块代码,没有module的话也就意味着他反过来的源代码是loader处理过后的代码。

另外还有几个模式,首先第一个看下inline-source-map

  1. inline-source-map

那他跟普通的source-map其实效果上是一样的,只不过source-map的模式下,他的source-map文件是以物理文件的方式存在,而我们inline-source-map的话他使用的是dataurl的方式去将我们的source-map以dataurl嵌入到我们的代码当中。

那我们之前遇到的eval-source-map他其实也是使用这种行内的方式把我们的source-map嵌入进来
,那这种方式实际上,个人觉得是最不可能用到的,因为他把source-map放到源代码当中过后那这时候就导致这个代码的体积会变大很多。

  1. hidden-source-map

那除此之外还有一个叫做hidden-source-map的模式,那这个模式下我们在开发工具中是看不到source-map的效果的。但是我们回到开发工具当中去找一下这个文件我们会发现,他确实是生成了source-map文件。

那这就跟jq是一样的,在构建过程当中,生成了source-map文件,但是他在代码当中并没有通过注释的方式去引入这个文件。所以说我们在开发工具中看不到效果。

那这个模式实际上是我们在开发一些第三方包的时候会比较有用,我们需要去生成source-map但是呢我们不想在我们的代码当中直接去引用他们,那一旦在使用我们这个包的开发者出现了问题,他可以再把这个source-map再引入回来,或者通过其他的方式去使用source-map。

source-map还有很多其他的使用方式,通过http的响应头也可以去使用,那这些我们就不在这里扩展了。

  1. nosources-source-map

然后回过来我们再看,最后还有一个叫做nosources-source-map的source-map模式,那这个模式下,我们能看到错误出现的位置,但是我们点击这个错误信息,我们点进去过后是看不到源代码的。

那这个nosource指的就是没有源代码,但是呢他同样给我们提供了行列信息,那这样的话对于我们来讲的话,我们还是结合我们自己编写的源代码还是可以找到错误出现的位置。只是我们在开发工具当中看不到源代码而已。

这是为了在生产环境当中去保护我们的源代码不会被暴露的这样一种情况,那这是nosource-source-map这个模式。

那以上我们介绍了很多种的source-map也做了一些具体的对比,通过这些对比,大家要能总结出来这个source-map里面这几个核心关键词他们的一些特点,然后对于其他几个模式我们没有介绍到的,你就很容易能知道他们一些特点了。

那可能了解很多这些模式过后呢对大家来讲的话最痛苦的一件事情就是选择一个合适的source-map模式,那这个我们再接着来看。

选择SourceMap模式

虽然webpack支持各种各样的SourceMap模式,但是其实我们掌握他们的特点过后呢我们发现一般我们再应用开发时也就只会用到其中的几种。根本就没有选择上需要纠结的点。

那这里呢我来介绍一下我个人在开发时的一些选择,那首先先说开发过程中。也就是开发环境下。

在开发环境下我会选择cheap-module-eval-source-map那具体的原因有三点。

第一就是我编写代码的风格一般会要求每一行代码不会超过80个字符,那对我而言的话能够帮我定位到行也就够了。

因为每一行里面最多也就80个字符,我能够很容易找到对应的位置。

然后第二就是我使用框架的情况会比较多,那我们以react和vue来说,无论是我们使用jsx还是vue的单文件组件,那loader转换过后的代码和我们转换之前都会有很大的差别,那我这里需要去调试我转换
之前的源代码,而不是转换过后的,所以我们要选择有module的这种方式。

那第三点就是虽然这种cheap-module-eval-source-map他的启动就是打包启动速度会比较慢一些,但是呢大多说时间我都是在使用webpack-dev-server以监视模式去重新打包,而不是每次都启动打包。

所以说这种模式下他重新打包速度比较快,那这也是我选择他的一个原因。

那综上所述在开发环境我选择就是cheap-module-eval-source-map

至于发布前的打包,也就是我们针对于生产环境的打包,那我会选择none,也就是不生成source-map。

那原因也很简单,因为source-map会暴露我源代码到生产环境,那这样的话但凡是有一点技术的人都可以很容易去复原我项目当中绝大多数的源代码,而这一点其实被很多开发者可能都忽略掉了。他们只是认为source-map能够带来便利,但是呢这个便利的同时也会有一些隐患,所以这个点是要注意的。

那其次我个人认为调试和报错,找错误这些都应该是开发阶段的事情,你应该在开发阶段就尽可能把所有的问题和隐患都找出来,而不是到了生产环境让全民去帮你公测。所以说这种情况我就尽量避免在生产环境去使用source-map。

那如果说你对你的代码实在是没有信心的话,那我建议你选择nosources-source-map模式,那这样的话出现错误的话在控制台当中就可以找到源代码对应的位置,但是不至于向外暴露你的源代码内容。

当然了,这个过程中我的这些选择实际上也没有绝对,那我们去理解这些模式之间差异的目的,就是为了可以让我们在不同环境当中,然后快速去选择一个合适的模式,而不是去寻求一个通用的法则。

在开发行业,没有绝对的通用法则。