Monaco Editor 實現(xiàn)一個日志查看器

前言

在 Web IDE 中慕匠,控制臺中展示日志是至關(guān)重要的功能譬挚。Monaco Editor 作為一個強大的代碼編輯器狠角,提供了豐富的功能和靈活的 API ,支持為內(nèi)容進行“裝飾”立帖,非常適合用來構(gòu)建日志展示器。如下圖:


file

除了實時日志外,還有一些需要查看歷史日志的場景描融。如下圖:


file

Monarch

Monarch 是 Monaco Editor 自帶的一個語法高亮庫年叮,通過它只损,我們可以用類似 Json 的語法來實現(xiàn)自定義語言的語法高亮功能跃惫。這里不做過多的介紹,只介紹在本文中使用到的那部分內(nèi)容.

一個語言定義基本上就是描述語言的各種屬性的JSON值,部分通用屬性如下:

  • tokenizer
    (必填項,帶狀態(tài)的對象)這個定義了tokenization的規(guī)則遥诉。 Monaco Editor 中用于定義語言語法高亮和解析的一個核心組件。它的主要功能是將輸入的代碼文本分解成一個個的 token债朵,以便于編輯器能夠根據(jù)這些 token 進行語法高亮、錯誤檢查和其他編輯功能谚中。
  • ignoreCase
    (可選項囊拜,默認(rèn)值:false)語言是否大小寫不敏感冠跷?tokenizer(分詞器)中的正則表達式使用這個屬性去進行大小寫(不)敏感匹配弟疆,以及case場景中的測試。
  • brackets
    (可選項柑司,括號定義的數(shù)組)tokenizer使用這個來輕松的定義大括號匹配蟆湖,更多信息詳見 @bracketsbracket 部分劲室。每個方括號定義都是一個由3個元素或?qū)ο蠼M成的數(shù)組充蓝,描述了open左大括號谓苟、close右大括號token令牌類斤讥。默認(rèn)定義如下:
[ ['{','}','delimiter.curly'],
['[',']','delimiter.square'],
['(',')','delimiter.parenthesis'],
['<','>','delimiter.angle'] ]

tokenizer

tokenizer 屬性描述了如何進行詞法分析派草,以及如何將輸入轉(zhuǎn)換成 token ,每個 token 都會被賦予一個 css 類名鉴竭,用于在編輯器中渲染矢洲,內(nèi)置的 css token 包括:

identifier         entity           constructor
operators          tag              namespace
keyword            info-token       type
string             warn-token       predefined
string.escape      error-token      invalid
comment            debug-token
comment.doc        regexp
constant           attribute

delimiter .[curly,square,parenthesis,angle,array,bracket]
number    .[hex,octal,binary,float]
variable  .[name,value]
meta      .[content]

當(dāng)然也可以自定義 css token袁滥,通過以下方式將自定義的 css token 注入题翻。

editor.defineTheme("vs", {
  base: "vs",
  inherit: true,
  rules: [
    {
      token: "token-name",
      foreground: "#117700",
    }
  ],
  colors: {},
});

一個 tokenizer 由一個描述狀態(tài)的對象組成垃喊。tokenizer 的初始狀態(tài)由 tokenizer 定義的第一個狀態(tài)決定。這句話什么意思呢?查看下方例子,root就是 tokenizer 定義的第一個狀態(tài)赏参,就是初始狀態(tài)。同理韧掩,如果把 afterIfroot 兩個狀態(tài)調(diào)換位置,那么 afterIf 就是初始狀態(tài)滑臊。

monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            // 初始狀態(tài)的規(guī)則
            [/\d+/, 'number'], // 識別數(shù)字
            [/\w+/, 'keyword'], // 識別關(guān)鍵字
            // 轉(zhuǎn)移到下一個狀態(tài)
            [/^if$/, { token: 'keyword', next: 'afterIf' }],
        ],
        afterIf: [
            // 處理 if 語句后的內(nèi)容
            [/\s+/, ''], // 忽略空白
            [/[\w]+/, 'identifier'], // 識別標(biāo)識符
            // 返回初始狀態(tài)
            [/;$/, { token: '', next: 'root' }],
        ]
    }
});

如何獲取 tokenizer 定義的第一個狀態(tài)呢聋庵?

class MonarchTokenizer {
  ...
  public getInitialState(): languages.IState {
    const rootState = MonarchStackElementFactory.create(null, this._lexer.start!);
    return MonarchLineStateFactory.create(rootState, null);
  }
  ...
}

通過 getInitialState 獲取初始的一個狀態(tài)氧映,通過代碼可以看到 確認(rèn)哪個是初始狀態(tài)是通過 this._lexer.start 這個屬性振峻。這個屬性又是怎么被賦值的呢烫堤?

function compile() {
  ...
  for (const key in json.tokenizer) {
    if (json.tokenizer.hasOwnProperty(key)) {
      if (!lexer.start) {
        lexer.start = key;
      }
  
      const rules = json.tokenizer[key];
      lexer.tokenizer[key] = new Array();
      addRules('tokenizer.' + key, lexer.tokenizer[key], rules);
    }
    }
  ...
}

在 compile 解析 setMonarchTokensProvider 傳入的語言定義對象時利诺,會將讀取出來的第一個 key 作為初始狀態(tài)慢逾≌饰可能會有疑問疆导,就一定能保證在定義對象時败富,寫入的第一個屬性兽叮,在讀取時一定第一個被讀出嗎账阻?

在 JavaScript 中,對象屬性的順序有一些特定的規(guī)則:

  1. 整數(shù)鍵:如果屬性名是一個整數(shù)(如 "1"蒲牧、"2" 等),這些屬性會按照數(shù)值的升序排列挎扰。
  2. 字符串鍵:對于非整數(shù)的字符串鍵港谊,屬性的順序是按照它們被添加到對象中的順序。
  3. Symbol 鍵:如果屬性的鍵是 Symbol 類型斜筐,這些屬性會按照它們被添加到對象中的順序。

因此,當(dāng)使用 for...in 循環(huán)遍歷對象的屬性時,屬性的順序如下:

  • 首先是所有整數(shù)鍵构哺,按升序排列。
  • 然后是所有字符串鍵蹦骑,按添加順序排列。
  • 最后是所有 Symbol 鍵笑窜,按添加順序排列。

看個例子:

file

上述例子可以看出智政,“1”垦垂、“2”雖然被寫在了后面劫拗,但仍然會被排序優(yōu)先輸出胁附,其后才是字符串鍵根據(jù)添加順序輸出汉嗽。所以稳析,盡可能不要使用整數(shù)鍵去定義狀態(tài)名。

當(dāng) tokenizer 處于某種狀態(tài)時畦徘,只有那個狀態(tài)的規(guī)則才能匹配溶握。所有規(guī)則是按順序進行匹配的睡榆,當(dāng)匹配到第一個規(guī)則時塘揣,它的 action 將被用來確定 token 的類型劳曹。不會再使用后面的規(guī)則進行嘗試,因此蜕劝,以一種最有效的方式排列規(guī)則是很重要的岖沛。比如空格和標(biāo)識符優(yōu)先。

如何定義一個狀態(tài)嗤朴?

每個狀態(tài)定義為一個用于匹配輸入的規(guī)則數(shù)組衡楞,規(guī)則可以有如下形式:

  • [regex, action]
    {regex: regex, action: action}形式的簡寫瘾境。
  • [regex, action, next]
    { regex: regex, action: action{ next: next} }形式的簡寫迷守。
monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            // [regex, action]
            [/\d+/, 'number'], 
            /** 
             * [regex, action, next]
             * [/\w+/, { token: 'keyword', next: '@pop' }] 的簡寫
             */
            [/\w+/, 'keyword', '@pop'], 
        ]
    }
});

regex 是正則表達式急膀,action 分為以下幾種:

  • string
    { token: string } 的簡寫
  • [action, ..., actionN]
    多個 action 組成的數(shù)組慷暂。這僅在正則表達式恰好由 N 個組(即括號部分)組成時才允許餐禁。舉個例子:
[/(\d)(\d)(\d)/, ['string', 'string', 'string']
  • { token: tokenClass }
    這個 tokenClass 可以是內(nèi)置的 css token帮非,也可以是自定義的 token筑舅。同時游盲,還規(guī)定了一些特殊的 token 類:
    • "@rematch"
      備份輸入并重新調(diào)用 tokenizer 优烧。這只在狀態(tài)發(fā)生變化時才有效(或者我們進入了無限的遞歸),所以這個通常和 next 屬性一起使用杖刷。例如颓鲜,當(dāng)你處于特定的 tokenizer 狀態(tài)乐严,并想要在看到某些結(jié)束標(biāo)記時退出艾扮,但是不想在處于該狀態(tài)時使用它們,就可以使用這個填物。例如:
monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
        root: [
            [/\d+/, 'number', 'word'],
        ],
        word: [
            [/\d/, '@rematch', '@pop'],
            [/[^\d]+/, 'string']
        ]
    }
});

這個 language 的狀態(tài)流轉(zhuǎn)圖是怎么樣的呢影涉?

file

可以看出涎劈,在定義一個狀態(tài)時,應(yīng)保證狀態(tài)存在出口即沒有定義轉(zhuǎn)移到其他狀態(tài)的規(guī)則),否則可能會導(dǎo)致死循環(huán)撞蜂,不斷的使用狀態(tài)內(nèi)的規(guī)則去匹配。

  • "@pop"
    彈出 tokenizer 棧以返回到之前的狀態(tài)浦旱。
  • "@push"
    推入當(dāng)前狀態(tài)例隆,并在當(dāng)前狀態(tài)中繼續(xù)涎永。
monaco.languages.setMonarchTokensProvider('myLanguage', {
    tokenizer: {
    root: [
      // 當(dāng)匹配到開始標(biāo)記時惶我,推送新的狀態(tài)
      [/^\s*function\b/, { token: 'keyword', next: '@function' }],
    ],
    function: [
      // 在 function 狀態(tài)下的匹配規(guī)則
      [/^\s*{/, { token: 'delimiter.bracket', next: '@push' }],
      [/[^}]+/, 'statement'],
      [/^\s*}/, { token: 'delimiter.bracket', next: '@pop' }],
    ],
  }
});
- $n  

匹配輸入的第n組,或者是0代表這個匹配的輸入。 -Sn
狀態(tài)的第 n 個部分,比如黑竞,狀態(tài) @tag.foo,用 S0 代表整個狀態(tài)名(即 tag.foo ),S1 返回 tag坷剧,$S2 返回 foo 狞尔。

實時日志

在本篇文章中偏序,Monaco Editor 的使用就不再提及研儒,不是本文的重點独令。利用 Monaco Editor 實現(xiàn)日志查看器主要是為了讓不同的類型的日志有不同的高亮主題。

實時日志中冲呢,存在不同的日志類型招狸,如:info裙戏、error、warning 等翰意。

/**
 * 日志構(gòu)造器
 * @param {string} log 日志內(nèi)容
 * @param {string} type 日志類型
 */
export function createLog(log: string, type = '') {
    let now = moment().format('HH:mm:ss');
    if (process.env.NODE_ENV == 'test') {
        now = 'test';
    }
    return `[${now}] <${type}> ${log}`;
}

根據(jù)日志可以看出信柿,每條日志都是[xx:xx:xx]開頭客年,緊跟著 <日志類型>绍傲,后面的是日志內(nèi)容。(日志類型:info 、error唐瀑、warning毅弧。)

注冊一個自定義語言 realTimeLog 作為實時日志的一個 language

這里規(guī)則也很簡單,在 root 中設(shè)置了兩條解析規(guī)則奢赂,分別是匹配日志日期和日志類型司致。在匹配到對應(yīng)的日志類型后庭再,給匹配到的內(nèi)容打上 token ,然后通過 next 攜帶匹配的引用標(biāo)識( $1 表示正則分組中的第1組)進入下一個狀態(tài) consoleLog,在狀態(tài)consoleLog 中茎辐,匹配日志內(nèi)容丐黄,并打上 token ,直到遇見終止條件(日志日期)缀棍。

import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { LanguageIdEnum } from "./constants";

languages.register({ id: LanguageIdEnum.REALTIMELOG });

languages.setMonarchTokensProvider(LanguageIdEnum.REALTIMELOG, {
  keywords: ["error", "warning", "info", "success"],
  date: /\[[0-9]{2}:[0-9]{2}:[0-9]{2}\]/,
  tokenizer: {
    root: [
      [/@date/, "date-token"],
      [
        /<(\w+)>/,
        {
          cases: {
            "$1@keywords": { token: "$1-token", next: "@log.$1" },
            "@default": "string",
          },
        },
      ],
    ],
    log: [
      [/@date/, { token: "@rematch", next: "@pop" }],
      [/.*/, { token: "$S2-token" }],
    ],
  },
});

// ===== 日志樣式 =====
export const realTimeLogTokenThemeRules = [
  {
    token: "date-token",
    foreground: "#117700",
  },
  {
    token: "error-token",
    foreground: "#ff0000",
    fontStyle: "bold",
  },
  {
    token: "info-token",
    foreground: "#999977",
  },
  {
    token: "warning-token",
    foreground: "#aa5500",
  },
  {
    token: "success-token",
    foreground: "#669600",
  },
];

狀態(tài)流轉(zhuǎn)圖:

file

普通日志

普通日志與實時日志有些許不同哑诊,他是的日志類型是不展示出來的,沒有一個起始/結(jié)束標(biāo)識符供Monarch高亮規(guī)則匹配。所以需要一個在文本中不展示稍浆,又能作為起始/結(jié)束的標(biāo)識符弦撩。

也確實存在這么一個東西,不占寬度,又能被匹配——“零寬字符”。

零寬字符(Zero Width Characters)是指在文本中占用零寬度的字符敦锌,通常用于特定的文本處理或編碼目的马胧。它們在視覺上不可見崔列,但在程序處理中可能會產(chǎn)生影響。

利用零寬字符創(chuàng)建不同日志類型的標(biāo)識江滨。

// 使用零寬字符作為不同類型的日志標(biāo)識
// U+200B
const ZeroWidthSpace = '';
// U+200C
const ZeroWidthNonJoiner = '';
// U+200D
const ZeroWidthJoiner = '';
// 不同類型日志的起始 / 結(jié)束標(biāo)識,用于 Monarch 語法文件的解析
const jobTag = {
    info: `${ZeroWidthSpace}${ZeroWidthNonJoiner}${ZeroWidthSpace}`,
    warning: `${ZeroWidthNonJoiner}${ZeroWidthSpace}${ZeroWidthNonJoiner}`,
    error: `${ZeroWidthJoiner}${ZeroWidthNonJoiner}${ZeroWidthJoiner}`,
    success: `${ZeroWidthSpace}${ZeroWidthNonJoiner}${ZeroWidthJoiner}`,
};

之后的編寫語法高亮規(guī)則尊勿,與實時日志相同盯孙。

import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { LanguageIdEnum } from "./constants";

languages.register({ id: LanguageIdEnum.NORMALLOG });

languages.setMonarchTokensProvider(LanguageIdEnum.NORMALLOG, {
  info: /\u200b\u200c\u200b/,
  warning: /\u200c\u200b\u200c/,
  error: /\u200d\u200c\u200d/,
  success: /\u200b\u200c\u200d/,
  tokenizer: {
    root: [
      [/@success/, { token: "success-token", next: "@log.success" }],
      [/@error/, { token: "error-token", next: "@log.error" }],
      [/@warning/, { token: "warning-token", next: "@log.warning" }],
      [/@info/, { token: "info-token", next: "@log.info" }],
    ],
    log: [
      [
        /@info|@warning|@error|@success/,
        { token: "$S2-token", next: "@pop" },
      ],
      [/.*/, { token: "$S2-token" }],
    ],
  },
});

// ===== 日志樣式 =====
export const normalLogTokenThemeRules = [
  {
    token: "error-token",
    foreground: "#BB0606",
    fontStyle: "bold",
  },
  {
    token: "info-token",
    foreground: "#333333",
    fontStyle: "bold",
  },
  {
    token: "warning-token",
    foreground: "#EE9900",
  },
  {
    token: "success-token",
    foreground: "#669600",
  },
];

狀態(tài)流轉(zhuǎn)圖:

file

其他

在 Monaco Editor 中支持a元素

Monaco Editor 本身是不支持在內(nèi)容中插入 HTML 元素的碟婆,原生只支持對鏈接進行高亮宋梧,并且支持cmd + 點擊打開鏈接。但仍可能會存在需要實現(xiàn)類似a元素的效果扑浸。

另辟蹊徑檬贰,查找 Monaco Editor 的 API 后,linkProvider 也許可以大致滿足咏闪,但仍有不足癣籽。

以下是介紹:

在 Monaco Editor 中棘劣,linkProvider 是一個用于提供鏈接功能的接口糙俗。它允許開發(fā)者為編輯器中的特定文本或代碼片段提供鏈接沧奴,當(dāng)用戶懸停或點擊這些鏈接時属愤,可以執(zhí)行特定的操作,比如打開文檔饵隙、跳轉(zhuǎn)到定義等沮脖。

具體用法:

const linkProvider = {
    provideLinks: function(model, position) {
        // 返回鏈接數(shù)組
        return [
            {
                range: new monaco.Range(1, 1, 1, 5), // 鏈接的范圍
                url: 'https://example.com', // 鏈接的 URL
                tooltip: '點擊訪問示例' // 懸停提示
            }
        ];
    }
};
monaco.languages.registerLinkProvider('javascript', linkProvider);

它是針對已注冊的語言進行注冊倘潜,不會影響到其他語言涮因。在文本內(nèi)容發(fā)生變化時就會觸發(fā) provideLinks 伺绽。

根據(jù)這個 API 想到一個思路:

  1. 在生成文本時奈应,在需要展示為 a 元素的地方使用 #link#${JSON.stringify(attrs)}#link# 包裹,attrs 是一個對象肩榕,其中包含了 a 元素的attribute惩妇。
  2. 在文本內(nèi)容傳遞給 Monaco Editor 之前筐乳,解析文本的內(nèi)容蝙云,利用正則將a 元素標(biāo)記匹配出來路召,使用 attrs鏈接文本替換標(biāo)記文本股淡,并記錄替換后鏈接文本在文本內(nèi)容中的索引位置。利用 Monaco Editor 的 getPositionAt 獲取鏈接文本在編輯器中的位置(起始/結(jié)束行列信息)贾铝,生成 Range 早敬。
  3. 使用一個容器收集對應(yīng)的日志中的 Link 信息搞监。在通過 linkProvider 將編輯器中對應(yīng)的鏈接文本識別為鏈接高亮。
  4. 給 editor 實例綁定點擊事件 onMouseDown 俘种,如果點擊的內(nèi)容位置在收集的 Link 中時绝淡,觸發(fā)對外提供的自定義鏈接點擊事件宙刘。

根據(jù)這一思路進行實現(xiàn):

  1. 生成 a 元素標(biāo)記。
interface IAttrs {
    attrs: Record<string, string>;
    props: {
        innerHTML: string;
    };
}
/**
 *
 * @param attrs
 * @returns
 */
export function createLinkMark(attrs: IAttrs) {
    return `#link#${JSON.stringify(attrs)}#link#`;
}
  1. 解析文本內(nèi)容
getLinkMark(value: string, key?: string) {
    if (!value) return value;
    const links: ILink[] = [];

    const logRegexp = /#link#/g;
    const splitPoints: any[] = [];
    let indexObj = logRegexp.exec(value);
    /**
     * 1. 正則匹配相應(yīng)的起始 / 結(jié)束標(biāo)簽 #link# , 兩兩為一組
     */
    while (indexObj) {
        splitPoints.push({
            index: indexObj.index,
            0: indexObj[0],
            1: indexObj[1],
        });
        indexObj = logRegexp.exec(value);
    }

    /**
     * 2. 根據(jù)步驟 1 獲取的 link 標(biāo)記范圍牢酵,處理日志內(nèi)容悬包,并收集 link 信息
     */
      /** l為起始標(biāo)簽,r為結(jié)束標(biāo)簽 */
    let l = splitPoints.shift();
    let r = splitPoints.shift();
    /** 字符串替換中移除字符個數(shù) */
    let cutLength = 0;
    let processedString = value;
    /** link 信息集合 */
    const collections:[number, number, string, ILink['attrs']][] = [];
    while (l && r) {
      const infoStr = value.slice(l.index + r[0].length, r.index);
      const info = JSON.parse(infoStr);
      /**
       * 手動補一個空格是由于后面沒有內(nèi)容馍乙,導(dǎo)致點擊鏈接后面的空白處,光標(biāo)也是在鏈接上的丝格,
       * 導(dǎo)致當(dāng)前的range也在link的range中撑瞧,觸發(fā)自定義點擊事件
       */
      const splitStr = info.props.innerHTML + ' ';

      /** 將 '#link#{"attrs":{"href":"xxx"},"props":{"innerHTML":"logDownload"}}#link#' 替換為 innerHTML 中的文本 */
      processedString =
          processedString.slice(0, l.index - cutLength) +
          splitStr +
          processedString.slice(r.index + r[0].length - cutLength);
      collections.push([
          /** 鏈接的開始位置 */
          l.index - cutLength,
          /** 鏈接的結(jié)束位置 */
          l.index + splitStr.length - cutLength - 1,
          /** 鏈接地址 */
          info.attrs.href,
          /** 工作流中應(yīng)用,點擊打開子任務(wù)tab */
          info.attrs,
      ]);

      /** 記錄文本替換過程中显蝌,替換文本和原文本的差值 */
      cutLength += infoStr.length - splitStr.length + r[0].length * 2;
      l = splitPoints.shift();
      r = splitPoints.shift();
    }
  
    /**
     * 3. 處理收集的 link 信息
     */
    const model = editor.createModel(processedString, 'xxx');
    for (const [start, end, url, attrs] of collections) {
        const startPosition = model.getPositionAt(start);
        const endPosition = model.getPositionAt(end);

        links.push({
            range: new Range(
                startPosition.lineNumber,
                startPosition.column,
                endPosition.lineNumber,
                endPosition.column
            ),
            url,
            attrs,
        });
    }
    model.dispose();

    return processedString;
}
  1. 使用一個容器存儲解析出來的 link
const value = `這是一串帶鏈接的文本:${createLinkMark({
  props: {
    innerHTML: '鏈接a'
  },
  attrs: {
    href: 'http://www.abc.com'
  }
})}`
const links = getLinkMark(value)
  1. 利用存儲的 links 注冊 LinkProvider
languages.registerLinkProvider('taskLog', {
    provideLinks() {
        return { links: links || [] };
    },
});
  1. 綁定自定義事件
    在點擊 editor 中的內(nèi)容時都會觸發(fā)onMouseDown预伺,在其中可以獲取當(dāng)前點擊位置的Range信息,循環(huán)遍歷收集的所有 Link,判斷當(dāng)前點擊位置的Range是否在其中酬诀。containsRange方法可以判斷一個Range是否在另一個Range中脏嚷。
useEffect(() => {
  const disposable = logEditorInstance.current?.onMouseDown((e) => {
      const curRange = e.target.range;
      if (curRange) {
        const link = links.find((e) => {
          return (e.range as Range)?.containsRange(curRange);
        });
        if (link) {
          onLinkClick?.(link);
        }
      }
    });

  return () => {
    disposable?.dispose();
  };
}, [logEditorInstance.current]);

缺點:在日志實時打印時,出現(xiàn)鏈接不會立馬高亮料滥,需要等一會才會變成鏈接高亮然眼。

file
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葵腹,隨后出現(xiàn)的幾起案子高每,更是在濱河造成了極大的恐慌,老刑警劉巖践宴,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鲸匿,死亡現(xiàn)場離奇詭異,居然都是意外死亡阻肩,警方通過查閱死者的電腦和手機带欢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烤惊,“玉大人乔煞,你說我怎么就攤上這事∑馐遥” “怎么了渡贾?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長雄右。 經(jīng)常有香客問我空骚,道長,這世上最難降的妖魔是什么擂仍? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任囤屹,我火速辦了婚禮,結(jié)果婚禮上逢渔,老公的妹妹穿的比我還像新娘肋坚。我一直安慰自己,他們只是感情好肃廓,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布冲簿。 她就那樣靜靜地躺著,像睡著了一般亿昏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上档礁,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天角钩,我揣著相機與錄音,去河邊找鬼。 笑死递礼,一個胖子當(dāng)著我的面吹牛惨险,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脊髓,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼辫愉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了将硝?” 一聲冷哼從身側(cè)響起恭朗,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎依疼,沒想到半個月后痰腮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡律罢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年膀值,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误辑。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沧踏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出巾钉,到底是詐尸還是另有隱情翘狱,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布睛琳,位于F島的核電站盒蟆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏师骗。R本人自食惡果不足惜历等,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辟癌。 院中可真熱鬧寒屯,春花似錦、人聲如沸黍少。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厂置。三九已至菩掏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昵济,已是汗流浹背智绸。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工野揪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞧栗。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓斯稳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親迹恐。 傳聞我的和親對象是個殘疾皇子挣惰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容