自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解讀SourceMap

開發(fā) 前端
針對不同類型的ast節(jié)點(diǎn)根據(jù)節(jié)點(diǎn)的含義執(zhí)行word/space/token/newline等方法生成代碼,這些方法里都會執(zhí)行append方法添加要生成的字符串代碼。

SourceMap的用途

前端工程打包后代碼會與源碼產(chǎn)生不一致,當(dāng)代碼運(yùn)行出錯(cuò)時(shí)控制臺會定位出錯(cuò)代碼的位置。SourceMap的用途是可以將轉(zhuǎn)換后的代碼映射回源碼,如果你部署了js文件對應(yīng)的map文件資源,那么在控制臺里調(diào)試時(shí)可以直接定位到源碼的位置。

SourceMap的格式

我們可以生成一個(gè)SouceMap文件看看里面的字段分別都對應(yīng)什么意思,這里使用webpack打包舉例。

源碼:

//src/index.js
function a() {
for (let i = 0; i < 3; i++) {
console.log('s');
}
}
a();

打包后的代碼:

//dist/main-145900df.js
!function(){for(let o=0;o<3;o++)console.log("s")}();
//# sourceMappingURL=main-145900df.js.map

.map文件:

//dist/main-145900df.js.map
{
"version": 3,
"file": "main-145900df.js",
"mappings": "CAAA,WACE,IAAK,IAAIA,
EAAI,EAAGA,EAAI,EAAGA,IACrBC,QAAQC,IAAI,KAGhBC",
"sources": ["webpack://source-map-webpack-demo/./src/index.js"],
"sourcesContent": ["function a() {\n for (let i = 0; i < 3; i++) {\n console.log('s');\n }\n}\na();"],
"names": ["i", "console", "log", "a"],
"sourceRoot": ""
}
  • version:目前source map標(biāo)準(zhǔn)的版本為3;
  • file:生成的文件名;
  • mappings:記錄位置信息的字符串;
  • sources:源文件地址列表;
  • sourcesContent:源文件的內(nèi)容,一個(gè)可選的源文件內(nèi)容列表;
  • names:轉(zhuǎn)換前的所有變量名和屬性名;
  • sourceRoot:源文件目錄地址,可以用于重新定位服務(wù)器上的源文件。

這些字段里大部分都很好理解,接下來主要解讀mappings這個(gè)字段是通過什么規(guī)則來記錄位置信息的。

?mappings字段的定義規(guī)則?

"mappings": "CAAA,WACE,IAAK,IAAIA,
EAAI,EAAGA,EAAI,EAAGA,IACrBC,QAAQC,IAAI,KAGhBC",

為了盡可能減少存儲空間但同時(shí)要達(dá)到記錄原始位置和目標(biāo)位置映射關(guān)系的目的,mappings字段按照了一些特殊的規(guī)則來生成。

  1. 生成文件中的一行作為一組,用“;”隔開。
  2. 連續(xù)的字母共同表示一個(gè)位置信息,用逗號分隔每個(gè)位置信息。
  3. 一個(gè)位置信息由1、4或5個(gè)可變長度的字段組成。
  1. // generatedColumn, [sourceIndex, originalLine, orignalColumn, [nameIndex]]
  2. 第一位,表示這個(gè)位置在轉(zhuǎn)換后的代碼第幾列,使用的是相對于上一個(gè)的相對位置,除非這是這個(gè)字段的第一次出現(xiàn)。
  3. 第二位(可選),表示所在的文件是屬于sources屬性中的第幾個(gè)文件,這個(gè)字段使用的是相對位置。
  4. 第三位(可選),表示對應(yīng)轉(zhuǎn)換前代碼的第幾行,這個(gè)字段使用的是相對位置。
  5. 第四位(可選),表示對應(yīng)轉(zhuǎn)換前代碼的第幾列,這個(gè)字段使用的是相對位置。
  6. 第五位(可選),表示屬于names屬性中的第幾個(gè)變量,這個(gè)字段使用的是相對位置。
  1. 字段的生成原理是將數(shù)值通過vlq-base64編碼轉(zhuǎn)換成字母。

?vlq原理?

vlq是Variable-length quantity的縮寫,是一種通用的,使用任意位數(shù)的二進(jìn)制來表示一個(gè)任意大的數(shù)字的一種編碼方式。

SourceMap中的編碼流程是將位置從十進(jìn)制數(shù)值—>二進(jìn)制數(shù)值—>vlq編碼—>base64編碼最終生成字母。

//   Continuation
// | Sign
// | |
// V V
// 101011

vlq編碼的規(guī)則:

  • 一個(gè)數(shù)值可能由多個(gè)字符組成
  • 對于每個(gè)字符使用6個(gè)2進(jìn)制位表示

如果是表示數(shù)值的第一個(gè)字符中的最后一個(gè)位置,則為符號位。

否則用于實(shí)際有效值的一位。

0為正,1為負(fù)(SourceMap的符號固定為0),

第一個(gè)位置是連續(xù)位,如果是1,代表下一個(gè)字符也屬于同一個(gè)數(shù)值;如果是0,表示這個(gè)字符是表示這個(gè)數(shù)值的最后一個(gè)字符。

最后一個(gè)位置

  • 至少含有4個(gè)有效值,所以數(shù)值范圍為(1111到-1111)即-15到15的可以由一個(gè)字符表示。

數(shù)值的第一個(gè)字符有4個(gè)有效值

之后的字符有5個(gè)有效值

最后將6個(gè)2進(jìn)制位轉(zhuǎn)換成base64編碼的字母,如圖。

圖片

舉例編碼數(shù)值29

數(shù)值29(十進(jìn)制)=11101(二進(jìn)制)

1|1101

先取低四位,數(shù)值的第一個(gè)字符有四個(gè)有效值1101

11010-----------最后加上符號位

111010----------開頭加上連續(xù)位1(后面還有字符表示同一個(gè)數(shù)值)

6---------------轉(zhuǎn)換為base64編碼對應(yīng)是6

數(shù)值的第二個(gè)字符

00001----------補(bǔ)充有效位

000001--------開頭加上連續(xù)位0(表示是數(shù)值的最后一個(gè)字符)

B---------------轉(zhuǎn)換為base64編碼

29=》6B

我們將上述轉(zhuǎn)換的規(guī)則通過代碼方式呈現(xiàn):

代碼實(shí)現(xiàn)vlq編碼

先在最后添加一個(gè)符號位,從低位開始截取5位作為一個(gè)字符,截取完若還有數(shù)值則在截取的5位前添加連續(xù)位1,即生成好一個(gè)字符;最后一個(gè)字符的數(shù)值直接與011111進(jìn)行與運(yùn)算即可。

//https://github.com/mozilla/source-map/blob/HEAD/lib/base64-vlq.js
const base64 = require("./base64");

//移動位數(shù)
const VLQ_BASE_SHIFT = 5;

// binary: 100000
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
//1左移5位:100000=32

// binary: 011111
const VLQ_BASE_MASK = VLQ_BASE - 1;

// binary: 100000
const VLQ_CONTINUATION_BIT = VLQ_BASE;

//符號位在最低位
//1.1左移一位并在最后加一個(gè)符號位
function toVLQSigned(aValue) {
return aValue < 0 ? (-aValue << 1) + 1 : (aValue << 1) + 0;
}

/**
* Returns the base 64 VLQ encoded value.
*/
function base64VLQ_encode(aValue) {
let encoded = "";
let digit;

let vlq = toVLQSigned(aValue);//第一步:左移一位,最后添加符號位

do {
digit = vlq & VLQ_BASE_MASK;
//第二步:vlq和011111進(jìn)行與運(yùn)算,獲取字符中已經(jīng)生成好的后5位
//從低位的5位開始作為第一個(gè)字符
vlq >>>= VLQ_BASE_SHIFT;//vlq=vlq>>>5
//第三步:vlq右移5位用于截取低位的5位,對剩下的數(shù)值繼續(xù)進(jìn)行操作
if (vlq > 0) {
//說明后面還有數(shù)值,則要在現(xiàn)在這個(gè)字符開頭加上連續(xù)位1
digit |= VLQ_CONTINUATION_BIT;//digit=digit|100000,與100000進(jìn)行或運(yùn)算
}
encoded = encoded+base64.encode(digit);//第四步:生成的vlq字符進(jìn)行base64編碼并拼接
} while (vlq > 0);

return encoded;
};
exports.encode = base64VLQ_encode;

舉例解碼字符6B

6B

第一個(gè)字符

6=>111010--------base64解碼并轉(zhuǎn)換為二進(jìn)制

111010------------符號位

110110------------連續(xù)位(表示后面有字符表示同一個(gè)數(shù)值)

第一個(gè)字符有效值value=1101

第二個(gè)字符

B=>000001------base64解碼并轉(zhuǎn)換為二進(jìn)制

000001----------有效值

000001----------連續(xù)位(表示后面沒有字符表示同一個(gè)數(shù)值)

第二個(gè)字符的有效值value=00001

合并value=000011101轉(zhuǎn)為十進(jìn)制29

代碼實(shí)現(xiàn)vlq解碼

從左到右開始遍歷字符,對每個(gè)字符都先去除連續(xù)位剩下后5位數(shù)值,將每個(gè)字符的5位數(shù)值從低到高拼接,最后去除處在最低一位的符號位。

//https://github.com/Rich-Harris/vlq/blob/HEAD/src/index.js
/** @param {string} string */
export function decode(string) {
/** @type {number[]} */
let result = [];

let shift = 0;
let value = 0;

for (let i = 0; i < string.length; i += 1) {//從左到右遍歷字母
let integer = char_to_integer[string[i]];//1.base64解碼

if (integer === undefined) {
throw new Error('Invalid character (' + string[i] + ')');
}

const has_continuation_bit = integer & 100000;//2.獲取連續(xù)位標(biāo)識

integer =integer & 11111;//3.移除符號位獲取后5位

value = value + (integer << shift);
//4.從低到高拼接有效值

if (has_continuation_bit) {
//5.有連續(xù)位
shift += 5;//移動位數(shù)
} else {
//6.沒有連續(xù)位,處理獲取到的有效值value
const should_negate = value & 1;//獲取符號位

value =value >>>1;//7.右移一位去除符號位,獲取最終有效值

if (should_negate) {
result.push(value === 0 ? -0x80000000 : -value);
} else {
result.push(value);
}

// reset
value = shift = 0;
}
}

return result;
}

整個(gè)轉(zhuǎn)換流程舉例

源碼:

//src/index.js

function a() {
for (let i = 0; i < 3; i++) {
console.log('s');
}
}
a();

打包后的代碼:

//dist/main-145900df.js

!function(){for(let o=0;o<3;o++)console.log("s")}();
//# sourceMappingURL=main-145900df.js.map

.map文件:

//dist/main-145900df.js.map
{
"version": 3,
"file": "main-145900df.js",
"mappings": "CAAA,WACE,IAAK,IAAIA,
EAAI,EAAGA,EAAI,EAAGA,IACrBC,QAAQC,IAAI,KAGhBC",
"sources": ["webpack://source-map-webpack-demo/./src/index.js"],
"sourcesContent": ["function a() {\n for (let i = 0; i < 3; i++) {\n console.log('s');\n }\n}\na();"],
"names": ["i", "console", "log", "a"],
"sourceRoot": ""
}

CAAA

[1,0,0,0]

轉(zhuǎn)換后的代碼的第1列

sources屬性中的第0個(gè)文件

轉(zhuǎn)換前代碼的第0行。

轉(zhuǎn)換前代碼的第0列。

對應(yīng)function

WACE

[11,0,1,2]

轉(zhuǎn)換后的代碼的第12(11+1)列

sources屬性中的第0個(gè)文件

轉(zhuǎn)換前代碼的第1行。

轉(zhuǎn)換前代碼的第2列。

對應(yīng)for

IAAK

[4,0,0,5]

轉(zhuǎn)換后的代碼的第16(12+4)列

sources屬性中的第0個(gè)文件

轉(zhuǎn)換前代碼的第1行。

轉(zhuǎn)換前代碼的第7(2+5)列。

對應(yīng)let

SourceMap使用的規(guī)則是如何優(yōu)化存儲位置信息空間的?

SourceMap規(guī)范進(jìn)行了版本迭代,最初,規(guī)范對所有映射都有非常詳細(xì)的輸出,導(dǎo)致SourceMap大約是生成代碼的10倍。第二個(gè)版本減少了50% 左右,第三個(gè)版本又減少了50% 。

因?yàn)槿绻傻奈恢眯畔?nèi)容比源碼還多未免有些得不償失,所以這樣的規(guī)則是在盡可能的減小存儲空間。

我們可以來總結(jié)一下這個(gè)規(guī)則里使用到的優(yōu)化點(diǎn):

  1. 使用相對位置,使位置數(shù)值盡可能小,在后續(xù)計(jì)算中獲取真實(shí)的位置數(shù)值,從而減少存儲空間;
  2. 使用vlq-base64編碼減少存儲空間,如32000=》ggxT,通過計(jì)算減少存儲空間;
  3. 行數(shù)信息直接用;分割來表示。

解析babel生成SourceMap的實(shí)現(xiàn)方式

我們?nèi)粘5母鞣N轉(zhuǎn)譯/打包工具是如何生成SourceMap的,這里來解析一下babel生成SourceMap的實(shí)現(xiàn)方式。

我們大概需要以下三個(gè)步驟來生成SourceMap:

  1. 獲取源碼的行列信息
  2. 獲取生成代碼的行列信息
  3. 將前后一一對應(yīng)起來,然后進(jìn)行vlq-base64編碼并按照規(guī)則生成sourcemap文件。

babel流程

babel主要執(zhí)行了三個(gè)流程:解析(parse),轉(zhuǎn)換(transform),生成(generate)。

parse解析階段(獲得源碼對應(yīng)的ast)=》transform(plugin插件執(zhí)行轉(zhuǎn)換ast)=》generate通過ast生成代碼

parse和transform階段

在解析和轉(zhuǎn)換的階段,源碼對應(yīng)的ast經(jīng)過一些plugin的執(zhí)行后節(jié)點(diǎn)的類型或者值會發(fā)生改變,但節(jié)點(diǎn)中有一個(gè)loc屬性(類型為SourceLocation)會一直記錄著源碼最開始的行列位置,所以獲取到源碼的ast就能夠得到源碼中的行列信息。

generate階段生成SourceMap

generator階段通過ast生成轉(zhuǎn)譯后的代碼,在這個(gè)階段會對ast樹進(jìn)行遍歷。

針對不同類型的ast節(jié)點(diǎn)根據(jù)節(jié)點(diǎn)的含義執(zhí)行word/space/token/newline等方法生成代碼,這些方法里都會執(zhí)行append方法添加要生成的字符串代碼。

在此之中有一個(gè)記錄生成代碼的行列信息屬性會按照添加的字符串長度進(jìn)行不斷的累加,從而得到轉(zhuǎn)譯前后行列信息的對應(yīng)。

//packages/babel-generator/src/index.ts
export default function generate(
ast: t.Node,
opts?: GeneratorOptions,
code?: string | { [filename: string]: string },
) {
const gen = new Generator(ast, opts, code);
//1.傳遞ast新建一個(gè)Generator對象
return gen.generate();
}
class Generator extends Printer {
generate() {
return super.generate(this.ast);
}
}

//packages/babel-generator/src/printer.ts
class Printer {
generate(ast) {
this.print(ast);
//2.通過ast生成代碼
this._maybeAddAuxComment();

return this._buf.get();
}

print(node, parent?) {

if (!node) return;

const oldConcise = this.format.concise;
if (node._compact) {
this.format.concise = true;
}

const printMethod = this[node.type];
//獲取不同節(jié)點(diǎn)類型對應(yīng)的生成方法

//....


//調(diào)用
this.withSource("start", loc, () => {
printMethod.call(this, node, parent);
});

// this._printTrailingComments(node);

// if (shouldPrintParens) this.token(")");

// end
this._printStack.pop();

this.format.concise = oldConcise;
this._insideAux = oldInAux;
}
}

例如,遍歷到一個(gè)SwitchCase類型的ast節(jié)點(diǎn),會在里面調(diào)用Printer對象的word/space/print/token等方法,而這些方法內(nèi)部都會調(diào)用append方法用于逐個(gè)添加要生成的字符串,并計(jì)算得到對應(yīng)的行列信息。

//packages/babel-generator/src/generators/statements.ts
export function SwitchCase(this: Printer, node: t.SwitchCase) {
if (node.test) {
this.word("case");
this.space();
this.print(node.test, node);//用于遍歷,執(zhí)行節(jié)點(diǎn)下的節(jié)點(diǎn)的方法
this.token(":");
} else {
this.word("default");
this.token(":");
}

if (node.consequent.length) {
this.newline();
this.printSequence(node.consequent, node, { indent: true });
}
}

Printer對象中聲明了word/space/print/token等方法,這些方法都會將字符串添加到Buffer對象中。

//packages/babel-generator/src/printer.ts
class Printer {
constructor(format: Format, map: SourceMap) {
this._buf = new Buffer(map);
}

//...

_append(str: string, queue: boolean = false) {
if (queue) this._buf.queue(str);
else this._buf.append(str);
}

word(str: string): void {
// prevent concatenating words and creating // comment out of division and regex
if (
this._endsWithWord ||
(this.endsWith(charCodes.slash) && str.charCodeAt(0) === charCodes.slash)
) {
this._space();
}

this._maybeAddAuxComment();

this._append(str);

this._endsWithWord = true;
}

token(str: string): void {
// space is mandatory to avoid outputting <!--
// http://javascript.spec.whatwg.org/#comment-syntax
const lastChar = this.getLastChar();
const strFirst = str.charCodeAt(0);
if (
(str === "--" && lastChar === charCodes.exclamationMark) ||
// Need spaces for operators of the same kind to avoid: `a+++b`
(strFirst === charCodes.plusSign && lastChar === charCodes.plusSign) ||
(strFirst === charCodes.dash && lastChar === charCodes.dash) ||
// Needs spaces to avoid changing '34' to '34.', which would still be a valid number.
(strFirst === charCodes.dot && this._endsWithInteger)
) {
this._space();
}

this._maybeAddAuxComment();
this._append(str);
}

}

Buffer對象的append方法會去計(jì)算生成代碼的行列信息,并將生成代碼的行列信息和原始代碼的行列信息傳遞給SourceMap對象,SourceMap對象將前后位置信息對應(yīng)起來并進(jìn)行編碼從而生成最終的SourceMap。

//packages/babel-generator/src/buffer.ts
class Buffer {
constructor(map?: SourceMap | null) {
this._map = map;
}

//用于記錄生成代碼的位置
_position = {
line: 1,
column: 0,
};


//第1步:執(zhí)行內(nèi)部_append方法
append(str: string): void {
this._flush();
const { line, column, filename, identifierName } = this._sourcePosition;
this._append(str, line, column, identifierName, filename);
}

//第2步:計(jì)算傳遞進(jìn)來的字符串參數(shù)對應(yīng)的位置
_append(
str: string,
line: number | undefined,
column: number | undefined,
identifierName: string | undefined,
filename: string | undefined,
): void {
this._buf += str;
this._last = str.charCodeAt(str.length - 1);

let i = str.indexOf("\n");//查找換行符位置
let last = 0;

if (i !== 0) {
//排除開頭是換行符的情況,其他情況執(zhí)行標(biāo)記
this._mark(line, column, identifierName, filename);
}

// Now, find each reamining newline char in the string.
while (i !== -1) {
//2-1.當(dāng)存在換行符時(shí),改變行數(shù)
this._position.line++;
this._position.column = 0;
last = i + 1;//換行符后一位

// We mark the start of each line, which happens directly after this newline char
// unless this is the last char.
if (last < str.length) {
this._mark(++line, 0, identifierName, filename);//改變行數(shù),行數(shù)+1
}
i = str.indexOf("\n", last);//尋找下一個(gè)換行符
}
//2-2.改變列數(shù),列數(shù)加上字符的長度
this._position.column += str.length - last;
}

//第3步:調(diào)用sourcemap對象的mark方法
_mark(
line: number | undefined,
column: number | undefined,
identifierName: string | undefined,
filename: string | undefined,
): void {
this._map?.mark(this._position, line, column, identifierName, filename);
}
}

export default class SourceMap {
//第4步:將前后行列信息對應(yīng)起來后對位置信息進(jìn)行編碼
mark(
generated: { line: number; column: number },
line: number,
column: number,
identifierName?: string | null,
filename?: string | null,
) {
this._rawMappings = undefined;

maybeAddMapping(this._map, {
name: identifierName,
generated,
source:
line == null
? undefined
: filename?.replace(/\/g, "/") || this._sourceFileName,
original:
line == null
? undefined
: {
line: line,
column: column,
},
});
}
}

相關(guān)鏈接

Introduction to JavaScript Source Maps

Source Map Revision 3 Proposal

https://github.com/babel/babel

https://github.com/Rich-Harris/vlq

https://github.com/mozilla/source-map

責(zé)任編輯:武曉燕 來源: ELab.zengjiaxin
相關(guān)推薦

2022-08-26 13:24:03

version源碼sources

2021-12-15 09:21:59

Webpack 前端Sourcemap

2023-05-31 08:19:23

Webpack4Webpack 5

2023-05-04 08:54:08

Toolformer語言模型

2024-11-26 07:20:25

2021-06-04 07:27:24

sourcemap前端技術(shù)

2023-09-28 08:41:11

OpenAILLMLangChain

2012-11-30 11:12:03

2010-08-26 22:05:39

DHCP服務(wù)

2022-10-21 13:52:56

JS 報(bào)錯(cuò)調(diào)試本地源碼

2012-12-20 13:02:20

2010-08-09 10:11:08

FlexBuilder

2021-01-22 07:43:35

Filecoin循環(huán)供應(yīng)

2014-03-26 09:54:36

SDN軟件定義網(wǎng)絡(luò)中央控制

2020-06-15 07:00:00

GitOpsKubernetesDevOps

2009-12-15 14:16:13

Ruby Contin

2010-09-27 10:48:44

2010-01-28 13:56:16

Android震動

2017-12-07 16:27:30

Zookeeper架構(gòu)設(shè)計(jì)

2010-05-10 16:20:32

負(fù)載均衡策略
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號