国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

SourceMap 與前端異常監(jiān)控

共 31892字,需瀏覽 64分鐘

 ·

2021-08-01 10:38

?? 加個(gè)關(guān)注,后續(xù)上新不錯(cuò)過(guò)~

背景

我們從事 Web 開(kāi)發(fā)工作中,異常監(jiān)控系統(tǒng)已經(jīng)是我們朝夕相處的好助手,但是這些異常處理工具通常都是建立在 Web 生態(tài),或者是假定運(yùn)行在瀏覽器環(huán)境下的,但是當(dāng)我們需要給一套跨端系統(tǒng)搭建一套類似的異常監(jiān)控系統(tǒng),并且期望該系統(tǒng)兼容 Web 生態(tài),現(xiàn)有的工具很可能就不滿足我們的需求了,因此我們需要考慮一套完整的異常監(jiān)控系統(tǒng)整個(gè)鏈路將會(huì)涉及到哪些工具鏈,以及如何修改這些工具鏈來(lái)適配我們的跨端系統(tǒng)。

精彩的一天從查 Bug 開(kāi)始

我們先從和我們程序員最息息相關(guān)的線上查 Bug 開(kāi)始。

下面這種圖,相信大家很多人都很熟悉,當(dāng)我們收到線上 Bug 反饋或者收到報(bào)警電話時(shí),第一時(shí)間基本就是去自己的監(jiān)控平臺(tái)去查看線上日志,大家很可能看到類似下面這張截圖


有經(jīng)驗(yàn)的老司機(jī),立馬就可以定位到自己代碼里哪里出了問(wèn)題,但是有沒(méi)有仔細(xì)思考過(guò)整套監(jiān)控系統(tǒng)是如何打通的呢?或者說(shuō)如果有一天你的監(jiān)控系統(tǒng)出了問(wèn)題,你知道如何追查是哪個(gè)環(huán)節(jié)出了問(wèn)題嗎?

代碼反解

有經(jīng)驗(yàn)的老司機(jī)立馬就反應(yīng)過(guò)來(lái)了,不就是代碼里集成下 Sentry 的 Client,然后每次把 SourceMap 也上傳一份給 Sentry,線上遇到錯(cuò)誤將錯(cuò)誤上傳給 Sentry Server,Sentry Server基于錯(cuò)誤堆棧和 SourceMap 反解出原始的堆棧就可以了。是的,監(jiān)控系統(tǒng)要解決的一個(gè)核心問(wèn)題就是代碼反解。

出于一些性能和安全等的考慮,通常我們發(fā)布到線上的代碼,通常并非原始的代碼,而是經(jīng)過(guò)混淆壓縮后的代碼,即使不經(jīng)過(guò)壓縮,大部分的前端工程都會(huì)經(jīng)過(guò)一個(gè) build 的過(guò)程,這個(gè)過(guò)程里通常會(huì)包括代碼的轉(zhuǎn)換、打包和壓縮等,這使得調(diào)試生成的代碼變得異常困難,因此,我們需要一個(gè)工具幫我們解決這類調(diào)試問(wèn)題。

SourceMap

SourceMap 幾乎完美的解決了代碼反解問(wèn)題,其使用方式十分簡(jiǎn)單,我們?cè)诰幾g的時(shí)候除了生成最終產(chǎn)物 xxx.js 文件外還會(huì)額外生成一個(gè) xxx.js.map 的文件,這個(gè) map 文件里包含了原始代碼及其位置映射信息,這樣我們利用 xxx.js 和 xxx.js.map 就可以將 xxx.js 的代碼及其位置完美的映射會(huì)源代碼以及位置,這樣我們的調(diào)試工具就可以基于這個(gè) map 文件實(shí)現(xiàn)源碼調(diào)試了。其原理雖然很簡(jiǎn)單,但是當(dāng)我們?cè)诠こ讨袑?shí)際應(yīng)用 SourceMap 的時(shí)候,仍然會(huì)碰到這樣或那樣的問(wèn)題。一個(gè)很常見(jiàn)的問(wèn)題就是,為啥用戶上報(bào)的錯(cuò)誤沒(méi)法反解為原始代碼的錯(cuò)誤堆棧了?

SourceMap 支持的全鏈路流程

SourceMap 的使用并非是簡(jiǎn)單的一個(gè)編譯生成即可,其實(shí)際上是需要我們整個(gè)的工作鏈路進(jìn)行配合,才能使得 SourceMap 可以正常工作,因此我們需要先看看我們的整個(gè)工作鏈路上哪些環(huán)節(jié)會(huì)涉及 SourceMap,以及可能會(huì)碰到哪些問(wèn)題。

以一個(gè)業(yè)務(wù)場(chǎng)景為例,我們用 Vue 開(kāi)發(fā)的應(yīng)用部署到線上 -> 發(fā)生了異常 -> 上報(bào)到了 Sentry -> Sentry 幫我們將錯(cuò)誤進(jìn)行反解展示給我們。這個(gè)業(yè)務(wù)場(chǎng)景非常簡(jiǎn)單但是實(shí)際涉及到了很多 SourceMap 的處理。

Transformer

首先我們需要我們的 DSL Transformer 支持 SourceMap

// App.vue
<template>
  <div></div>
</template>
<script lang="ts">
  let x: 10;
export default {}
</script>
<style>
h1 {
  color: red;
}
</style>
<i18n>
{ "greeting": "hello" }
</i18n>

我們使用 @vue/compiler-sfc 將該 Vue 文件編譯為一個(gè) SFCRecord,此時(shí) SFCRecord 里實(shí)際上包含了每個(gè) block 的 SourceMap

const { parse } = require('@vue/compiler-sfc');
const fs = require('fs');
const path = require('path');

async function main(){
  const content = fs.readFileSync(path.join(__dirname, './App.vue'),'utf-8');
  const sfcRecord = parse(content);
  const map = sfcRecord.descriptor['styles'][0].map
  console.log('sfc:',map) // 打印style的SourceMap
}

main();

我們可以進(jìn)一步的根據(jù)每個(gè) block 的 tag 和 lang 來(lái)繼續(xù) transform 每個(gè) block,如使用 Babel | Typescript 處理 Script,PostCSS來(lái)處理 Style,Pug 來(lái)處理 Template,這里每個(gè) Transformer 也都需要處理 SourceMap。

Bundler

處理完 Vue 文件的編譯后,我們希望通過(guò)一個(gè) bundler 來(lái)處理 Vue 模塊的打包,此時(shí)我們可以使用esbuild、rollup、或者 Webpack,我們這里使用 rollup-plugin-vue 來(lái)配合 rollup 給 Vue 應(yīng)用進(jìn)行打包。

async function bundle(){
  const bundle = await rollup.rollup({
    input: [path.join(__dirname, './App.vue')],
    plugins:[rollupPluginVue({needMap:true})],
    external: ['vue'],
    output: {
      sourcemap:'inline'
    }
   
  })
  const result = await bundle.write({
    output: {
      file'bundle.js',
      sourcemap:true
    },
  })
  for(const chunk of result.output){
    console.log('chunk:',chunk.map) // SourceMap
  }
  
}
bundle()

因此這里也需要 bundle 在進(jìn)行 bundler 的過(guò)程中同時(shí)處理生成 sourcemap。

Minifier

但我們 bundler 完代碼后,還需要將代碼進(jìn)行壓縮混淆才能發(fā)布到線上,這時(shí)我們需要使用 minify 工具進(jìn)行混淆壓縮。我們使用 terser 進(jìn)行壓縮。壓縮時(shí)不僅需要處理 minfy 過(guò)程生成的 SourceMap 還需要處理其和原始 bundler 生成的 SourceMap 合并的問(wèn)題,否則 SourceMap 和經(jīng)過(guò)壓縮處理的代碼對(duì)應(yīng)不上了。

  for(const chunk of result.output){
    console.log('chunk:', chunk.map)
    const minifyResult = await require('terser').minify(chunk.code, {
      sourceMap:true,
    })
    console.log('minifyMap:', minifyResult.map)
  }

Runtime

經(jīng)過(guò)一番折騰,我們的編譯流程終于處理完 SourceMap 了,我們開(kāi)發(fā)過(guò)程中突然發(fā)現(xiàn)了代碼出問(wèn)題了,我們希望錯(cuò)誤的堆棧能顯示源碼的位置,另外能支持源碼調(diào)試應(yīng)用,這時(shí)候就需要用的瀏覽器的 SourceMap 支持和 node 的 SourceMap 支持了。

日志收集和上報(bào)

經(jīng)過(guò)一番眼花繚亂的操作,我們的代碼終于和 SourceMap 對(duì)應(yīng)上了,我們平穩(wěn)的將業(yè)務(wù)部署上線,上線前我們需要確保我們的錯(cuò)誤能夠以正確的格式上報(bào)到我們的日志平臺(tái),然而我們線上運(yùn)行的平臺(tái)那么多樣,運(yùn)行的 JS 引擎也是各式各樣,我們需要將用戶的錯(cuò)誤統(tǒng)一成一個(gè)格式上報(bào)給平臺(tái),幸運(yùn)的是 Sentry 的客戶端已經(jīng)幫我們做了這件事情。我們只需要考慮接入 Sentry 的客戶端就行了。因?yàn)槿绻苯訉?SourceMap 一起跟隨 js 代碼下發(fā),這就導(dǎo)致用戶可以直接窺探你的源碼了,類似發(fā)生這樣的事情就很尷尬了https://zhuanlan.zhihu.com/p/26033573,因此我們還需要考慮將 SourceMap 發(fā)布到內(nèi)網(wǎng)而非公網(wǎng)上,這時(shí)就需要處理 SourceMap 關(guān)聯(lián)的問(wèn)題了。

錯(cuò)誤日志反解

一切都妥當(dāng)了,只需要等用戶的錯(cuò)誤上報(bào)上來(lái)(最好永遠(yuǎn)別來(lái)),我就可以在 Sentry 上查看用戶的原始錯(cuò)誤堆棧,幫用戶排查問(wèn)題了,這時(shí)候?qū)嶋H上 Sentry Server 端偷偷幫我們做了根據(jù)用戶的錯(cuò)誤棧和用戶的 SourceMap,幫我們反解錯(cuò)誤棧的事情了。

總結(jié)一下,一個(gè)完整的 SourceMap 流程支持包括了如下這些步驟:

  • transformer: Babel、typescript、emscripten 、 esbuild
  • minifier: esbuild ,terser
  • bundler: esbuild, webpack, rollup
  • runtime: browser & node &  deno
  • 日志上報(bào): sentry client
  • 錯(cuò)誤日志反解: sentry server && node-sourcemap-support

上面這些流程,基本上大多數(shù)工具都幫我們封裝好了,我們只需要安心使用即可,但是當(dāng)某天你需要自己開(kāi)發(fā)一個(gè)自定義的 DSL 的 transformer 通過(guò)自研的 bundler 進(jìn)行編譯打包,運(yùn)行在自研的 JS 引擎上并且使用自研的 monitor client 上報(bào)到自研的 apm 平臺(tái)上,任何環(huán)節(jié)的出錯(cuò)都可能導(dǎo)致你線上的錯(cuò)誤日志反解前功盡棄,你所能做的就是在整個(gè)鏈路上進(jìn)行分析定位。

我們接下來(lái)就看看整個(gè)鏈路上有多少種出錯(cuò)的風(fēng)險(xiǎn)和可能,并且如何定位修復(fù)這些問(wèn)題。

SourceMap 格式

首先我們需要了解下 SourceMap 的基本格式

我們將一個(gè) .ts 文件編譯為 .js 文件,看看其 SourceMap 信息是如何處理映射的。我們項(xiàng)目包含了原始的 ts 文件 add.ts、編譯后的產(chǎn)物文件 add.js 和 SourceMap 文件 add.js.map,其內(nèi)容如下

  • add.ts
const add = (x:number, y:number) => {
  return x + y;
}
  • add.js
var add = function (x, y{
    return x + y;
};
//# sourceMappingURL=module.js.map

SourceMap 的規(guī)范本身十分精簡(jiǎn)和清晰,其本身是一個(gè) JSON 文件,包含如下幾個(gè)核心字段

 {
   version : 3// SourceMap標(biāo)準(zhǔn)版本,最新的為3
   file: "add.js"// 轉(zhuǎn)換后的文件名
   sourceRoot : ""// 轉(zhuǎn)換前的文件所在目錄,如果與轉(zhuǎn)換前的文件在同一目錄,該項(xiàng)為空
   sources: ["add.ts"], // 轉(zhuǎn)換前的文件,該項(xiàng)是一個(gè)數(shù)組,表示可能存在多個(gè)文件合并
   names: [], // 轉(zhuǎn)換前的所有變量名和屬性名,多用于minify的場(chǎng)景
   sourcesContent: [ // 原始文件內(nèi)容
    "const add = (x:number,y:number) => {\n  return x+y;\n}"
  ]
  mappings: "AAAA,IAAM,GAAG,GAAG,UAAC,CAAQ,EAAC,CAAQ;IAC5B,OAAO,CAAC,GAAC,CAAC,CAAC;AACb,CAAC,CAAA",
 }

簡(jiǎn)單介紹下 mapping 的格式,mapping 實(shí)際上是個(gè)三級(jí)結(jié)構(gòu),我們以上述的例子為例

  • line:每個(gè) mapping 包含由 ; 分割的多行內(nèi)容
lines = mappings.split(';')
//  [
  'AAAA,IAAM,GAAG,GAAG,UAAC,CAAQ,EAAC,CAAQ'// var add = function (x, y) {
  'IAC5B,OAAO,CAAC,GAAC,CAAC,CAAC',     // return x + y;
  'AACb,CAAC,CAAA' // };
]

其中每一行都對(duì)應(yīng)生成代碼的每行文件的位置映射信息,這里的三行分別對(duì)應(yīng)了編譯產(chǎn)物的三行信息

  • segment:每一行同包含由 , 分割的多個(gè) segment 信息,其中每個(gè) segment 都對(duì)應(yīng)了產(chǎn)物里每一行里每一個(gè)符合所在的列的信息
const segments = lines.map(x => {
  return x.split(',')
})

console.log('segments:',segments)
//  [
  [
    'AAAA''IAAM',
    'GAAG''GAAG',
    'UAAC''CAAQ',
    'EAAC''CAAQ'
  ], 
  [ 'IAC5B''OAAO''CAAC''GAAC''CAAC''CAAC' ],
  [ 'AACb''CAAC''CAAA' ]
]
  • fields:每個(gè) segment 實(shí)際上又包含了幾個(gè) field,每個(gè) field 都編碼了具體的行列映射信息,依次為
    • 第一位: 轉(zhuǎn)換后代碼所處的列號(hào),如果這是當(dāng)前行的第一個(gè) segment,那么是個(gè)絕對(duì)值,否則是相對(duì)于上一個(gè) segment 的相對(duì)值
    • 第二位:表示這個(gè)位置屬于 sources 屬性中的哪一個(gè)文件,相對(duì)于前一個(gè) segment 的位置(區(qū)別于列號(hào),下一行的第一個(gè) segment 仍然是相對(duì)于上一行的最后一個(gè) segment,并不會(huì) reset)
    • 第三位:表示這個(gè)位置屬于轉(zhuǎn)換前代碼的第幾行,相對(duì)位置,同第二列
    • 第四位:表示這個(gè)位置屬于轉(zhuǎn)換前代碼的第幾列,相對(duì)位置,同第二列
    • 第五位:表示這個(gè)位置屬于 names 屬性中的哪一個(gè)變量,相對(duì)位置,同第二列

這里 field 存儲(chǔ)的值并非是直接的數(shù)字值,而是將數(shù)字使用 vlq 進(jìn)行了編碼,根據(jù)上述這些信息我們實(shí)際上就可以實(shí)現(xiàn) SourceMap 的雙向映射了,即可以根據(jù) SourceMap 和原始代碼的位置信息查找到生成代碼的信息,也可以根據(jù) SourceMap 和生成代碼的位置信息,查找到原始代碼的信息。接下來(lái)我們就實(shí)踐下如何進(jìn)行代碼位置的雙向查找。

雙向查找流程

vlq 解碼

首先第一步我們需要將 vlq 編碼的 SourceMap 反解為原始的數(shù)字偏移信息,我們可以直接使用封裝好的 vlq 庫(kù)完成這一步

function decode({
  const { decode} = require('vlq')
  const mappings = JSON.parse(result.sourceMapText).mappings;
  console.log('mappings:', mappings)
  /**
   * @type {string[]}
   */

  const lines = mappings.split(';');
  const decodeLines = lines.map(line => {
    const segments = line.split(',');
    const decodedSeg = segments.map(x => {
      return decode(x)
    })
    return decodedSeg;
  })
  console.log(decodeLines)
}

此時(shí)我們得到一個(gè)解碼后的位置信息

[
  [
    [ 0000 ],
    [ 4006 ],
    [ 3003 ],
    [ 3003 ],
    [ 10001 ],
    [ 1008 ],
    [ 2001 ],
    [ 1008 ]
  ],
  [
    [ 401-28 ],
    [ 7007 ],
    [ 1001 ],
    [ 3001 ],
    [ 1001 ],
    [ 1001 ]
  ],
  [ [ 001-13 ], [ 1001 ], [ 1000 ] ]
]

還原絕對(duì)位置索引

此時(shí)的這些位置信息都是相對(duì)位置,我們需要將其還原為絕對(duì)位置

  const decoded = decodeLines.map((line) => {
    absSegment[0] = 0// 每行的第一個(gè)segment的位置要重置
    if (line.length == 0) {
      return [];
    }
    const absoluteSegment = line.map((segment) => {
      const result = [];
      for (let i = 0; i < segment.length; i++) {
        absSegment[i] += segment[i];
        result.push(absSegment[i]);
      }
      return result;
    });
    return absoluteSegment;
  });
  console.log('decoded:', decoded)
}

結(jié)果如下,此時(shí)為絕對(duì)位置映射表

 [
  [
    [ 0000 ],
    [ 4006 ],
    [ 7009 ],
    [ 100012 ],
    [ 200013 ],
    [ 210021 ],
    [ 230022 ],
    [ 240030 ]
  ],
  [
    [ 4012 ],
    [ 11019 ],
    [ 120110 ],
    [ 150111 ],
    [ 160112 ],
    [ 170113 ]
  ],
  [ [ 0020 ], [ 1021 ], [ 2021 ] ]
]

雙向映射

有了這個(gè)絕對(duì)位置映射,我們就可以構(gòu)建源碼和產(chǎn)物的雙向映射了。我們可以實(shí)現(xiàn)兩個(gè)核心 API

originalPositionFor 用于根據(jù)產(chǎn)物的行列號(hào),查找對(duì)應(yīng)源碼的信息,而generatedPositionFor 則是根據(jù)源碼的文件名、行列號(hào),查找產(chǎn)物里的位置信息。

class SourceMap {
  constructor(rawMap) {
    this.decode(rawMap);
    this.rawMap = rawMap
  }
 
  /**
   * 
   * @param {number} line 
   * @param {number} column 
   */

  originalPositionFor(line, column){
    const lineInfo = this.decoded[line];
    if(!lineInfo){
      throw new Error(`不存在該行信息:${line}`);
    }
    const columnInfo = lineInfo[column];
    for(const seg of lineInfo){
      // 列號(hào)匹配
      if(seg[0] === column){
        const [column, sourceIdx,origLine, origColumn] = seg;
        const source = this.rawMap.sources[sourceIdx]
        const sourceContent = this.rawMap.sourcesContent[sourceIdx];
        const result = codeFrameColumns(sourceContent, {
         start: {
           line: origLine+1,
           column: origColumn+1
         }
        }, {forceColor:true})
        return {
          source,
          line: origLine,
          column: origColumn,
          frame: result
        }
      }
    }
    throw new Error(`不存在該行列號(hào)信息:${line},${column}`)
  }
  
  decode(rawMap) {
    const {mappings} = rawMap
    const { decode } = require('vlq');
    console.log('mappings:', mappings);
    /**
     * @type {string[]}
     */

    const lines = mappings.split(';');
    const decodeLines = lines.map((line) => {
      const segments = line.split(',');
      const decodedSeg = segments.map((x) => {
        return decode(x);
      });
      return decodedSeg;
    });
    const absSegment = [00000];
    const decoded = decodeLines.map((line) => {
      absSegment[0] = 0// 每行的第一個(gè)segment的位置要重置
      if (line.length == 0) {
        return [];
      }
      const absoluteSegment = line.map((segment) => {
        const result = [];
        for (let i = 0; i < segment.length; i++) {
          absSegment[i] += segment[i];
          result.push(absSegment[i]);
        }
        return result;
      });
      return absoluteSegment;
    });
    this.decoded = decoded;
  }
}

const consumer = new SourceMap(rawMap);

console.log(consumer.originalPositionFor(0,21).frame)

我們還可以使用 codeFrame 直接可視化查找出源碼的上下文信息

generatedPositionFor 的實(shí)現(xiàn)原理類似,不再贅述。

事實(shí)上上面這些反解流程并不需要我們自己去實(shí)現(xiàn),https://github.com/mozilla/source-map 已經(jīng)幫我們提供了很多的編譯方法,包括不限于

  • originalPositionFor:查找源碼位置

  • generatedPositionFor:查找生成代碼位置

  • eachMapping:生成每個(gè) segment 的詳細(xì)映射信息

Mapping {
  generatedLine: 2,
  generatedColumn: 17,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 2,
  originalColumn: 13,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 0,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 0,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 1,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 1,
  name: null
}
Mapping {
  generatedLine: 3,
  generatedColumn: 2,
  lastGeneratedColumn: null,
  source: 'add.ts',
  originalLine: 3,
  originalColumn: 1,
  name: null
}

事實(shí)上 Sentry 的 SourceMap 反解功能也是基于此實(shí)現(xiàn)的。

SourceMap 全鏈路支持

前面我們已經(jīng)介紹的 SourceMap 的基本格式,以及如何基于 SourceMap 的內(nèi)容,來(lái)實(shí)現(xiàn) SourceMap 的雙向查找功能,大部分的 sourcmap 相應(yīng)的工具鏈都是基于此設(shè)計(jì)的,但是在給整個(gè)鏈路做 SourceMap 支持的時(shí)候,但是鏈路的每一步需要解決的問(wèn)題卻各有不同(的坑),我們來(lái)一步步的研(踩)究(坑)吧。

給 transformer 添加 SourceMap 映射

Web 社區(qū)的主流語(yǔ)言的工具鏈都已經(jīng)有了內(nèi)置的 SourceMap 支持了,但是如果你自行設(shè)計(jì)一個(gè) DSL 要怎么給其添加 SourceMap 支持呢?事實(shí)上 SourceMapGenerator 給我們提供了便捷的生成 SourceMap 內(nèi)容的方法,但是當(dāng)我們處理各種字符串變換的時(shí)候,直接使用其 API 仍然較為繁瑣。幸運(yùn)的是很多工具封裝了生成 SourceMap 的操作,提供了較為上層的 api。我們自己實(shí)現(xiàn) transformer 主要分為兩種場(chǎng)景,一種是基于 AST 的變換,另一種則是對(duì)字符串(可能壓根不存在 AST)的增刪改查。

  • AST 變換

大部分的前端 transform 工具,都內(nèi)置幫我們處理好了 SourceMap 的映射,我們只需要關(guān)心如何處理 AST 即可,以 babel 為例,并不需要我們手動(dòng)的進(jìn)行 SourceMap 節(jié)點(diǎn)的操作

import babel from '@babel/core';
import fs from 'fs';
const result = babel.transform('a === b;', {
  sourceMapstrue,
  filename'transform.js',
  plugins: [
    {
      name'my-plugin',
      pre() => {
        console.log('xx');
      },
      visitor: {
        BinaryExpression(path, t) {
          let tmp = path.node.left;
          path.node.left = path.node.right;
          path.node.right = tmp;
        }
      }
    }
  ]
});
console.log(result.code, result.map);
// 結(jié)果
b === a; 
{
  version3,
  sources: [ 'transform.js' ],
  names: [ 'b''a' ],
  mappings'AAAMA,CAAN,KAAAC,CAAC',
  sourcesContent: [ 'a === b;' ]
}
  • 但是 AST 并不能覆蓋所有場(chǎng)景,例如我們?nèi)绻枰獙?c++ 或者 brainfuck 編譯為 js,就很難找到便捷的工具,或者我們只需要替換代碼里的部分內(nèi)容,AST 分析就是大才小用了。此時(shí)我們可以使用 magic-string 來(lái)實(shí)現(xiàn)。
const MagicString = require('magic-string');
const s = new MagicString('problems = 99');

s.overwrite(08'answer');
s.toString(); // 'answer = 99'

s.overwrite(1113'42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source'source.js',
  file'converted.js.map',
  includeContenttrue
}); // generates a v3 SourceMap

console.log('code:', s.toString());
console.log('map:', map);
// 結(jié)果 
code: var answer = 42;
map: SourceMap {
  version3,
  file'converted.js.map',
  sources: [ 'source.js' ],
  sourcesContent: [ 'problems = 99' ],
  names: [],
  mappings'IAAA,MAAQ,GAAG'
}

我們發(fā)現(xiàn)對(duì)于簡(jiǎn)單的字符串處理,magic-string 比使用 AST 的方式要方便和高效很多。

SourceMap 驗(yàn)證

當(dāng)我們給我們的 transformer 加了 SourceMap 支持后,我們?cè)趺打?yàn)證我們的 SourceMap 是正確的呢?你除了可以使用上面提到的 SourceMap 庫(kù)的雙向反解功能進(jìn)行驗(yàn)證外,一個(gè)可視化的驗(yàn)證工具將大大簡(jiǎn)化我們的工作。esbuild 作者就開(kāi)發(fā)了一個(gè) SourceMap 可視化驗(yàn)證的網(wǎng)站 https://evanw.github.io/source-map-visualization/ 來(lái)幫我們簡(jiǎn)化 SourceMap 的驗(yàn)證工作。


SourceMap 合并

當(dāng)我們處理好 transformer 的 SourceMap 生成之后,接下來(lái)就需要將 transformer 接入到 bundler 了,一定意義上 bundler 也可以視為一種 transformer,只是此時(shí)其輸入不再是單個(gè)源文件而是多個(gè)源文件。但這里牽扯到的一個(gè)問(wèn)題是將 A 進(jìn)行編譯生成了 B with SourceMap1  接著又將 B 進(jìn)一步進(jìn)行編譯生成了 C with SourceMap2,那么我們?nèi)绾胃鶕?jù) C 反解到 A 呢?很明顯使用 SourceMap2 只能幫助我們將 C 反解到 B,并不能反解到 A,大部分的反解工具也不支持自動(dòng)級(jí)聯(lián)反解,因此當(dāng)我們將 B 生成 C 的時(shí)候,還需要考慮將 SourceMap1 和 SourceMap2 進(jìn)行合并,不幸的是很多 transformer 并不會(huì)自動(dòng)的處理這種合并,如 TypeScript,但是大部分的 bundler 都是支持自動(dòng)的 SourceMap 合并的。

如在 Rollup 里,你可以在 load 和 transform 里返回 code 的同時(shí),返回 mapping。Rollup 會(huì)自動(dòng)將該 mapping 和 builder 變換的 mapping 進(jìn)行合并,vite 和 esbuild 也支持類似功能。如果我們需要自己處理 SourceMap 合并該如何操作,社區(qū)上已經(jīng)有庫(kù)幫我們處理這個(gè)事情。我們簡(jiǎn)單看下

import ts from 'typescript';
import { minify } from 'terser';
import babel from '@babel/core';

import fs from 'fs';
import remapping from '@ampproject/remapping';
const code = `
const add = (a,b) => {
  return a+b;
}
`
;

const transformed = babel.transformSync(code, {
  filename'origin.js',
  sourceMapstrue,
  plugins: ['@babel/plugin-transform-arrow-functions']
});
console.log('transformed code:', transformed.code);
console.log('transformed map:', transformed.map);

const minified = await minify(
  {
    'transformed.js': transformed.code
  },
  {
    sourceMap: {
      includeSourcestrue
    }
  }
);
console.log('minified code:', minified.code);
console.log('minified map', minified.map);

const mergeMapping = remapping(minified.map, (file) => {
  if (file === 'transformed.js') {
    return transformed.map;
  } else {
    return null;
  }
});

fs.writeFileSync('remapping.js', minified.code);
fs.writeFileSync('remapping.js.map', minified.map);
//fs.writeFileSync('remapping.js.map', JSON.stringify(mergeMapping));

我們來(lái)簡(jiǎn)單驗(yàn)證下效果

  • 使用 mergeMapping 之前

  • 使用 mergeMapping 之后

我們可以看出做了 mergeSourcemap 后可以成功的還原出最初的源碼

性能 matters

我們支持好了上面的 SourceMap 生成和 SourceMap 合并了,迫不及待的在業(yè)務(wù)中加以使用了,但是卻“驚喜”的發(fā)現(xiàn)整個(gè)構(gòu)建流程的速度直線下降,因?yàn)?SourceMap 操作的開(kāi)銷(xiāo)實(shí)際上是非??捎^的,在不需要 SourceMap 的情況下或者在對(duì)性能極其敏感的場(chǎng)景下(服務(wù)端構(gòu)建),實(shí)際是不建議默認(rèn)開(kāi)啟 SourceMap 的,事實(shí)上 SourceMap 對(duì)性能極其敏感,以至于 source-map 庫(kù)的作者們重新用 rust 實(shí)現(xiàn)了 source-map,并將其編譯到了 webassembly。

錯(cuò)誤日志上報(bào)和反解

當(dāng)我們處理好 SourceMap 的生成后,就可以進(jìn)行日志上報(bào)了

Sentry

錯(cuò)誤上報(bào)需要解決的一個(gè)問(wèn)題就是統(tǒng)一上報(bào)格式問(wèn)題,我們生產(chǎn)環(huán)境遇到的錯(cuò)誤并非直接將原始的 Error 信息上報(bào)給服務(wù)端的,而是需要先進(jìn)行格式化處理,如下面這種錯(cuò)誤

function inner({
  myUndefinedFunction();
}
function outer({
  inner();
}
setTimeout(() => {
  outer();
}, 1000);

原始的錯(cuò)誤堆棧如下


Sentry Client 會(huì)將其先進(jìn)行格式化處理,Sentry 發(fā)送給后端的錯(cuò)誤堆棧格式下面這種格式化數(shù)據(jù)


問(wèn)題來(lái)了,為啥 Sentry 要經(jīng)過(guò)這樣一番格式化處理,以及格式化處理中可能會(huì)發(fā)生什么問(wèn)題呢。

V8 StackTrace API

按理來(lái)講 Error 對(duì)象作為標(biāo)準(zhǔn)里規(guī)定的 Ordinary Object ,其在不同的 JS 引擎上表現(xiàn)行為應(yīng)該一致,但是很不幸,標(biāo)準(zhǔn)里雖然規(guī)定了 Error 對(duì)象是個(gè) Ordinary Object,但也只規(guī)定了 name 和 message 兩個(gè)屬性的行為,對(duì)于最廣泛使用的 stack 屬性,并沒(méi)有加以定義,這導(dǎo)致了 JS 引擎在 stack 屬性的表現(xiàn)差別很大(目前已經(jīng)有一個(gè)標(biāo)準(zhǔn)化 stack 的 proposal),甚至有的引擎實(shí)現(xiàn)已經(jīng)突破了標(biāo)準(zhǔn)的限定,使得 Error 表現(xiàn)的更像一個(gè) Exotic Object。我們來(lái)具體看看各引擎對(duì)于 Error 對(duì)象的實(shí)現(xiàn)差異。

V8 支持了 stack 屬性,并且給 stack 屬性提供了豐富的配置,如下是一個(gè)基本的 stack 信息。

function inner({
  myUndefinedFunction();
}
function outer({
  inner();
}
function main({
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

main();

我們可以使用 https://github.com/GoogleChromeLabs/jsvu 來(lái)很方便的測(cè)試不同 JS 引擎的表現(xiàn)差異


V8 的 stacktrace默認(rèn)最多展示 10 個(gè) frame,但是該數(shù)目可以通過(guò) Error.stackLimit 進(jìn)行配置,同時(shí) V8 也支持了 async stacktrace,默認(rèn)也會(huì)展示 async|await 的錯(cuò)誤棧。

stacktrace 的捕獲不僅僅可以在出現(xiàn)異常時(shí)觸發(fā),還可以業(yè)務(wù)主動(dòng)捕獲當(dāng)前的 stacktrace,這樣我們就可以基于此實(shí)現(xiàn)自定義 Error 對(duì)象。

Error.captureStackTrace

V8 提供了 Error.captureStackTrace 支持用戶自定義收集 stackTrace。

這個(gè) API 主要有兩種功能,一個(gè)是給自定義對(duì)象追加 stack 屬性,達(dá)到模擬 Error 的效果

function CustomError(message{
  this.message = message;
  this.name = CustomError.name;
  Error.captureStackTrace(this); // 給對(duì)象追加stack屬性
}

try {
  throw new CustomError('msg');
catch (e) {
  console.error(e.name); // CustomError
  console.error(e.message); //msg
  console.error(e.stack); 
  /*
    CustomError: msg
    at new CustomError (custom_error.js:4:9)
    at custom_error.js:7:9
  */

}

另一個(gè)作用就是可以隱藏部分實(shí)現(xiàn)細(xì)節(jié),這一方面可以避免一些對(duì)用戶無(wú)用的信息泄露給用戶,而對(duì)用戶造成困擾;另一方面可能有一些內(nèi)部信息涉及一些敏感信息,需要防止泄露給用戶。比如一般用戶是不需要關(guān)心 native 的調(diào)用棧,因此就需要將 native 的調(diào)用棧進(jìn)行隱藏。下面的例子就簡(jiǎn)單的演示了如何通過(guò) captureStackTrace 來(lái)隱藏部分調(diào)用棧信息。

function CustomError(message, stripPoint{
  this.message = message;
  this.name = CustomError.name;
  Error.captureStackTrace(this, stripPoint);
}

function leak_secure({
  throw new CustomError('secure泄漏了');
}

function hidden_secure({
  throw new CustomError('secure沒(méi)泄露', outer_api);
}

function outer_api({
  try {
    leak_secure();
  } catch (err) {
    console.error('stk:', err.stack);
  }
  
  try {
    hidden_secure();
  } catch (err) {
    console.error('stk2:', err.stack);
  }
}

outer_api();

Error.prepareStackTrace

另一個(gè)值得注意點(diǎn)的是,雖然 stack 名義上應(yīng)該是一個(gè) frame 的數(shù)組,但是實(shí)際上 stack 卻是個(gè)字符串(歷史包袱,兼容性問(wèn)題吧),因此 V8 同時(shí)提供了一個(gè)結(jié)構(gòu)化的 stack 信息,方便用戶根據(jù)結(jié)構(gòu)化的 stack 信息來(lái)自定義 stack 結(jié)構(gòu)。我們可以通過(guò) Error.prepareStackTrace 來(lái)獲取原始的棧幀結(jié)構(gòu):

Error.prepareStackTrace = (error, structedStackTrace) => {
  for (const frame of structedStackTrace) {
    console.log('frame:', frame.getFunctionName(), frame.getLineNumber(), frame.getColumnNumber());
  }
};

其中的 structedStackTrace 是個(gè)包含了 frame 信息的數(shù)組,其中包含了很多我們感興趣的信息,更詳細(xì)的信息可參考 stack-trace-api。

另外 stack 雖然是個(gè) value property ,但是實(shí)際表現(xiàn)卻是個(gè) getter property,這也是 V8 的實(shí)現(xiàn)違反 EcmaScript 規(guī)范的地方。


這里的 stack 雖然是個(gè) value,但是其表現(xiàn)其實(shí)更像是一個(gè) getter,因?yàn)槠湓L問(wèn) stack 的屬性會(huì)觸發(fā) prepareStackTrace 回調(diào)。這導(dǎo)致 error.stack 實(shí)際上是可能存在副作用的。不僅如此, stack 屬性的計(jì)算也是惰性計(jì)算的,當(dāng) error 觸發(fā)的時(shí)候并不會(huì)進(jìn)行 stack 的計(jì)算,而只有當(dāng)首次訪問(wèn) stack 的時(shí)候才會(huì)觸發(fā)計(jì)算,因?yàn)楸旧?stack 的計(jì)算實(shí)際上是有一定的性能開(kāi)銷(xiāo)的,實(shí)際上 chrome devtools 就因?yàn)?stackstrace 的性能問(wèn)題出過(guò)問(wèn)題(faster-stack-trace),筆者也因?yàn)?stack-trace 的性能問(wèn)題導(dǎo)致嚴(yán)重影響了編譯工具的編譯性能(https://github.com/evanw/esbuild/issues/1039)。

Stack Trace Format

前面已經(jīng)提到 V8 的 stack 是個(gè)字符串,如果需要將一個(gè)字符串解析為一個(gè)結(jié)構(gòu)化數(shù)據(jù),勢(shì)必該字符串需要符合某個(gè)格式規(guī)范,幸運(yùn)的是 V8 的有一套格式規(guī)范,具體格式可見(jiàn) stack-trace-format。雖然 V8 引擎定義了一套格式規(guī)范,不幸的是其他引擎的 stack 格式規(guī)范與 V8 截然不同(不帶重樣的),我們來(lái)看看不同引擎的格式規(guī)范。

V8(Chrome)


SpiderMonkey(Firefox)


JavaScriptCore(Safari)


QuickJS


我們發(fā)現(xiàn)四個(gè) JS 引擎的 stack 格式各不相同,因此需要我們?cè)谏蠄?bào)錯(cuò)誤前需要將這些格式進(jìn)行分別的格式化處理,幸運(yùn)的是 Sentry Client 已經(jīng)幫我們給主流的 JS 引擎做了適配。

sentry-compute-stack-trace


不幸的是,如果你的 JS 引擎是自研的或者 stack 格式和上述的引擎都不一致,你可能需要修改 Sentry 加以支持。

這里我們還發(fā)現(xiàn)了一個(gè)比較嚴(yán)重的問(wèn)題就是 QuickJS 引擎的報(bào)錯(cuò)是沒(méi)有行列號(hào)信息的,這對(duì)于平時(shí)的開(kāi)發(fā)可能足夠了,但是如果你將你的代碼壓縮成一行,那么就會(huì)導(dǎo)致行列號(hào)信息都被丟失了,那么上報(bào)的錯(cuò)誤是根本沒(méi)法進(jìn)行反解的。更加不幸的是 QuickJS 至今仍然不支持列信息,如果你的系統(tǒng)里使用了 QuickJS,可能需要修改 QuickJS 自行添加列號(hào)支持。

eval is eval

如果你的代碼是在 eval 里執(zhí)行,那么將會(huì)碰到另一個(gè)問(wèn)題,有的 JS 引擎針對(duì) eval 的代碼并不會(huì)包含錯(cuò)誤的行列號(hào)信息。

const code = `function inner() {
  myUndefinedFunction();
}

function outer() {
  inner();
}

function main() {
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

function foo() {
  bar();
}

function bar() {
  main();
}

foo();

eval(code);

比如我們看一下不同引擎的結(jié)果

  • V8 雖然包含了行列號(hào)信息,但是堆棧里含有了兩個(gè)行列號(hào)信息,可能導(dǎo)致 sentry-client 識(shí)別出錯(cuò)
  • JavascriptCore 則徹底沒(méi)有行列號(hào)信息

  • SpiderMonkey 雖然能夠顯示,但與非 eval 版本格式完全不同,很難兼容

其實(shí)為了解決 eval 的錯(cuò)誤堆棧的行列號(hào)問(wèn)題,我們可以借助 sourceURL 進(jìn)行還原,我們來(lái)看一看結(jié)果

const code = `function inner() {
  myUndefinedFunction();
}
function outer() {
  inner();
}
function main() {
  try {
    outer();
  } catch (err) {
    console.log(err.stack);
  }
}

function foo() {
  bar();
}
function bar() {
  main();
}

foo()
//# sourceURL=my-foo.js 
`
// 這里通過(guò)sourceURL支持還原

eval(code);

V8:成功還原


JavaScriptCore:不支持


SpiderMonkey:成功還原


anonymous function is bad

解決了 eval 的問(wèn)題后,似乎可以高枕無(wú)憂了,但是實(shí)際開(kāi)發(fā)中仍然碰到了一些匪夷所思的問(wèn)題,線上反解仍然失敗,后來(lái)定位發(fā)現(xiàn) JavaScriptCore 在生成異常的時(shí)候,其行列號(hào)可能計(jì)算錯(cuò)誤,以及給 QuickJS 引擎添加的行列號(hào)功能也存在不少 bug。那么在行列號(hào)不靠譜的情況下如何查問(wèn)題。那就只能借助于 function name 去全局搜索了,不幸的是我們的業(yè)務(wù)中和 SDK 中存在大量的匿名函數(shù),這無(wú)異于雪上加霜。


對(duì) YDKJS 的觀點(diǎn)深感贊同,不幸的是 JavaScript 里將 anonymous function 和 lexical this 兩個(gè) feature 糅合在一起了,你除了通過(guò)變量聲明的方式,沒(méi)有其他更簡(jiǎn)潔的方式來(lái)給一個(gè) arrow function 進(jìn)行命名

const xxx = () => {} // xxx.name is xxx
call('login', () => {
   this.crash()
}) // 如果這里crash了,很不幸調(diào)用棧里拿不到函數(shù)名了

const cb = () => {this.crash()}
call('login', cb) // 太繞了。。。。

call('login'function loginCb(){
   this.crash()
}.bind(this)) // 還是很繞

call('login', loginCb() => { // 這樣就最好了,可惜不支持,we need a proposal 
  this.crash();
})

如果 arrow function 也能便捷的命名就好了。

SourceMap 的局限

代碼反解只是 SourceMap 的一個(gè)功能,其實(shí)還扮演著源碼調(diào)試等功能,但是 SourceMap 在源碼調(diào)試上卻面臨著更大的問(wèn)題和挑戰(zhàn),比如難以應(yīng)對(duì)其他高級(jí)語(yǔ)言的轉(zhuǎn)換問(wèn)題,例如 C++ 編譯到 asm.js 或者 C++編譯為 wasm,如何處理 wasm 或者 asm.js 的源碼調(diào)試和代碼反解,是另一個(gè)比較復(fù)雜的話題了。

參考文獻(xiàn)

  • https://github.com/Rich-Harris/vlq/tree/master/sourcemaps
  • https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
  • https://tc39.es/ecma262/#sec-error-objects
  • getOwnPropertyDescriptor side effects
  • https://bugs.chromium.org/p/v8/issues/detail?id=5834
  • https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly/




瀏覽 76
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 无码操逼| 91久久无码一区人妻A片蜜桃| 免费中文字幕视频| 黄色视频网站免费观看| av日韩无码| 操逼啦| 超碰人人妻| 国产精品女人777777| 在线天堂v| 日本少妇视频| 老女人的逼| 伊人激情五月天| 日韩精品一区二区三区中文在线 | 国产黄色一级片| 欧美日韩一二三区| 中日韩无码| 久久综合伊人777777| 五月婷婷精品| 亚洲成a人| 男人午夜网站| 高清无码网| 人人操人人操人人操人人操人人操 | 亚洲精品国产成人| 精品人妻一区二区三区四区| 丁香六月婷婷久久综合| 免费毛片基地| H片在线免费观看| 久久99久久99精品免视看婷婷 | 91蝌蚪丨人妻丨丝袜| AV网站在线播放| 成人在线不卡| 国产女人18毛片水18精品| 久久中文字幕人妻| 91大神网址| 亚洲无码十八禁| 在线天堂v| 无码在线视频免费观看| 国产精品秘久久久久久网站| 婷婷五月丁香六月| 天天做天天爱天天高潮| 特级特黄AAAAAAAA片| 躁BBB躁BBB添BBBBBB| 农村一级婬片A片AAA毛片古装| 黄色免费观看网站| 亚洲无码人妻视频| av影片在线播放| 四季AV之日韩人妻无码| 亚洲无码在线观看网站| 亚洲国产中文字幕在线播放| av一二三区| 国产精品女人精品久久久天天| 3级片网站| 漂亮人妻吃鸡啪啪哥哥真的好| 午夜无码视频| 大香蕉国产精品| 成人网站在线免费看| 91av成人| 岛国精品在线播放| 少妇嫩搡BBBB搡BBBB| www.91国产| 精品视频免费观看| 美女天堂网| 日本久久综合| 日韩大香蕉在线| 国产精品免费人成人网站酒店| 日本中文字幕不卡| 国产精品午夜福利视频| 老婆被黑人杂交呻吟视频| 亚洲成人AV电影| 狠狠狠狠狠狠狠狠狠| 国产女人高潮毛片| 97国产精品视频人人做人人爱| 美女特黄视频| 黄色大片在线| 91豆花成人社区| 77777精品成人免费A片| 五月丁香六月激情综合| av手机天堂| 丰滿人妻-区二区三区| 色老板网址| 国产酒店自拍| 亚洲成人777| 9l视频自拍九色9l视频成人| 国产愉拍91九色国产愉拍| 日韩无码视屏| 午夜综合在线| 黄色在线免费观看网站| 国产精品一区二区不卡| 日韩欧美视频在线| 18禁网站禁片免费观看| 蜜桃av在线| 亚洲免费黄色电影| 岛国免费av| 精品国产三级| 农村A片婬片AAA毛片| 亚洲综合日韩在线| 日韩无码视频一区| 亚洲天堂中文字幕| 2025精品精品视频| 在线日韩中文字幕| 人妻无码蜜桃视频| 亚洲日韩中文字幕无码| 亚洲图片中文字幕| 日本白嫩的BBw| 91av在线电影| 久久婷综合| 婷婷午夜精品久久久久久性色| 国产99久久久| 久草在线播放| 欧美黄片免费在线观看| 91久久香蕉囯产熟女线看蜜桃| 中文字幕人妻一区| 久久精品小视频| 91久色| 豆花视频在线| 日韩无码人妻久久一区二区三区 | 国产成人精品免高潮在线观看| 欧美在线一级片| 免费无码在线视频| 热久久伊人| 日韩在线播放视频| 成人视频在线观看黄色18| 91人人妻人人澡人人爽| www.黄色视频| 国产精品久久777777| 美日韩视频欧美一区二区视频| 久久久XXX| 99色亚洲| 欧亚一区二区| 淫香淫色天天影视| www.人人摸| 日韩欧美V| 水蜜桃视频免费观看| 亚洲夜夜撸| 97成人视频| 尻屄视频| 亚洲高清视频一区| 99精品国产一区二区| 亚洲成人av在线播放| 日本在线一区二区| 亚洲成人Av| 爆草美女| 国产久久这里只有精品视频| 午夜小电影| 在线观看av中文字幕| 国产精品一级A片| 国产精品女人777777| www.污污污| 日本一区二区三区在线观看网站| 性爱av在线| 亚洲艹逼| 在线高清无码| 日日干天天日| 亚洲日本无码50p| 国产成人精品一区二区三区视频| 99精品色| 日逼www| 黄色福利网站| 欧美色伊人| 亚洲无码高清电影| 一区二区三区水蜜桃| 国产乱码一区二区三区的区别| 999国产精品| 五月激情网站| 久久精品在线| 老司机在线免费视频| 在线观看中文字幕无码| 不卡a12| av天堂中文在线| 91亚洲在线| 一区二区av| 特一级黄色电影| 国产第页| 一区二区三区免费播放| 五月天婷婷av| 天堂精品| 欧美老妇日韩| 亚洲午夜福利视频在线观看| 尤物在线免费视频| 日韩爱爱网| 亚洲一级在线| 色色一级| 成年人免费毛片| 一本一本久久a久久精品牛牛影视| 黄色一级在线观看| 黑人AV在线| 中文字幕免费看高清| 精品秘一区性综合三区| 蜜桃av秘一区二区三区| 欧美综合视频在线观看| 欧美污视频在线观看| 丁香婷婷激情| 又紧又嫩又爽无遮挡免费| 日韩免费在线| 99视频在线免费| 波多野结衣av在线观看| 3344在线观看免费下载视频| 激情一区二区三区| 嫩草视频在线观看免费网站| jizz国产视频| 狠狠插视频| www.色五月| 日欧美美女逼| 91AV在线观看视频| 91精品成人| 五月天av在线| 无码av无码AV| 欧美香蕉在线| 豆花在线视频| 特级西西人体WWWww| 五月天毛片| 蜜桃久久久亚洲| 99精品色| 毛片一级| 久久无码影视| 久久久久久久伊人| 青青自拍视频| 亚洲欧洲在线观看| 国产女人18水真多18精品| 中文字幕无码成人| 国产精品精品| 人人操人人干97| 欧美v在线观看| 一级二级三级无码| 亚洲成人在线免费观看| 久久久婷婷五月亚洲国产精品| av六月天| 色丁香五月婷婷| 91超碰在线观看| 亚洲欧美日韩免费| 婷婷五月开心五月| 在线无码人妻| 操你啦青青草| 三级无码在线观看| 99爱在线观看| 激情一一区二区三区| 成年人免费毛片| 超碰久操| 91狠狠综合久久| 国产无遮挡又黄又爽又| 久久婷婷五月综合伊人| 一区二区三区无码在线观看| 国产欧美激情| 一个人看的www日本高清视频 | www.欧美日韩| 高清免费在线中文Av| 国产AV中文| 俺来了俺去了| 国产精品黑人ThePorn| 五月丁香激情婷婷| 成人欧美在线| 欧美成人乱码一区二区三区| 丁香六月综合激情| h网站在线观看| 欧美在线日韩在线| 操比视频在线观看| 中文字幕有码视频| 亚洲无码免费在线视频| 成人A片免费视频| 日本人妻中文字幕| 亚洲va欧美va| 国产V视频| 看毛片网站| 2017天天干天天射| 成人免看一级a一片A片| 欧美三级在线| 天堂在线中文网| 成人无码视频在线观看| 国产无码在线影院| 国产香蕉91| 安徽妇搡BBB搡BBBB户外老太太 | 日韩一区二区三区免费视频 | 日本欧美黄色| 91麻豆大奶巨乳一区白虎| 男人的天堂久久| 欧洲成人免费视频| 蜜桃av秘无码一区二区三| 日韩精品成人av| 免费看一级片| 97夜色| 毛片操逼视频| 亚洲天堂视频在线观看| 99精品视频免费在线观看| 国产色视频在线| 久久99精品国产麻豆婷婷洗澡| 久久久久久久久久国产精品| 国产传媒自拍| 亚洲免费观看高清完整| 蜜桃av一区二区三区| 伊人激情| 亚洲无码视频在线免费观看 | 国产非洲欧美在线| 夜夜撸日日| 日日夜夜精品视频| 国产又粗又大又爽| 久久久成人免费电影| 日本一区二区三| 亚洲超碰在线观看| 免费观看无码| 人人操人人人| 97人人草| AV资源网站在线| 亚洲欧美日韩色图| 2025最新国产精品每日更新| 91九色TS另类国产人妖| 亚洲vs无码秘蜜桃少妇| 一道本无码视频| 激情av| 免费一级欧美片在线观看| 黄色无遮挡| 三级网站在线| 日韩在线三级片| 日韩欧美在线视频观看| 人妻丰满熟妇av无码| 性爱无码网站| 国产精品麻豆视频| www.婷婷六月天| 国产麻豆AⅤMDMD0071| 国产精品V亚洲精品V日韩精品| 蜜桃视频一区| 91妻人人澡人人爽人人精品| 精品成人电影| 梁祝艳谭A级毛片| 欧美性网站| 亚洲日韩一区二区三区四区| 婷婷丁香五月社区亚洲| 久久久久久三级电影| 免费无码又爽又黄又刺激网站| 国产AV影院| 亚洲免费看黄| 第一页在线| 69国产成人综合久久精品欧美 | 国产成人精品亚洲男人的天堂| 亚州天堂| 日韩一区二区三免费高清在线观看 | 大香蕉综合网站| 日韩高清无码一区二区| 天天草天天草| 天堂在线免费视频| 少妇久久久久久久久久| 五月天性爱| 五月天无码免费视频| 日韩无码AV电影| 国产美女被爽到高潮免费A片软件| 成人激情五月天| 黄页网站视频| 久草视频福利| 99色天堂| 国产精品久久视频| 美女91小视频| 欧美成人性爱网| 91人妻中文字幕| av无码高清| 中文无码高清视频| 欧美视频免费| 另类老妇奶性BBWBBwBBw| 91免费在线| 成人91视频| 国产精品96久久久久久| 午夜天堂精品久久久久9| 影音先锋成人| 91精品在线观看视频| 按摩忍不住BD中文字幕| 91人妻综合| 性爱一级视频| 亚欧成人| 最新av网| 亚洲精品视频在线播放| 免费观看A级毛片| 水果派解说在线观看| 国产精品被狂躁到高潮| 91精品久久久久| 国产精品毛片视频| 北条麻妃无码视频| 亚洲成人内射| 久久久久久麻豆| 精品资源成人| 亚洲一区无码在线观看| 良妇露脸15P| 一区二区三区福利| 中文字幕在线免费视频| 国产精品精品精品| 日韩精品欧美一区二区三区| 日本91| 日韩一级无码毛片| 日韩无码免费播放| 五月丁香婷婷在线观看| 亚洲图片欧美色图| 日本久久综合网| 最新午夜综合福利视频| 北条麻妃人妻中文字幕91影视 | 国产精品91视频| 9色在线| 久久久999精品日韩一区二区 | 亚洲69v久久久无码精品| 亚洲天堂影院| 久久久久无码国产精品不卡| 国产A级黄色片| 91视频内射| 黄色视频在线观看| 国精产品一区二区三区| 五月婷婷色色色| 北条麻妃一区二区三区在线| 18毛片| 最新中文| 免费看操逼| 日韩中文字幕无码中字字幕| 一级a片在线免费观看| 日韩一级电影在线观看| 水蜜桃一区二区三区| 亚洲第一伊人| 七六十路の高齢熟妇无码| 黄色A一级| 操屄影院| 夜夜嗨AV| 香蕉漫画在线观看18| 久久久久久久91| 中文字幕东京热| 91精品无码| www.俺去| 久久色在线视频| 日本爱爱网站| 黄色不卡| 一本久道综合| 欧美性爱免费网站| 中文字幕AV一区| 親子亂子倫XXXX| 欧美a在线观看| 日本人妻在线观看| 欧美国产日韩综合在线观看170 | 国产中文字幕片| 伊人视频在线观看| 操逼123首页| 久久久久久久| 国产黄片免费在线观看| 可以看的黄色视频| 福利一区二区| 手机成人在线视频| 一级黄片免费视频| 高清无码视频在线观看| 无码AV中文字幕| 国产a片免费观看| 国内夫妻【20p】| 尤物视频在线| 人人天天久久| 精品人妻一区二区三区四区不卡在 | 翔田千里一区二区三区精品播放 | 国产三级片自拍| 亚洲免费观看在线观看| 大雞巴疯狂浓精合集| 久久99久久99久久99| 国产思思99re99在线观看| 中文字幕亚洲视频| 中文字幕四区| 天天操超碰| 亚洲欧美大香蕉视频网| JiZZjiZZ亚洲成熟熟妇| 亚洲天堂成人在线| 国产熟女一区二区三区五月婷| 蜜桃av色偷偷av老熟女| 亚洲视频精品| 最新中文字幕在线播放| 人人摸人人艹| 91黄色视频网站| 簧片网站免费| 日本黄色免费网站| 91在线| 天天干天天操天天爽| 久久精品水多多www| 久久久久久成人电影| 亚欧洲精品在线视频| 中文字幕高清视频| 免费无码成人片在线观看在线| 亚洲日韩免费视频| 无码另类| 色94色.欧美.setu| 日日爱爱| 围内精品久久久久久久久久‘变脸 | 亚洲黄色av网站| 国产无码免费视频| 亚洲视频成人| 亚洲毛片网站| 鸡巴在线观看| 天天干天天操天天| 四季AV一区二区夜夜嗨| av牛牛| 久久久国产一区二区三区| 一区二区无码在线| av色色| 国产精品人妻无码久久久郑州天气网| 91少妇精品| 日韩精品人妻中文字幕| 超碰福利在线| 无码一页| 久久无码影视| 怡春院免费视频| 毛片一级片| 成人丁香五月| 国产青草视频在线观看| 操老女人视频| 成人做爰黄AAA片免费直播岛国 | 亚洲精品自拍视频| 色眯眯久久爱| 四川少扫搡BBw搡BBBB| XXXXⅩHD亚洲人HD| 日韩无码1| 大鸡巴影院| 蜜桃精品视频| 亚洲第五页| 国产成人无码精品| 91精品久久久久久综合五月天| 狼友视频在在观看| 免费看黃色AAAAAA片| 欧美成人精品欧美一级| 国产av小电影| 天天搞天天色| 99视频免费在线观看| 欧洲成人免费视频| 日本色电影在线观看| 免费看污网站| 精品无码久久久久久久久app | 淫色综合| 91探花足浴店按摩店| 欧美一级片在线| 97亚洲精品| 黄片高清无码在线观看| 伊人在线综合| 无码迷穴| 高清国产AV| 日韩在线精品视频| 99久热在线精品视频| 欧美老妇另类BBwBBw| 免费在线性爱视频| 伊人乱伦| 岛国精品在线播放| 亚洲免费观看高清完整版在线 | 国产主播在线播放| 成人无码免费视频| 国产欧美日韩综合在线视频| 性欧美V| 成人一级片| 激情网五月天| 国产美女av| 在线免费观看网站| 欧美精品黄| 欧美大鸡吧视频| 日韩欧美一区二区三区不卡| 黄片高清| 亚洲中文字幕视频在线观看| 看肏屄视频| 美女久久久久| 欧美一级A片免费看视频小说| 99视频色| 99这里只有精品| 成人网站在线观看视频| 色99视频| 国产高潮白浆喷| 久干妞| www.射| 五月天亚洲激情| 亚洲午夜福利| 日韩成人无码AV| 免费的毛片| 日韩av中文字幕在线| 五月天三级片| 中文亚洲视频| 免费高清无码在线| 好吊AV| 中文一区二区| 欧美日韩精品一区二区三区钱| a视频免费在线观看| 国产精品免费人成人网站酒店| 国产乱伦毛片| 成年人黄色在线观看| 国产黄色视频在线播放| 激情av在线观看| 91色色| 国产精品视频免费| 岛国精品在线播放| 国产操操操| 亚洲人妻无码一区| 老熟妇搡BBBB搡BBBB| 亚洲激情自拍| 在线看v片| 干B网| 97精品人妻麻豆一区二区| 黃色A片一級二級三級免費久久久| 伊人毛片| caobi视频| 一级婬片A片AAAA毛片A级| 亚洲电影av| 久久99久久99久久99| 中日韩特黄A片免费视频| 91丨九色丨熟女泻火| www.色日本| 欧美日韩a片| 操逼操逼操逼操逼操逼操逼| 国产一级二级三级视频| 欧美一级黄色性爱视频| 三级网站在线| 国产免费无码一区二区| 麻豆av在线| 日本黄色一级视频| 中文字幕人妻一区| 亚洲成人AV在线观看| 五月丁香亭亭| 超碰福利在线| 99热这里只有精| 欧美婬乱片A片AAA毛片地址| 2025中文字幕在线| 欧美日黄| 91人人妻人人澡人人爽人人精品| 97爱视频| 婷婷五月天免费视频| 久射精品| 18+免费网站| 日韩一区二区三免费高清在线观看 | 影音先锋成人在线| 欧美激情五月| 日韩欧美高清视频| 免费一二区| 色色视频网站| 国产理论视频| 91日逼视频| 国产精彩视频| 97午夜| 精品一区二区ww| aaa无码| 激情综合婷婷久久| 五月婷婷六月丁香综合| 亚洲欧美成人网站| 国产成人免费在线视频| 日韩第1页| 欧美在线中文| 波多野结衣一二三区| 伊人久久香蕉网| 国产成人自拍偷拍视频| 91精品视频网| 无码六区| 特级西西人体444www高清| 精品人妻一区二区免费蜜桃| 九九九九综合| 俺也来最新色视频| 天天搞天天干| 91人妻人人爽人人澡| 操逼毛片视频| 日韩精品一区在线| 欧美熟妇BBB搡BBB| 成人图片小说| 国产精品99视频| 亚洲AV综合网| 五月天色色网站| 人人色人人看| 日皮视频在线| 欧美操人| 97精品国产97久久久久久免费| 欧美日韩东京热| 在线播放一区| 天堂在线中文字幕| www.一区二区三区| 少妇嫩搡BBBB搡BBBB| 中文字幕在线亚洲| 久久亚洲成人| 日韩国产av| 毛片二区| 毛片网| 国产精品电影大全| 天天澡天天爽日日AV| 夫妻成人免费看片一区二区| 一道本一区二区三区免费视频| 蜜桃av秘一区二区三区| 俺来俺去www色婷婷| 亚洲情在线| 激情青青草| 日韩欧美国产一区二区| 成人黃色A片免费看| 久久青娱乐| 成人精品一区二区三区无码视频| 操操操无码| 黄色永久网站| 高清无码三级片在线观看| 色情片在线观看| 在线免费黄| 久草热在线| 99er热精品视频| 午夜社区| 国产精品视频免费观看| 无码字幕| 人妻少妇被猛烈进入中文字幕 | 伊人影院在线观看| 国产日韩欧美一区| 亚洲老鸭窝| 特黄aaaaaaaa真人毛片| 亚洲色综合| 成人午夜精品| 欧美高清无码| 久久视频一二| 久久午夜无码鲁丝午夜精品| 91精品国产综合久久久蜜臀粉嫩| 18禁网站在线播放| 九九九视频在线观看| 国产无遮挡又黄又爽又色视频| 国产精品揄拍100视频| 欧美性性生交XXXXX无码| 2019天天干| 欧美日韩成人在线视频| 苍井空无码| 色男人天堂| 欧美不卡在线视频| 很很撸在线视频| 先锋影音资源一区| 中文字幕免费观看视频| 亚洲精品女人久久久| 亚洲爱爱网| 抽插网| 亚洲第二页| 中文字幕成人| 蜜桃久久久久久久| 天天草B| 97干在线| yjizz视频| 超碰人人91| 人人操碰| 国产激情视频在线观看| 91熟女丰满原味| av天天日| 特级西西444www大胆高清图片 | 国产福利视频导航| 蜜臀91| 亚洲激情视频| 欧美在线观看视频一区| 国产成人久久777777| 久久久久久久久久成人| 色婷婷婷| 人人操人人干人人爽| 国产电影一区二区三区| 亚洲小电影| 免费观看高清无码视频| 黄色小说在线播放| 大伊香蕉久久| 欧美一级精品| 国产—a毛—a毛A免费| 成人在线网| 日韩性爱一区二区| 韩国无码成人电影啊荒| 男女av免费观看| 91丝袜一区在线观看| 日韩a电影| 久久精品6| 久久中文字幕电影| h在线网站| 一级片黄片| 在线黄色AV| 久久久福利视频| 97一区二区| 日本高清视频网站| 六十路老熟女码视频| 国产欧美日韩在线视频| 污视频在线看| 无码中文av| 无码人妻日韩精品一区二区三 | 天天摸天天摸| 波多野结衣与黑人| 免费在线观看无码视频| 亚洲成人黄色网| jizzjizz国产| 亚洲国产精品成人va在线观看| 天天射中文| 日比视频网站| 国产成人视频免费观看| 成人午夜免费视频| 99成人乱码一区二区三区在线 | 天堂av中文字幕| 天天爆操| 大香蕉国产精品视频| 色噜噜av| 亚洲人内射片又| 高潮91PORN蝌蚪九色| 日本在线一区二区| 狠狠干2021| 久久不卡| 一本久久A精品一合区久久久| 成人大香蕉视频| 色婷婷五月天在线观看| V天堂在线| 日韩性爱网站| 一级A片久久久免费直播间| a片在线观看视频| 久久999| 国产亚洲久一区二区^_^| 好吊视频一区二区三区红桃视频you| 2021av| 国产TS变态重口人妖| 成人电影一区| 免费射精一二三区| 成人黄色在线看| 国产l精品久久久久久久久久| 一本一道AV| 日韩无码免费| 日本AⅤ中文字幕| 人人干国产| 国产精品无码白浆高潮| 高清无码在线免费观看| 国产精品一级A片| 黄片高清视频| 水蜜桃视频免费观看| 青春草在线视频| 在线视频三区| 囯产一级a一级a免费视频| 欧美淫乱视频| 91精品无码| 精品成人无码一区二区三区| 色婷婷久久综合| 亚洲色图一区二区三区| 国产777| 人人操天天干| 色综合天天操| 日韩在线不卡视频| 麻豆蜜桃wwww精品无码| 国产一级二级三级久久久| 美腿丝袜中文字幕精品| 亚韩无码| 国产精品爽爽久久久| 免费国产成人看片在线| 91精品国产三级| 成人免费观看的毛视频| 日韩av在线看| 欧美亚洲综合在线| 操逼无码视频| 黄色一级aa片| 天干天干天夜夜| 久久国产精品久久| 伊人久久狼人| 免费av观看| 青青青草视频在线观看| 亚洲成人黄色| 三级操逼| 26uuu亚洲| 中国操逼网| 中文字幕无码精品三级在线欧美| 51AV在线| 美女做爱视频| 国产精品欧美综合亚洲| 国产视频999| 91在线无码精品秘| 天堂资源地址在线| 久热思思| 亚洲无码成人在线| 日本三级片视频不卡| 丁香六月色| 少妇bbw搡bbbb搡bbbb | 中文√在线天堂8| 强伦轩人妻一区二区三区四区| 亚洲第一福利视频| 91香蕉视频在线看| 亚州天堂网| 超碰人妻在线| 亚洲最新在线观看| 日本无码在线视频| 中文字幕在线视频观看| 亚洲国产高清国产精品| 国产精品免费人成人网站酒店| 人人色人人摸| 18禁无码网站| 啊啊啊啊啊网站| 亚洲天堂视频在线观看| 无码人妻AⅤ一区二区三区| 婷婷五月天小说| 国产成人一区二区三区A片免费| 亚洲天堂AV在线观看| 麻豆二区| 超碰老熟女| 国内不卡一卡二视频| 国产美女自拍视频| 亚洲中文字幕日韩在线| 亚洲精品ww| 亚洲秘无码一区二区三区av| 射射AV| 九九在线观看视频| 亚洲色图偷拍| 欧美午夜影院| 蜜芽成人在线| 狠狠色AV| 中文字幕第11页| 国产成人99久久亚洲综合精品| 福利视频导航自拍| 色色婷婷五月| 无码少妇| 91av一区二区三区| 操b视频在线观看| 日韩免费在线观看一区入口| 婷婷五月色综合| 日本色色视频| 亚洲人人爱| 手机在线观看av| 午夜艹| 黄色A片免费观看| 日韩AV毛片| 特黄特色一级特黄大片| AV色色|