细读 JS | JavaScript 模块化之路

配图源自 Freepik

学习不能停,都给我卷起来...

一、前世今生

在 ES6 之前,JavaScript 一直没有官方的模块(Module)体系,对于开发大型、复杂的项目形成了巨大的障碍。幸好社区上有一些模块加载方案,最主要的有 CommonJS(CommonJS Modules)和 AMD(Asynchronous Module Definition)两种模块规范,前者用于服务器,后者用于浏览器。

随着 ES6 的正式发布,全新的模块将逐步取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想尽量的静态化,是的编译时就能确定模块的依赖关系,以及输入和输出的变量。

rollupwebpack 等构建工具中常见的 Tree Shaking 能力,就是依赖于 ES6 模块的静态特性实现的。

而 CommonJS 和 AMD 模块都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS 模块
const { stat, exists, readFile } = require('fs')

// 相当于
const _fs = require('fs')
const stat = _fs.stat
const exists = _fs.exists
const readFile = _fs.readFile

以上示例,实际上是整体加载了 fs 模块(即加载 fs 的所有方法),生成了一个对象 _fs,然后再从这个对象上读取了 3 个方法。这种方式称为“运行时加载”,原因是只有运行时才能得到这个对象,导致完全没有办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

import { stat, exists, readFile } from 'fs'

以上示例,实际上是从 fs 模块中加载了 3 个方法,其他方法不加载。这种方式称为“编译时加载”或“静态加载”,即 ES6 模块可以在编译时就完成模块加载,效率要高于 CommonJS 模块的加载方式。这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

除了静态加载带来的各种好处,ES6 模块还有以下好处:

  • 不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性。
  • 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。

二、为什么需要模块化?

举个 🌰

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
  </body>
</html>
// module-a.js
var person = { name: 'Frankie', age: 20 }
// module-b.js
console.log(person.name) // 将会打印什么呢?

我们可以轻而易举就知道 module-b.js 里将会打印出 Frankie,原因很简单,它们都是处于全局作用域下,因此 module-b.js 中的 person.name 就能读取到在 module-a.js 中定义的 person 变量。

如果将 module-a.jsmodule-b.js 在 HTML 中的顺序换过来,就会抛出错误。原因是<script> 是按块加载的,包括下载、(预)编译和执行。唯有当前块执行完毕,或者抛出错误,才会接着加载下一个 <script>

注意,这里提到的按顺序加载,是指没有 deferasync 属性的哈。它俩对外部脚本的加载方式是有影响的。但非本文话题,因此不展开讲述。

那问题就来了,这很容易造成全局污染,对于大型、复杂的项目来说会非常棘手。

假设没有诸如 CommonJS 等模块化解决方案可用,要怎样解决这种问题呢?

1. 对象字面量(Object Literal)

// 声明
var namespace = {
  prop: 123,
  method: function () {},
  // ...
}

// 调用
namespace.prop
namespace.method()

缺点:

作为一个单一的、有时很长的句法结构,它对其内容施加了限制。内容必须在 {} 之间,并且属性或方法之间必须添加逗号。当模块内容复杂起来之后,维护成本高,移动内容变得更加困难。

在多个文件中使用相同的命名空间:可以将模块定义分散到多个文件中,并按如下方式创建命名空间变量,则可忽视加载文件的顺序。

var namespace = namespace || {}

使用多个模块,可以通过创建单个全局命名空间并向其添加子模块来避免全局名称的扩散。不建议进一步嵌套,如果名称冲突是一个问题,您可以使用更长的名称。这种方式称为:嵌套命名空间。

// 全局命名空间
var globalns = globalns || {}

// 添加 A 子模块
globalns.moduleA = {
  // module content
}

// 添加 B 子模块
globalns.moduleB = {
  // module content
}

尽管使用命名空间可以在一定程度上解决了命名冲突的问题,但是存在一个问题:在 moduleB 中可以修改 moduleA 的内容,而且 moduleA 可能还蒙在鼓里,不知情。

以上命名空间内的所有成员和方法,无论是否私有,对外都是可访问的。这是一个明显的缺点,模块化不应该如此设计。

Yahoo 公司的 YUI 2 就是采用了这种方案。

2. 立即执行函数表达式(Immediately-Invoked Function Expression,简称 IIFE)

在模块模式中,我们使用 IIFE 将环境附加到模块数据。可以从模块访问该环境内的绑定,但不能从外部访问。另一个优点是 IIFE 为我们提供了执行初始化的地方。

var namespace = (function () {
  // private data
  var _prop = 123
  var _method = function () {}

  return {
    // read-only
    get prop() {
      return _prop
    },
    get method() {
      return _method
    }
  }
})()

这样的话,我们就不用担心,在外部直接修改 namespace 内部的成员或者方法了。

// 读取
namespace.prop // 123
namespace.method() 

// 写入
namespace.prop = 456 // 无效
namespace.method = function foo() {} // 无效

因此,结合前面的内容,就可以这样去处理:

// 全局命名空间
var globalns = globalns || {}

// 添加 A 子模块
globalns.moduleA = (function () {
  // ...

  return {
    // ...
  }
})()

// 添加 B 子模块
globalns.moduleB = (function () {
  // ...

  return {
    // ...
  }
})()

到现在,有了命名空间解决了命名冲突问题,同时使用 IIFE 来维护各模块的私有成员和方法,导出对外的开放接口即可。这似乎有了模块化该有的样子。

但是,还有一个问题。前面提到过 <script> 是按书写顺序加载的(即使下载顺序可能并行的),主要包括:

  • 脚本下载
  • 脚本解析(编译和执行)

假设我们的脚本如下:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
  </body>
</html>

那么我们的 modueA 在(首次)解析的时候,就没办法调用 moduleB 的内容,因为它压根还没解析执行。一旦项目复杂度、模块数量上来之后,模块之间的依赖关系就很难维护了。

三、社区模块化方案

在 ES2015 之前,社区上已经有了很多模块化方案,流行的主要有以下几个::

  • CommonJS
  • AMD(Asynchronous Module Definition)
  • CMD(Common Module Definition)
  • UMD(Universal Module Definition)

其中 CommonJS 规范在 Node.js 环境下取得了很不错的实践,它只能应用于服务器端,而不支持浏览器环境。CommonJS 规范的模块是同步加载的,由于服务器的模块文件存在于本地硬盘,只有磁盘 I/O 的,因此同步加载机制没什么问题。

但在浏览器环境,一是会产生开销更大的网络 I/O,二是天然异步,就会产生时序上的错误。后来社区上推出了异步加载、可在浏览器环境运行的 RequireJS 模块加载器,不久之后,起草并发布了 AMD 模块化标准规范。

由于 AMD 会提前加载,很多开发者担心有性能问题。假设一个模块依赖了另外 5 个模块,不管这些模块是否马上被用到,都会执行一遍,这些性能消耗是不容忽视的。为了避免这个问题,有部分人试图保留 CommonJS 书写方式和延迟加载、就近声明(就近依赖)等特性,并引入异步加载机制,以适配浏览器特性。比如,已经凉凉的 BravoJS、FlyScript 等方案。

在 2011 年,国内的前端大佬玉伯提出了 SeaJS,它借鉴了 CommonJS、AMD,并提出了 CMD 模块化标准规范。但并没有大范围的推广和使用。

在 2014 年,美籍华裔 Homa Wong 提出了 UMD 方案:将 CommonJS 和 AMD 相结合。本质上这不算是一种模块化方案。

到了 2015 年 6 月,随着 ECMAScript 2015 的正式发布,JavaScript 终于原生支持模块化,被称为 ES Module。同时支持服务器端和浏览器端。

尽管到了 2022 年,现状仍然是多种模块化方案共存,但未来肯定是 ES Module 一统江湖...

关于 JavaScript 模块化历史线,可以看下这篇文章

四、CommonJS

Node.js 的模块系统是基于 CommonJS 规范的实现的。除此之外,像 CouchDB 等也是 CommonJS 的一种实现。而且它们有一些是没有完全按照 CommonJS 规范去实现的,甚至额外添加了特有的功能。

由于我们接触到的 CommonJS 通常指 Node.js 中的模块化解决方法,因此,接下来提到的 CommonJS 均指 Node.js 的模块系统。

先瞅一下,一个 CommonJS 模块里面都包括一些什么信息:

如果有一些看不懂或不了解其用处的,先不急,下面娓娓道来。

CommonJS 的模块特点:

  • 每一个 JavaScript 文件就是一个独立模块,其作用域仅在模块内,不会污染全局作用域。
  • 一个模块包括 requiremoduleexports 三个核心变量。
  • 其中 module.exportsexports 负责模块的内容导出。后者只是前者的“别名”,若使用不当,还可能会导致无法导出预期内容。其中 require 负责其他模块内容的导入,而且其导入的是其他模块的 module.exports 对象。
  • 模块可以加载多次,但只会在第一次加载时运行一次,然后运行结果就会被缓存起来。下次再加载是直接读取缓存结果。模块缓存是可以被清除的。
  • 模块的加载是同步的,而且是按编写顺序进行加载。

4.1 Module 对象

前面打印的 module 就是 Module 的实例对象。每个模块内部,都有一个 module 对象,表示当前模块。它有以下属性:

// Module 构造函数
function Module(id = '', parent) {
  this.id = id
  this.path = path.dirname(id)
  this.exports = {}
  moduleParentCache.set(this, parent)
  updateChildren(parent, this, false)
  this.filename = null
  this.loaded = false
  this.children = []
}

源码 👉 node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

module.id:返回字符串,表示模块的标识符,通常这是完全解析的文件名。
module.path:返回字符串,表示模块的目录名称,通常与 module.idpath.dirname() 相同。
module.exports:模块对外输出的接口,默认值为 {}。默认情况下,module.exportsexports 是相等的。
module.filename:返回字符串,表示模块的完全解析文件名(含绝对路径)。
module.loaded:返回布尔值,表示模块是否已完成加载或正在加载。
module.children:返回数组,表示当前模块引用的其他模块的实例对象。
module.parent:返回 null 或数组。若返回值为数组时,表示当前模块被其他模块引用了,而且每个数组元素表示被引用模块对应的实例对象。
module.paths:返回数组,表示模块的搜索路径(含绝对路径)。
module.isPreloading:返回布尔值,如果模块在 Node.js 预加载阶段运行,则为 true。

注意点

  • 赋值给 module.exports 必须立即完成,不能在任何回调中完成(应在同步任务中完成)。
    比如,在 setTimeout 回调中对 module.exports 进行赋值是“不起作用”的,原因是 CommonJS 模块化是同步加载的。

请看示例:

// module-a.js
setTimeout(() => {
  module.exports = { welcome: 'Hello World' }
}, 0)

// module-b.js
const a = require('./a')
console.log(a.welcome) // undefined

// ❌ 错误示例

再看个示例:

// module-a.js
const EventEmitter = require('events')
module.exports = new EventEmitter() // 同步任务中完成对 module.exports 的赋值

setTimeout(() => {
  module.exports.emit('ready') // ❓ 这个会生效吗?
}, 1000)

// module-b.js
const a = require('./module-a')
a.on('ready', () => {
  console.log('module a is ready')
})

// ⚠️ 执行 `node module-b.js` 命令运行脚本,以上 ready 事件可以正常响应,
// 原因 require() 会对模块输出值进行“浅拷贝”,因此 module-a.js 中的 setTimeout 是可以更新 EventEmitter 实例对象的。
  • module.exports 属性被新对象完全替换时,通常也会“自动”重新分配 exports(自动是指不显式分配新对象给 exports 变量的前提下)。但是,如果使用 exports 变量导出新对象,则必须“手动”关联 module.exprotsexports,否则无法按预期输出模块值。

请看示例:

// 1️⃣ 以这种方式进行模块的输出,module.exports 与 exports 会自动分配,即 module.exports === exports
module.exports = {
  // ...
}

// 2️⃣ 以这种方式导出的值,将会是空对象 {},而不是 { sayHi: <Function> }
// 此时 module.exports !== exports
exports = { sayHi: function () {} } // ❌

// 3️⃣ 解决以上问题,需要手动关联 module.exprots 和 exports,使得二者相等
module.exports = exports = { sayHi: function () {} } // ✅
  • 由以上示例也可以看出,require() 方法引用的是 module.exports 对象,而不是 exports 变量。
  • 利用 module.parent 返回 null 或数值的特性,可以判断当前模块是否为入口脚本。另外,也可以通过 require.main 来获取入口脚本的实例对象。

module.exports 与 exports 的注意点

此前已写过一篇文章去介绍它俩的区别了。

一句话总结:exports 变量只是 module.exports 属性的一个别名,仅此而已。

我们可以这样对模块进行输出:

module.exports = {
  name: 'Frankie',
  age: 20,
  sayHi: () => console.log('Hi~')
}

// 相当于
exports.name = 'Frankie'
exports.age = 20
exports.sayHi = () => console.log('Hi~')

但请注意,若模块只对外输出一个接口,使用不当,可能会无法按预期工作。比如:

// ❌ 以下模块的输出是“无效”的,最终输出值仍是 {}
exports = function () { console.log('Hi~') }

原因很简单,在默认情况下 module.exports 属性和 exports 变量都是同一个空对象 {}(默认值)的引用(reference),即 module.exports === exports

当对 exports 变量重新赋予一个基本值或引用值的时候, module.exportsexports 之间的联系被切断了,此时 module.exports !== exports,在当前模块下 module.exports 的值仍为 {},而 exports 变量的值变为函数。而 require() 方法的返回值是所引用模块的 module.exports 的浅拷贝结果。

正确姿势应该是:

module.exports = export = function () { console.log('Hi~') } // ✅

使用类似处理,使得 module.exportsexports 重新建立关联关系。

这里并不存在任何难点,仅仅是 JavaScript 基本数据类型和引用数据类型的特性罢了。如果你还是分不清楚的话,建议只使用 module.exports 进行导出,这样的话,就不会有问题了。

4.2 require 查找算法

require() 参数很简单,那么 require() 内部是如何查找模块的呢?

简单可以分为几类:

  • 加载 Node 内置模块
    形式如:require('fs')require('http') 等。

  • 相对路径、绝对路径加载模块
    形式如:require('./file')require('../file')require('/file')

  • 加载第三方模块(即非内置模块)
    形式如:require('react')require('lodash/debounce')require('some-library')require('#some-library') 等。

其中,绝对路径形式在实际项目中几乎不会使用(反正我是没用过)、而 require('#some-library') 形式目前仍在试验阶段...

以下基于 Node.js 官网 相关内容翻译并整理的版本(存档

场景:在 `Y.js` 文件下,`require(X)`,Node.js 内部模块查找算法:

1. 如果 `X` 为内置模块的话,立即返回该模块;

   因此,往 NPM 平台上发包的话,`package.json` 中的 `name` 字段不能与 Node.js 内置模块同名。

2. 如果 `X` 是以绝对路径或相对路径形式,根据 `Y` 所在目录以及 `X` 的值以确定所要查找的模块路径(称为 `Z`)。

  a. 将 `Z` 当作「文件」,按 `Z`、`Z.js`、`Z.json`、`Z.node` 顺序查找文件,若找到立即返回文件,否则继续往下查找;
  b. 将 `Z` 当作「目录」,
     1)查找 `Z/package.json` 是否存在,若 `package.json` 存在且其 `main` 字段值不为虚值,将会按照其值确定模块位置,否则继续往下;
     2)按 `Z/index.js`、`Z/index.json`、`Z/index.node` 顺序查找文件,若找到立即返回文件,否则会抛出异常 "not found"。

3. 若 `X` 是以 `#` 号开头的,将会查找最靠近 `Y` 的 `package.json` 中的 `imports` 字段中 `node`、`require` 字段的值确认模块的具体位置。
  (这一类现阶段用得比较少,后面再展开介绍一下)
   // https://github.com/nodejs/node/pull/34117

4. 加载自身引用 `LOAD_PACKAGE_SELF(X, dirname(Y))`

    a. 如果当前所在目录存在 `package.json` 文件,而且 `package.json` 中存在 `exports` 字段,
       其中 `name` 字段的值还要是 `X` 开头一部分,
       满足前置条件下,就会匹配 subpath 对应的模块(无匹配项会抛出异常)。
      (这里提到的 subpath 与 5.b.1).1.1 类似)
    b. 若不满足 a 中任意一个条件均不满足,步骤 4 执行完毕,继续往下查找。

5. 加载 node_modules `LOAD_NODE_MODULES(X, dirname(Y))`
   a. 从当前模块所在目录(即 `dirname(Y)`)开始,逐层查找是否 `node_modules/X` 是否存在,
      若找到就返回,否则继续往父级目录查找 `node_modules/X` ,依次类推,直到文件系统根目录。
   b. 从全局目录(指 `NODE_PATH` 环境变量相关的目录)继续查找。
  
   若 `LOAD_NODE_MODULES` 过程查找到模块 X(可得到 X 对应的绝对路径,假定为 M),将按以下步骤查找查找:
      1) 若 Node.js 版本支持 `exports` 字段(Node.js 12+),
          1.1 尝试将 `M` 拆分为 name 和 subpath 形式(下称 name 为 `NAME`)

              比如 `my-pkg` 拆分后,name 为 `my-pkg`,subpath 则为空(为空的话,对应  `exports` 的 "." 导出)。
              比如 `my-pkg/sub-module` 拆分后,name 为 `my-pkg`,subpath 为 `sub-module`。
              请注意带 Scope 的包,比如 `@myorg/my-pkg/sub-module` 拆分后 name 应为 `@myorg/my-pkg`,subpath 为 `sub-module`。

          1.2 如果在 M 目录下存在 `NAME/package.json` 文件,而且 `package.json` 的 `exports` 字段是真值,
              然后根据 subpath 匹配 `exports` 字段配置,找到对应的模块(若 subpath 匹配不上的将会抛出异常)。
              请注意,由于 `exports` 支持条件导出,而且这里查找的是 CommonJS 模块,
              因此 `exports` 的 `node`、`require`、`default` 字段都是支持的,键顺序更早定义的优先级更高。

          1.3 如果以上任意一个条件不满足的话,将继续执行 2) 步骤

      2) 将 X 以绝对路径的形式查找模块(即前面的步骤 2),若找不到步骤 5 执行完毕,将会跑到步骤 6。

6. 抛出异常 "not found"

如果不是开发 NPM 包,在实际使用中的话,要不并没有以上那么多复杂的步骤,很容易理解。但深入了解之后有助于平常遇到问题更快排查出原因并处理掉。如果你是发包的话,可以利用 exports 等按一定的策略导出模块。

想了解 Node.js package.json 的两个字段的意义,请看:

4.3 require 源码

源码 👉 node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

// Loads a module at the given file path. Returns that module's `exports` property.
Module.prototype.require = function (id) {
  validateString(id, 'id')
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string')
  }
  requireDepth++
  try {
    return Module._load(id, this, /* isMain */ false)
  } finally {
    requireDepth--
  }
}

源码 👉 node/lib/internal/modules/cjs/loader.js(Node.js v17.x)

/**
 * 检查所请求文件的缓存
 * 1. 如果缓存中已存在请求的文件,返回其导出对象(module.exports)
 * 2. 如果请求的是原生模块,调用 `NativeModule.prototype.compileForPublicLoader()` 并返回其导出对象
 * 3. 否则,为该文件创建一个新模块并将其保存到缓存中。 然后让它在返回其导出对象之前加载文件内容。
 */
Module._load = function (request, parent, isMain) {
  let relResolveCacheIdentifier
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id)
    // Fast path for (lazy loaded) modules in the same directory. The indirect
    // caching is required to allow cache invalidation without changing the old
    // cache key names.
    relResolveCacheIdentifier = `${parent.path}\x00${request}`
    const filename = relativeResolveCache[relResolveCacheIdentifier]
    if (filename !== undefined) {
      const cachedModule = Module._cache[filename]
      if (cachedModule !== undefined) {
        updateChildren(parent, cachedModule, true)
        if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule)
        return cachedModule.exports
      }
      delete relativeResolveCache[relResolveCacheIdentifier]
    }
  }

  // 1️⃣ 获取 require(id) 中 id 的绝对路径(filename 作为模块的标识符)
  const filename = Module._resolveFilename(request, parent, isMain)

  if (StringPrototypeStartsWith(filename, 'node:')) {
    // Slice 'node:' prefix
    const id = StringPrototypeSlice(filename, 5)

    const module = loadNativeModule(id, request)
    if (!module?.canBeRequiredByUsers) {
      throw new ERR_UNKNOWN_BUILTIN_MODULE(filename)
    }

    return module.exports
  }

  // 2️⃣ 缓动是否存在缓存
  // 所有加载过的模块都缓存于 Module._cache 中,以模块的绝对路径作为键值(cache key)
  const cachedModule = Module._cache[filename]

  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true)
    if (!cachedModule.loaded) {
      const parseCachedModule = cjsParseCache.get(cachedModule)
      if (!parseCachedModule || parseCachedModule.loaded) return getExportsForCircularRequire(cachedModule)
      parseCachedModule.loaded = true
    } else {
      // 若该模块缓存过,则直接返回该模块的 module.exports 属性
      return cachedModule.exports
    }
  }

  // 3️⃣ 加载 Node.js 原生模块(内置模块)
  const mod = loadNativeModule(filename, request)
  if (mod?.canBeRequiredByUsers) return mod.exports

  // 4️⃣ 若请求模块无缓存,调用 Module 构造函数生成模块实例 module
  const module = cachedModule || new Module(filename, parent)

  // 如果是入口脚本,将入口模块的 id 置为 "."
  if (isMain) {
    process.mainModule = module
    module.id = '.'
  }

  // 5️⃣ 将模块存入缓存中
  // ⚠️⚠️⚠️ 在模块执行之前,提前放入缓存,以处理「循环引用」的问题
  // See, http://nodejs.cn/api/modules.html#cycles
  Module._cache[filename] = module
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename
  }

  let threw = true
  try {
    // 6️⃣ 执行模块
    module.load(filename)
    threw = false
  } finally {
    if (threw) {
      delete Module._cache[filename]
      if (parent !== undefined) {
        delete relativeResolveCache[relResolveCacheIdentifier]
        const children = parent?.children
        if (ArrayIsArray(children)) {
          const index = ArrayPrototypeIndexOf(children, module)
          if (index !== -1) {
            ArrayPrototypeSplice(children, index, 1)
          }
        }
      }
    } else if (
      module.exports &&
      !isProxy(module.exports) &&
      ObjectGetPrototypeOf(module.exports) === CircularRequirePrototypeWarningProxy
    ) {
      ObjectSetPrototypeOf(module.exports, ObjectPrototype)
    }
  }

  // 7️⃣ 返回模块的输出接口
  return module.exports
}

4.4 require 中几个常见的问题

Q: Node.js 是如何实现同步加载机制的?
A:

未完待续...

References

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

推荐阅读更多精彩内容

  • 1. CommonJS 规范(同步加载 NodeJS) 2. AMD(异步加载模块 requireJS) 采用异步...
    菜鸡前端阅读 587评论 0 0
  • 前言 初期的web端交互还是很简单,不需要太多的js就能实现。随着时代的的发展,用户对Web浏览器的性能也提出了越...
    菠菜女皇阅读 435评论 0 0
  • ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。因为当前版本的ES6是在2015...
    陈大冲阅读 740评论 0 0
  • ECMAScript6(以下简称ES6)是JavScript语言的下一代标准。因为当前版本的ES6是在2015年发...
    imtns阅读 476评论 0 0
  • ES6 代码尽量标签化语义化、优先使用标签,class基于功能命名、基于内容命名、基于表现命布局提高css重用率,...
    唯轩_443e阅读 316评论 0 0