babel源码解析一

我们直接创建一个工程,然后执行:

npm install -D @babel/cli

我们用的是最新版本7.8.0

创建一个test1.js测试:

/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;

创建一个babel配置文件.babelrc(先不写任何配置):

/* .babelrc */
{
 
}

然后我们执行:

npx babel test1.js -o test1.babel.js --config-file .babelrc

最后看一下结果test1.babel.js:

/* test.js */
const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1); //测试插件1

var a = 10;

哦?为啥一点变化都没有呢? 我们带着疑问研究一下源码~

为了更好的研究babel的源码,我们直接去github clone一份:

git clone https://github.com/babel/babel.git

然后当我们执行:

npx babel test1.js -o test1.babel.js --config-file 

的时候,我们直接打开packages/babel-cli/bin/babel.js:

#!/usr/bin/env node

require("../lib/babel");

packages/babel-cli/src/babel/index.js:

#!/usr/bin/env node

import parseArgv from "./options";
import dirCommand from "./dir";
import fileCommand from "./file";

const opts = parseArgv(process.argv);

if (opts) {
  const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
  fn(opts).catch(err => {
    console.error(err);
    process.exitCode = 1;
  });
} else {
  process.exitCode = 2;
}

packages/babel-cli/src/babel/file.js:

export default async function({
  cliOptions,
  babelOptions,
}: CmdOptions): Promise<void> {
     if (cliOptions.filenames.length) {
    await files(cliOptions.filenames);
  } else {
    await stdin();
  }
}

然后执行files方法:

 async function files(filenames: Array<string>): Promise<void> {
    if (!cliOptions.skipInitialBuild) {
      await walk(filenames);
    }

然后执行walk方法:

async function walk(filenames: Array<string>): Promise<void> {
    
        try {
          return await util.compile(
          );
        } catch (err) {
       
    );
  }

可以看到最后执行了util的compile方法(packages/babel-cli/src/babel/util.js):

export function compile(
  filename: string,
  opts: Object | Function,
): Promise<Object> {
  opts = {
    ...opts,
    caller: CALLER,
  };

  return new Promise((resolve, reject) => {
    babel.transformFile(filename, opts, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

可以看到,经过babel-cli后,获取我们传入的参数:

  1. 源文件test1.js
  2. 输入文件test1.babel.js
  3. babel配置文件.babelrc
npx babel test1.js -o test1.babel.js --config-file .babelrc

然后通过babel-core的babel.transformFile方法后获取编译后的代码,最后babel-cli根据传入的-o配置输出最后编译完成的代码。

所以我们重点研究一下babel.transformFile方法

packages/babel-core/src/index.js:

export {
  transformFile
} from "./transform-file";
const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
    //加载配置文件
    const config: ResolvedConfig | null = yield* loadConfig(options);
    if (config === null) return null;
    //加载源文件
    const code = yield* fs.readFile(filename, "utf8");
    //开始编译
    return yield* run(config, code);
  },
);

我们先说一下loadConfig方法,还记得我们传入的.babelrc不,读取这个文件后然后获取里面的presets跟plugins属性,presets是一组preset跟plugin、plugins是一组plugin,我们试着改一下我们的.babelrc配置文件:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs":false
    }],
    ["./plugins/PluginTest1.js"]
  ]
}

然后执行:

npx babel test1.js -o test1.babel.js --config-file .babelrc

结果:

import "core-js/modules/es7.array.includes";
import "core-js/modules/es6.string.includes";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import "core-js/modules/es6.promise";

/* test.js */
var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1); //测试插件1

var aaa = 10;

关于配置文件、prestes跟plugins我们之后慢慢介绍,我们继续看一下babel-core是怎么加载我们的配置文件的。
babel-core/src/config/full.js:

export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
  inputOpts: mixed,
): Handler<ResolvedConfig | null> {
  const result = yield* loadPrivatePartialConfig(inputOpts);
  if (!result) {
    return null;
  }
  const { options, context } = result;

  const optionDefaults = {};
  const passes = [[]];
  try {
    const { plugins, presets } = options;
}

可以看到我们获取了配置文件的plugins跟presets属性然后遍历所有的preset跟plugin执行preset跟plugin提供的方法:

import * as context from "../index";

const loadDescriptor = makeWeakCache(function*(
  { value, options, dirname, alias }: UnloadedDescriptor,
  cache: CacheConfigurator<SimpleContext>,
): Handler<LoadedDescriptor> {

    try {
     const api = {
      ...context,
      ...makeAPI(cache),
    };
      item = value(api, options, dirname);
    } catch (e) {
    
      throw e;
    }
  }

api就是我们传入的babel-core对象、options是我们传入的参数、dirname是我们当前文件夹目录babel-test。

我们先提前写一个插件PluginTest1.js(把变量var a=10变成var aaa=10):

module.exports = function (api, options, dirname) {
    let t = api.types;
    console.log(options)
    console.log(dirname)
    return {
        visitor: {
            VariableDeclarator: {
                enter(path,state) {
                    console.log(path)
                    if(path.node.id.name == 'a'){
                        path.node.id.name="aaa";
                    }
                },
                exit() {
                    console.log("Exited!");
                }
            }
        }
    }
};

插件我们之后再具体解析哈,我们可以看到,我们提供的插件对应的参数也就是api、options、dirname:

module.exports = function (api, options, dirname) {

好啦,说完配置文件我们继续接着之前packages/babel-core/src/transform-file.js往下走:

const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
    //加载配置文件
    const config: ResolvedConfig | null = yield* loadConfig(options);
    if (config === null) return null;
    //加载源文件
    const code = yield* fs.readFile(filename, "utf8");
    //开始编译
    return yield* run(config, code);
  },
);

可以看到,获取完config后直接执行了run方法
packages/babel-core/src/transformation/index.js:

  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

  const opts = file.opts;
  try {
    yield* transformFile(file, config.passes);
  } catch (e) {
    e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
    if (!e.code) {
      e.code = "BABEL_TRANSFORM_ERROR";
    }
    throw e;
  }

  let outputCode, outputMap;
  try {
    if (opts.code !== false) {
      ({ outputCode, outputMap } = generateCode(config.passes, file));
    }
  } catch (e) {
    e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
    if (!e.code) {
      e.code = "BABEL_GENERATE_ERROR";
    }
    throw e;
  }

  return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };
}

代码有点多,不要方!我们一步一步的来

首先我们看到执行了一个normalizeFile方法:

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

packages/babel-core/src/transformation/normalize-file.js:

export default function* normalizeFile(
  pluginPasses: PluginPasses,
  options: Object,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<File> {
  code = `${code || ""}`;

  if (ast) {
    if (ast.type === "Program") {
      ast = t.file(ast, [], []);
    } else if (ast.type !== "File") {
      throw new Error("AST root must be a Program or File node");
    }
    ast = cloneDeep(ast);
  } else {
    ast = yield* parser(pluginPasses, options, code);
  }

可以看到,如果我们传入不是ast的话,就会通过parser方法去获取一个ast(Abstract Syntax Tree)对象。

那么ast是什么呢?

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

好吧,到此babel的一个重量级选手parser登场了
parser是 Babel 的解析器。最初是 从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构

通过babel的parse转换后我们的代码:

/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;

就会被转换成:

{
  "type": "Program",
  "start": 0,
  "end": 120,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 14,
      "end": 33,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 20,
          "end": 33,
          "id": {
            "type": "Identifier",
            "start": 20,
            "end": 22,
            "name": "fn"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 25,
            "end": 33,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 31,
              "end": 33,
              "body": []
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 34,
      "end": 55,
      "expression": {
        "type": "NewExpression",
        "start": 34,
        "end": 55,
        "callee": {
          "type": "Identifier",
          "start": 38,
          "end": 45,
          "name": "Promise"
        },
        "arguments": [
          {
            "type": "ArrowFunctionExpression",
            "start": 46,
            "end": 54,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 52,
              "end": 54,
              "body": []
            }
          }
        ]
      }
    },
    {
      "type": "ClassDeclaration",
      "start": 56,
      "end": 69,
      "id": {
        "type": "Identifier",
        "start": 62,
        "end": 66,
        "name": "Test"
      },
      "superClass": null,
      "body": {
        "type": "ClassBody",
        "start": 67,
        "end": 69,
        "body": []
      }
    },
    {
      "type": "VariableDeclaration",
      "start": 70,
      "end": 101,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 76,
          "end": 101,
          "id": {
            "type": "Identifier",
            "start": 76,
            "end": 77,
            "name": "c"
          },
          "init": {
            "type": "CallExpression",
            "start": 80,
            "end": 101,
            "callee": {
              "type": "MemberExpression",
              "start": 80,
              "end": 98,
              "object": {
                "type": "ArrayExpression",
                "start": 80,
                "end": 89,
                "elements": [
                  {
                    "type": "Literal",
                    "start": 81,
                    "end": 82,
                    "value": 1,
                    "raw": "1"
                  },
                  {
                    "type": "Literal",
                    "start": 84,
                    "end": 85,
                    "value": 2,
                    "raw": "2"
                  },
                  {
                    "type": "Literal",
                    "start": 87,
                    "end": 88,
                    "value": 3,
                    "raw": "3"
                  }
                ]
              },
              "property": {
                "type": "Identifier",
                "start": 90,
                "end": 98,
                "name": "includes"
              },
              "computed": false
            },
            "arguments": [
              {
                "type": "Literal",
                "start": 99,
                "end": 100,
                "value": 1,
                "raw": "1"
              }
            ]
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "VariableDeclaration",
      "start": 110,
      "end": 119,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 114,
          "end": 118,
          "id": {
            "type": "Identifier",
            "start": 114,
            "end": 115,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 116,
            "end": 118,
            "value": 10,
            "raw": "10"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

小伙伴可以直接使用:在线版的ast转换器

在这里插入图片描述

我们先简单的看一下parse方法:

export default function* parser(
  pluginPasses: PluginPasses,
  { parserOpts, highlightCode = true, filename = "unknown" }: Object,
  code: string,
): Handler<ParseResult> {
  try {
    const results = [];
    for (const plugins of pluginPasses) {
      for (const plugin of plugins) {
        const { parserOverride } = plugin;
        if (parserOverride) {
          const ast = parserOverride(code, parserOpts, parse);

          if (ast !== undefined) results.push(ast);
        }
      }
    }

    if (results.length === 0) {
      return parse(code, parserOpts);
    } else if (results.length === 1) {
      yield* []; // If we want to allow async parsers
      if (typeof results[0].then === "function") {
        throw new Error(
          `You appear to be using an async parser plugin, ` +
            `which your current version of Babel does not support. ` +
            `If you're using a published plugin, you may need to upgrade ` +
            `your @babel/core version.`,
        );
      }
      return results[0];
    }
    throw new Error("More than one plugin attempted to override parsing.");
  } catch (err) {
    if (err.code === "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED") {
      err.message +=
        "\nConsider renaming the file to '.mjs', or setting sourceType:module " +
        "or sourceType:unambiguous in your Babel config for this file.";
      // err.code will be changed to BABEL_PARSE_ERROR later.
    }

    const { loc, missingPlugin } = err;
    if (loc) {
      const codeFrame = codeFrameColumns(
        code,
        {
          start: {
            line: loc.line,
            column: loc.column + 1,
          },
        },
        {
          highlightCode,
        },
      );
      if (missingPlugin) {
        err.message =
          `${filename}: ` +
          generateMissingPluginMessage(missingPlugin[0], loc, codeFrame);
      } else {
        err.message = `${filename}: ${err.message}\n\n` + codeFrame;
      }
      err.code = "BABEL_PARSE_ERROR";
    }
    throw err;
  }
}

代码还是很多,我们先提一下一个跟parse的插件有关的部分:

  for (const plugins of pluginPasses) {
      for (const plugin of plugins) {
        const { parserOverride } = plugin;
        if (parserOverride) {
          const ast = parserOverride(code, parserOpts, parse);

          if (ast !== undefined) results.push(ast);
        }
      }

所以当我们插件中有提供parserOverride方法的时候就直接走我们插件的parserOverride去覆盖babel/parser的解析了。
不懂也没关系哈!!parse具体用法我们放到后面解析。

简单看完parse后,我们继续run方法
packages/babel-core/src/transformation/index.js:

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
    //获取ast对象
  const file = yield* normalizeFile();

  const opts = file.opts;
  try {
    //执行转换操作
    yield* transformFile(file, config.passes);
  } catch (e) {
  }

可以看到继续执行了transformFile方法,然后把我们ast对象传给了transformFile方法:

function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
  for (const pluginPairs of pluginPasses) {
    const passPairs = [];
    const passes = [];
    const visitors = [];

    for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.pre;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        if (isThenable(result)) {
          throw new Error(
            `You appear to be using an plugin with an async .pre, ` +
              `which your current version of Babel does not support. ` +
              `If you're using a published plugin, you may need to upgrade ` +
              `your @babel/core version.`,
          );
        }
      }
    }

    // merge all plugin visitors into a single visitor
    const visitor = traverse.visitors.merge(
      visitors,
      passes,
      file.opts.wrapPluginVisitorMethod,
    );
    traverse(file.ast, visitor, file.scope);

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.post;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        if (isThenable(result)) {
          throw new Error(
            `You appear to be using an plugin with an async .post, ` +
              `which your current version of Babel does not support. ` +
              `If you're using a published plugin, you may need to upgrade ` +
              `your @babel/core version.`,
          );
        }
      }
    }
  }
}

还是那句话 “不要方!!我们一步一步来看”,首先我们看到:

//遍历所有的插件,获取插件的visitor属性,然后传给visitors
 for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

那么visitor是什么呢? 可以看到我们之前有提到一个自定义一个插件PluginTest1.js:

module.exports = function (api, options, dirname) {
    let t = api.types;
    console.log(options)
    console.log(dirname)
    return {
        visitor: {
            VariableDeclarator: {
                enter(path,state) {
                    console.log(path)
                    if(path.node.id.name == 'a'){
                        path.node.id.name="aaa";
                    }
                },
                exit() {
                    console.log("Exited!");
                }
            }
        }
    }
};

先提前说一下,visitor其实就是提供给之后的traverse使它能够去访问抽象语法树ast对象

所以接下来就是babel的又一个重量级选手traverse登场了
Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。

再次回到run方法
packages/babel-core/src/transformation/index.js:

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
 //获取ast对象(parser)
  const file = yield* normalizeFile();
  try {
  //(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
    yield* transformFile(file, config.passes);
  } catch (e) {
   
  }
({ outputCode, outputMap } = generateCode(config.passes, file));
 

可以看到执行了generateCode方法,这时babel的最后一个重量级选手babel-generator登场了

Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

最后run方法返回generator生成的代码:

return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };

整个babel-cli到babel-core的过程随着我们的demo跟我们的源码就讲完了。

我们重新整理一下整个过程:

  1. babel-cli开始读取我们的参数(源文件test1.js、输出文件test1.babel.js、配置文件.babelrc)
  2. babel-core根据babel-cli的参数开始编译
  3. Babel Parser 把我们传入的源码解析成ast对象
  4. Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点(也就是结合我们传入的插件把es6转换成es5的一个过程)
  5. Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

好啦,到此我们算是把babel的整个过程简单的跑了一下,为了加深对每个流程的理解,我们不经过babel-core跟babel-cli单独去用一下parser、traverse、generator。

//我们的es6源码
const code = `
    const result=a*b;
    const result1=()=>{};
`;
const {parse}=require("@babel/parser");
const traverse =require("@babel/traverse").default;
const t = require("babel-types");
const generator = require("@babel/generator").default;

//把es6源码通过parser转换成ast对象
const ats=parse(code,{
    sourceType: "module"
});
//把ast对象通过traverse转换成es5代码
traverse(ats,{
    enter(path) {
        if (t.isIdentifier(path.node, { name: "a" })) {
          path.node.name = "aa";
        }
        if (path.isArrowFunctionExpression()){ //es6转换成es5
            path.arrowFunctionToExpression({
                // While other utils may be fine inserting other arrows to make more transforms possible,
                // the arrow transform itself absolutely cannot insert new arrow functions.
                allowInsertArrow: false,
                specCompliant: false,
              });
        }
    }
});
//通过generator转换ast最后输出es5代码
console.log(generator(ats));

我们运行一下代码:

$ node ./babel-test/demo/demo1.js 

结果输出:

{ 
  code: 'const result = aa * b;\n\nconst result1 = function () {};',
  map: null,
  rawMappings: null 
}

可以看到,最终我们实现了把es6的箭头函数转换成es5的过程。

代码中我们可以看到:

//把ast对象通过traverse转换成es5代码
traverse(ats,{
    enter(path) {
        if (t.isIdentifier(path.node, { name: "a" })) {
          path.node.name = "aa";
        }
        if (path.isArrowFunctionExpression()){ //es6转换成es5
            path.arrowFunctionToExpression({
                // While other utils may be fine inserting other arrows to make more transforms possible,
                // the arrow transform itself absolutely cannot insert new arrow functions.
                allowInsertArrow: false,
                specCompliant: false,
              });
        }
    }
});

我们打开一个官方的插件babel-plugin-transform-arrow-functions:

import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";

export default declare((api, options) => {
  api.assertVersion(7);

  const { spec } = options;
  return {
    name: "transform-arrow-functions",

    visitor: {
      ArrowFunctionExpression(
        path: NodePath<BabelNodeArrowFunctionExpression>,
      ) {
        // In some conversion cases, it may have already been converted to a function while this callback
        // was queued up.
        if (!path.isArrowFunctionExpression()) return;

        path.arrowFunctionToExpression({
          // While other utils may be fine inserting other arrows to make more transforms possible,
          // the arrow transform itself absolutely cannot insert new arrow functions.
          allowInsertArrow: false,
          specCompliant: !!spec,
        });
      },
    },
  };
});

哈哈!! 是不是很简单呢? 其实babel也就是把很多的一些插件组合起来最终实现代码的转换,好啦~ 接下来我们就围绕babel的一些常用的插件做解析了

未完待续~~

欢迎志同道合的小伙伴一起学习、一起进步

欢迎入群~~~~~~~

参考:

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-check-if-a-node-is-a-certain-type

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容