source maps
文件,map
文件是一种对应编译文件和源文件的方法sourcemap 类型 | 适用场景 | 优缺点 |
---|---|---|
source-map |
原始代码,需要最好的 sourcemap 质量 | 最高的质量和最低的性能 |
eval-source-map |
原始代码,需要高质量的 sourcemap | 高质量和低性能,sourcemap 可能会很慢 |
cheap-module-eval-source-map |
原始代码,需要高质量和低性能的 sourcemap | 高质量和更低的性能,只有每行的映射 |
cheap-eval-source-map |
转换代码,需要行内 sourcemap | 更高的质量和更低的性能,每个模块被 eval 执行 |
eval |
生成代码,需要带 eval 的构建模式 | 最低的质量和更低的性能,但可以缓存 sourcemap |
cheap-source-map |
转换代码,需要行内 sourcemap | 没有列映射,从 loaders 生成的 sourcemap 没有被使用 |
cheap-module-source-map |
原始代码,需要只有行内的 sourcemap | 没有列映射,但包括从 loaders 中生成的 sourcemap 的每行映射 |
hidden-source-map |
需要隐藏 sourcemap | 能够隐藏 sourcemap |
nosources-source-map |
需要正确提示报错位置,但不暴露源码 | 能够正确提示报错位置,但不会暴露源码 |
sourcemap 类型 | 描述 |
---|---|
source-map |
生成 .map 文件 |
eval |
使用 eval 包裹模块代码 |
cheap |
不包含列信息,也不包含 loader 的 sourcemap |
module |
包含 loader 的 sourcemap,否则无法定义源文件 |
inline |
将 .map 作为 DataURI 嵌入,不单独生成 .map 文件 |
src\index.js
let a=1;
let b=2;
let c=3;
dist\main.js
({
"./src/index.js":
(function (module, exports) {
let a = 1;
let b = 2;
let c = 3;
})
});
//# sourceMappingURL=main.js.map
eval
执行代码 ({
"./src/index.js":
(function (module, exports) {
eval("let a=1;\r\nlet b=2;\r\nlet c=3;\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
eval-source-map
就会带上源码的sourceMap.map
文件cache sourceMap
,从而rebuild的速度会比较快 ({
"./src/index.js":
(function (module, exports) {
eval("let a=1;\r\nlet b=2;\r\nlet c=3;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,In0=\n//# sourceURL=webpack-internal:///./src/index.js\n");
})
});
devtool: "eval-source-map" is really as good as devtool: "source-map", but can cache SourceMaps for modules. It’s much faster for rebuilds.
inline
就是将map作为DataURI嵌入,不单独生成.map文件inline-source-map
({
"./src/index.js":
(function (module, exports) {
let a = 1;
let b = 2;
let c = 3;
})
});
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIj
cheap(低开销)
的sourcemap,因为它没有生成列映射(column mapping),只是映射行数cheap-source-map
cheap-module-source-map
let source = 1;
let map1 = source+=1;
let map2 = source+=2;
source-=2;
source-=1;
npm i webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env style-loader css-loader less-loader less file-loader url-loader -D
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode:'development',
devtool:'cheap-module-source-map',
entry:'./src/index.js',
module: {
rules: [
{
test: /\.js$/,
use: [{
loader:'babel-loader',
options:{
presets:["@babel/preset-env"]
}
}]
}
]
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html'
})
]
}
import './sum';
sum(1,2);
//console.log(window.a.b);
src\sum.js
let sum = (a,b)=>a+b;
export {sum}
devtool: cheap-module-eval-source-map
hidden-source-map
filemanager-webpack-plugin
是一个用于 Webpack 的插件,可以在 Webpack 构建结束后执行一些文件管理操作,比如拷贝文件、删除文件、归档文件等等。它可以让我们在构建结束后自动执行这些文件管理操作,减少手动操作,提高构建效率webpack.SourceMapDevToolPlugin
是 webpack 提供的用于生成 source map
的插件之一。它可以为开发环境下的代码生成 source map
,方便调试和定位问题,该插件的主要作用就是在打包的代码中生成对应的 source map
文件filename
: 指定生成的 source map 文件名,一般为 [file].map 或者 [file].[contenthash].mapappend
: 是否将 source map 添加到已有的 source map 中,如果为 false,则生成的 source map 会覆盖已有的 source mapwebpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'production',
devtool: false,
entry: './src/index.js',
resolveLoader: {
modules: ['node_modules', 'loaders']
},
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"]
}
}]
},
{
test: /\.(jpg|png|gif|bmp)$/,
type: 'asset'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.SourceMapDevToolPlugin({
append: '\n//# sourceMappingURL=http://127.0.0.1:8081/[url]',
filename: '[file].map[query]',
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
{
source: './dist/*.map',
destination: 'D:/aprepare/webpacksource/sourcemap',
},
],
delete: ['./dist/*.map'],
archive: [
{
source: './dist',
destination: './dist/dist.zip',
}
]
}
}
})
]
}
// webpack.config.js
module.exports = {
// ...
devtool: 'hidden-source-map',
// ...
}
sourceMap
的地址(此处需要启用一个静态资源服务,可以使用 http-server
或者通过浏览器打开对应混淆代码的.map
文件 ),完成。本地代码执行构建命令,注意需要打开 sourceMap
配置,编译产生出构建后的代码,此时构建后的结果会包含 sourceMap
文件。关联上 sourceMap
后,我们就可以看到 sources -> page
面板上的变化了regex:(?inx)http:\/\/localhost:8080\/(?<name>.+)$
*redir:http://127.0.0.1:8081/${name}
script.js
let a=1;
let b=2;
let c=3;
java -jar compiler.jar --js script.js --create_source_map ./script-min.js.map --source_map_format=V3 --js_output_file script-min.js
script-min.js
var a=1,b=2,c=3;
script-min.js.map
{
"version": 3,// Sourcemap 版本号,目前最新的版本为 3。
"file": "script-min.js",// 编译后的 JavaScript 代码文件名。
"lineCount": 1,// 编译后的 JavaScript 代码的总行数。
"mappings": "AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;",// 包含将编译后的 JavaScript 代码映射回原始源代码所需的信息。
"sources": ["script.js"],// 包含所有原始源代码文件名的数组。
"names": ["a", "b", "c"]// 包含所有原始代码中出现过的标识符的数组。
}
对应 | 含义 |
---|---|
第一层是行对应 | 以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。 |
第二层是位置对应 | 以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。 |
第三层是位置转换 | 以VLQ编码表示,代表该位置对应的转换前的源码位置。 |
"AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;"
位置 | 含义 |
---|---|
第一位 | 表示这个位置在(转换后的代码的)的第几列 |
第二位 | 表示这个位置属于sources属性中的哪一个文件 |
第三位 | 表示这个位置属于转换前代码的第几行 |
第四位 | 表示这个位置属于转换前代码的第几列 |
第五位 | 表示这个位置属于names属性中的哪一个变量 |
首先,所有的值都是以0作为基数的。其次,第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位,再次,每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成
如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。
const origin = 'feel the force';
const target = 'the force feel';
const mapping = {
mappings: [[10, 'a.js', 0, 0, 'feel'], [-10, 'a.js', 0, 5, 'the'], [4, 'a.js', 0, 4, 'force']],
sources: ['a.js'],//源代码的文件名
names: ['feel', 'the', 'force']
}
/**
转换前的0行0列对应转换后的0行10列 feel
转换前的0行5列对应转换后的0行0列 the
转换前的0行9列对应转换后的0行4列 force
*/
//1.将137改写成二进制形式 10001001
let binary = (137).toString(2);
console.log(binary);//10001001
//2.从右向左七位一组做分组,不足的补在左侧补0
const totalLength = Math.ceil((binary.length) / 7) * 7;
console.log(totalLength);//14
const padded = binary.padStart(totalLength, '0');
console.log(padded);//00000010001001
//3.最后一组开头补0,其余补1
const groups = padded.match(/(\d{7})/g);
const lastGroup = groups.pop();
const vlqCode = groups.map(group => '1' + group).join('') + '0' + lastGroup;
console.log(vlqCode);
//1000000100001001
var base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
* 1. 将137改写成二进制形式 10001001
* 2. 127是正数,末位补0 100010010
* 3. 五位一组做分组,不足的补0 01000 10010
* 4. 将组倒序排序 10010 01000
* 5. 最后一组开头补0,其余补1 110010 001000
* 6. 转64进制 y和I
*/
function encode(num) {
//1. 将137改写成二进制形式,如果是负数的话是绝对值转二进制
let binary = (Math.abs(num)).toString(2);
//2.正数最后边补0,负数最右边补1,127是正数,末位补0 100010010
binary = num >= 0 ? binary + '0' : binary + '1';
//3.五位一组做分组,不足的补0 01000 10010
let zero = 5 - (binary.length % 5);
if (zero > 0) {
binary = binary.padStart(Math.ceil(binary.length / 5) * 5, '0');
}
let parts = [];
for (let i = 0; i < binary.length; i += 5) {
parts.push(binary.slice(i, i + 5));
}// 01000 10010
//4. 将组倒序排序 10010 01000
parts.reverse();// ['00000','00001']
//5. 最后一组开头补0,其余补1 110010 001000
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
parts[i] = '0' + parts[i];
} else {
parts[i] = '1' + parts[i];
}
}
console.log(parts);//[ '110010', '001000' ]
//6.转64进制 y和I
let chars = [];
for (let i = 0; i < parts.length; i++) {
chars.push(base64[parseInt(parts[i], 2)]);
}
return chars.join('')
}
const result = encode(137);
console.log(result);//yI
const result4 = encode(4);
console.log(result4);
const result0 = encode(0);
console.log(result0);
//IAAIA
//[4,0,0,4,0]
//IAAIA
const base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function decode(str) {
let parts = str.split('');
let allNumbers = [];
let numbers = [];
for (let i = 0; i < parts.length; i++) {
const index = base64.indexOf(parts[i]);
const binary = index.toString(2).padStart(6, '0');
numbers.push(binary.slice(1));
let isLast = binary[0] === '0';
if (isLast) {
allNumbers.push(numbers);
numbers = [];
}
}
let result = [];
for (let i = 0; i < allNumbers.length; i++) {
const numbers = allNumbers[i];
numbers.reverse();
let sign;
let binary = numbers.map((number, index) => {
if (index === numbers.length - 1) {
sign = number[number.length - 1] === '0' ? 1 : -1;
return number.slice(0, 4);
}
return number;
}).join('');
result.push(parseInt(binary, 2) * sign);
}
return result;
}
function explain(lines) {
return lines.split(',').map(decode);
}
let positions = explain('AAAA,IAAIA,EAAE,CAAN,CACIC,EAAE,CADN,CAEIC,EAAE');
//后列,哪个源文件,前行,前列,变量
console.log('positions', positions);
// [转换前行,转换前列,转换后行,转换后列]
let offsets = positions.map(item => [item[2], item[3], 0, item[0],]);
console.log('offsets', offsets);
let origin = { row: 0, col: 0 };
let target = { row: 0, col: 0 };
let mapping = [];
for (let i = 0; i < offsets.length; i++) {
let [originRow, originCol, targetRow, targetCol] = offsets[i];
//第一个是绝对位置,
if (i === 0) {
origin = { row: originRow, col: originCol };
target = { row: targetRow, col: targetCol };
} else {//后面的是相对位置
origin.row += originRow;
origin.col += originCol;
target.row += targetRow;
target.col += targetCol;
}
mapping.push(`[${origin.row},${origin.col}]=>[${target.row},${target.col}]`);
}
console.log('mapping', mapping);
positions [
[ 0, 0, 0, 0 ],
[ 4, 0, 0, 4, 0 ],
[ 2, 0, 0, 2 ],
[ 1, 0, 0, -6 ],
[ 1, 0, 1, 4, 1 ],
[ 2, 0, 0, 2 ],
[ 1, 0, -1, -6 ],
[ 1, 0, 2, 4, 1 ],
[ 2, 0, 0, 2 ]
]
offsets [
[ 0, 0, 0, 0 ],
[ 0, 4, 0, 4 ],
[ 0, 2, 0, 2 ],
[ 0, -6, 0, 1 ],
[ 1, 4, 0, 1 ],
[ 0, 2, 0, 2 ],
[ -1, -6, 0, 1 ],
[ 2, 4, 0, 1 ],
[ 0, 2, 0, 2 ]
]
mapping [
'[0,0]=>[0,0]',
'[0,4]=>[0,4]',
'[0,6]=>[0,6]',
'[0,0]=>[0,7]',
'[1,4]=>[0,8]',
'[1,6]=>[0,10]',
'[0,0]=>[0,11]',
'[2,4]=>[0,12]',
'[2,6]=>[0,14]'
]