Express4.x源碼分析

1.源碼目錄結構

Express4.x自己實現了一個router組件,實現http請求的順序流程處理,去除了很多綁定的中間件,使代碼更清晰。

下面是express4.14的文件結構圖:


這里寫圖片描述

1.middleware(中間件)下主要有init.js和query.js,init.js的作用是初始化request,response,而query.js中間件的作用是格式化url,將url中的rquest參數剝離,儲存到req.query中;
2.router文件夾為router組件,包括index.js、route.js和layer.js,router組件負責中間件的插入和鏈式執行,具體將在下面講解;
3.express.js和application.js是主要的框架文件,暴露了express的api;
4.request.js和response.js提供了一些方法豐富request和response實例的功能,如req.is、req.get、req.params、req.originalUrl等;
5.view.js封裝了模板渲染引擎,通過res.render()調用引擎渲染網頁。

2. Express啟動過程分析

先看一下官方示例

var express = require('express');
var app = express();

app.get('/', function(req, res){
  res.send('Hello World');
});

app.listen(3000);

運行后訪問localhost:3000顯示Hello World。下面讓我們仔細看一下這段代碼。

首先第一行

var express = require('express');

require('express')載入了express框架,我們來看<font color=red size=3>源代碼中的index.js</font>

'use strict';

module.exports = require('./lib/express');

好吧,還要繼續require,我們來看<font color=red size=3>./lib/express.js</font>

'use strict';

/**
 * 依賴模塊.
 */

var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');

/**
 * 暴露`createApplication()`.
 */

exports = module.exports = createApplication;

/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}

從 <font color=red size=3>exports = module.exports = createApplication;</font>可以看出var express =require('express')最后實際是這個createApplication函數,createApplication就相當于express的'main'函數。

createApplication的開始定義了一個函數,函數有形參req,res,next為回調函數。函數體只有一條語句,執行<font color=red size=3> app.handle </font>,<font color=red size=3> handle </font>方法在<font color=red size=3> application.js </font>文件中定義,<font color=red size=3> handle </font>的代碼如下:

/**
 * Dispatch a req, res pair into the application. Starts pipeline processing.
 *
 * If no callback is provided, then default error handlers will respond
 * in the event of an error bubbling through the stack.
 *
 * @private
 */

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  // no routes
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

  router.handle(req, res, done);
};

看它的注釋可知<font color=red size=3>app.handle</font>的作用就是將每對[req,res]進行逐級分發,作用在每個定義好的路由及中間件上,直到最后完成。

再看看createApplication方法中間的兩行

mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);

<font color=red size=3>mixin</font>是在頭部require載入的<font color=red size=3>merge-descriptors</font>模塊,它的代碼如下

function merge(dest, src, redefine) {
  if (!dest) {
    throw new TypeError('argument dest is required')
  }

  if (!src) {
    throw new TypeError('argument src is required')
  }

  if (redefine === undefined) {
    // Default to true
    redefine = true
  }

  Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {
    if (!redefine && hasOwnProperty.call(dest, name)) {
      // Skip desriptor
      return
    }

    // Copy descriptor
    var descriptor = Object.getOwnPropertyDescriptor(src, name)
    Object.defineProperty(dest, name, descriptor)
  })

  return dest
}

<font color=red size=3>Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {})</font>將src參數的屬性遍歷,屬性名稱傳入參數name中,<font color=red size=3>Object.defineProperty(dest, name, descriptor)</font> 則將src的每一個屬性name和name的值descriptor復制到目標參數dest中,所以 <font color=red size=3>mixin(app, proto, false);</font> 的作用即是將proto中所有的property全部導入進app,第三個參數false表示app中已有的屬性不被proto的屬性所覆蓋,proto定義了大部分express的public api,如app.set,app.get,app.use...詳見官方的API文檔。<font color=red size=3> mixin(app, EventEmitter.prototype, false); </font>則將Node.js的EventEmitter中的原型方法全部導入了app。

再來看createApplication接下來的兩行

app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };

這里定義了app的 <font color=red size=3>request</font> 和 <font color=red size=3>response</font> 對象,使其分別繼承自req(頂部導入的<font color=red size=3> request.js</font> )和res(頂部導入的<font color=red size=3> response.js</font> ),另外,把app對象賦值給app參數是為了后面在<font color=red size=3> request </font>和<font color=red size=3> response </font>對象中能夠通過<font color=red size=3> this.app </font>獲得所創建的<font color=red size=3>express實例</font>。

接下來是

app.init();。

顯然,作用是初始化,做哪些工作呢?

app.init = function(){
  this.cache = {};
  this.settings = {};
  this.engines = {};
  this.defaultConfiguration();
};

設定了cache對象(render的時候用到),各種setting的存儲對象,engines對象(模板引擎),最后進行默認的配置,代碼有點長這里就不貼了,就是做一些默認的配置。

看官網示例下一句

app.get('/', function(req, res){
res.send('Hello World');
});

app.get可以獲取app.set設置的全局變量,也可以設置路由的處理函數,下面是get實現的源碼

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

methods是一個數組,存儲了http所有請求的類型,在method模塊里定義,除了基本的get、post請求外,還有多達十幾種請求,可能是為了兼容新的http標準吧。app[method]中,method=='get'且只有一個參數,則執行set,將route和回調存儲進一個棧中。遇到http請求時觸發執行,app.get也將產生一條路由中間件,執行后返回瀏覽器html頁面。具體的路由組件代碼將在后面分析。

還有最后一句

app.listen(3000);

listen方法的代碼如下

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

這里其實是調用了Node.js原生的http模塊的CreatServer方法創建服務器。

3.中間件

所謂中間件,就是在收到請求后和發送響應之前這個階段執行的一些函數。
express對象的use方法可以在一條路由的處理鏈上插入中間件,如

//加載路由
app.use( '/', require( './routes/index' ) );

當你為某個路徑安裝了中間件,則當以該路徑為基礎的路徑被訪問時,都會應用該中間件。比如你為“/”設置了中間件,那么所有請求都會應用該中間件。

中間件函數的原型如下:

function (req, res, next)

第一個參數是Request對象req。第二個參數是Response對象res。第三個則是用來驅動中間件調用鏈的函數next,如果你想讓后面的中間件繼續處理請求,就需要調用next方法。
app.static中間件
Express提供了一個static中間件,可以用來處理網站里的靜態文件的GET請求,可以通過express.static訪問。express.static的用法如下:

express.static(root, [options])

第一個參數root,是要處理的靜態資源的根目錄,可以是絕對路徑,也可以是相對路徑。第二個可選參數用來指定一些選項,比如maxAge、lastModified等,一個典型的express.static應用如下:

//全局變量
global.ABSPATH = path.join( __dirname, '/' );

//express應用static中間件
const staticOptions = {
    dotfiles: "ignore", //allow,deny,ignore
    etag: true,
    extensions: false,
    index: "index.html",    //set false to disable directory indexing
    lastModified: true,
    maxAge: 0,
    redirect: true,
    setHeaders () {}
};

app.use( express.static( path.join( __dirname, 'public' ), staticOptions ) );
app.use( express.static( path.join( __dirname, 'upload' ), staticOptions ) );

上面這段代碼將相對路徑下的public和upload目錄作為靜態文件,并設置staticOptions 那些屬性,如Cache-Control頭部的max-age選項為0天。還有其它一些屬性,請對照express.static的文檔來理解。

4.Router組件

Router是Express中一個非常核心的東西,基本上就是一個簡化版的Express框架。下面我們來一起看看,app.get()是如何實現的,之前我們在application.js中已經發現app.get的實現代碼:

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

從上面的代碼可以看出app.get()函數如果參數長度是1,則返回app.set()定義的變量,如果參數長度大于1,則進行路由處理。繼續往下看 this.lazyrouter(),從名字來看,好像是懶加載router,那我們看看源碼:

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

由此可以看出_router是Router的實例,如果_router不存在,就new一個Router出來,而這個Router就是我們剛才在目錄結構中看到的router目錄,也就是今天的主角Router組件。繼續上邊的代碼,加載完_router之后,執行了this._router.route(path)這樣一行代碼,那這行代碼有做了什么呢,我們再繼續往下挖,我們在router目錄下的index.js中找到了它的實現:

//Create a new Route for the given path.
proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

這里new了一個Route對象,并且new了一個Layer對象,然后將Route對象賦值給layer.route,最后將這個Layer添加到stack數組中。我們先來看看Route,這個Route是什么呢,它和Router組件有什么關系呢?
這里先聲明一下,本文提到的路由容器(Router)代表“router/index.js”文件的到導出對象,路由中間件(Route)代表“router/route.js”文件的導出對象。
首先,Router是怎么來的呢,Router對象只會在首次調用lazyrouter時被實例化,然后賦值給app._router字段。而Route只是路由中間件,封裝了路由信息,這里要特別注意<font color=red size=3> Router與Route的區別,Router可以看作是一個中間件容器,不僅可以存放路由中間件(Route),還可以存放其他中間件,在lazyrouter方法中實例化Router后會首先添加兩個中間件:query和init;而Route 僅僅是路由中間件,封裝了路由信息。Router和Route都各自維護了一個stack數組,該數組就是用來存放中間件和路由的。</font>
Router和Route的stack是有差別的,這個差別主要體現在存放的layer(<font color=red size=3>layer是用來封裝中間件的一個數據結構</font>)不太一樣

這里寫圖片描述

由于Router.stack中存放的中間件包括但不限于路由中間件,而只有路由中間件的執行才會依賴與請求method,因此Router.stack里的layer沒有method屬性,而是將其動態添加到了Route.stack的layer中;layer.route字段也是動態添加的,可以通過該字段來判斷中間件是否是路由中間件??梢酝ㄟ^兩種方式添加中間件:app.use和app[method],前者用來添加非路由中間件,后者添加路由中間件,這兩種添加方式都在內部調用了Router的相關方法來實現:

/*在router目錄下的index.js*/

/*添加非路由中間件*/
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  /* 此處略去部分代碼 */
  
  callbacks.forEach(function (fn) {
    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }
    // 添加中間件
    debug('use %s %s', path, fn.name || '<anonymous>');
    //實例化layer對象并進行初始化
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;
    this.stack.push(layer);
  }
  return this;


/*添加路由中間件*/
proto.route = function(path){
  //實例化路由對象
  var route = new Route(path);
  //實例化layer對象并進行初始化
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  //指向剛實例化的路由對象(非常重要),通過該字段將Router和Route關聯來起來
  layer.route = route;
  this.stack.push(layer);
  return route;
};

對于路由中間件,路由容器中的stack(<font color=red size=3>Router.stack</font>)里面的layer通過route字段指向了路由對象,那么這樣一來,<font color=red size=3>Router.stack</font>就和<font color=red size=3>Route.stack</font>發生了關聯,關聯后的示意模型如下圖所示:


這里寫圖片描述

這里大家就會發現,express實例在處理路由的時候,會先創建一個Router對象,然后用Router對象和對應的path來生成一個Route對象,最后由Route對象來處理具體的路由實現。
好了,那接下來我們繼續深入研究,看看route.method究竟做了什么,我們找到route.js文件,發現如下的代碼:

methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
        throw new Error(msg);
      }

      debug('%s %s', method, this.path);

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});

原來route和application運用了同樣的技巧,通過循環methods來動態添加method函數,我們直接看函數內部實現,首先通過入參獲取到handles,這里的handles就是我們定義的路由中間件函數,這里我們可以看到是一個數組,所以我們可以給一個路由添加多個中間件函數。接下來循環handles,在每個循環中利用handle來創建一個Layer對象,然后將Layer對象push到stack中去,這個stack其實是Route內部維護的一個數組,用來存放所有的Layer對象?,F在你一定想這道這個Layer到底是什么東西,那我們來看看layer.js的源代碼:

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %s', path);
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  if (path === '/' && opts.end === false) {
    this.regexp.fast_slash = true;
  }
}

上邊是Layer的構造函數,我們可以看到這里定義handle,params,path和regexp等幾個主要的屬性:

1.其中最重要的就是handle,它就是我們剛剛在route中創建Layer對象傳入的中間件函數。
2.params其實就是req.params,至于如何實現的我們可以以后再做探討,今天先不做說明。
3.path就是我們定義路由時傳入的path。
4.regexp對于Layer來說是比較重要的一個屬性,因為下邊進行路由匹配的時候就是靠它來搞定的,而它的值是由pathRegexp得來的,其實這個pathRegexp對應的是一個第三方模塊path-to-regexp,它的功能是將path轉換成regexp。
我們再來看看Layer有什么方法:

/**
 * Check if this route matches `path`, if so
 * populate `.params`.
 *
 * @param {String} path
 * @return {Boolean}
 * @api private
 */

Layer.prototype.match = function match(path) {
  if (path == null) {
    // no path, nothing matches
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  if (this.regexp.fast_slash) {
    // fast path non-ending match for / (everything matches)
    this.params = {};
    this.path = '';
    return true;
  }

  var m = this.regexp.exec(path);

  if (!m) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = m[0];

  var keys = this.keys;
  var params = this.params;

  for (var i = 1; i < m.length; i++) {
    var key = keys[i - 1];
    var prop = key.name;
    var val = decode_param(m[i]);

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }

  return true;
};

match函數主要用來匹配path的,當我們向express發送一個http請求時,當前請求對應的是哪個路由,就是通過這個match函數來判斷的,如果path中帶有參數,match還會把參數提取出來賦值給params,所以說match是整個路由中很重要的一點。還有下面一個處理中間件的函數

/**
 * Handle the request for the layer.
 *
 * @param {Request} req
 * @param {Response} res
 * @param {function} next
 * @api private
 */

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

從上邊的代碼我們可以看到調用了fn,而這個fn就是layer的handle屬性,就是我們定義路由時傳入的路由中間件,到這里我們總算找到了我們的路由中間件被執行的地方。那Layer和Route之間又有什么千絲萬縷的聯系呢?

每個Route都會維護一個Layer數組,每一個Layer對應一個中間件函數,Layer存儲了每個路由的path和handle等信息,并且實現了match和handle的功能,所以可以發現Route和Layer是一對多的關系,每個Route代表一個路由,而每個Layer對應的是路由的每一個中間件函數。

講完了Route和Layer的關系,我們再來回頭看看Router和Layer的關系,從index.js中prop.route方法

proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

我們可以看出來Router每次添加一個route,都會把route包裝到layer中,并且將layer添加到自己的stack中,那為什么要把route包裝到layer中呢,前邊我們已經仔細研究了Layer模塊的代碼,我們發現Layer具有match和handle的功能,這樣我們就可以通過Layer的match來進行route的匹配了。這里有一個關鍵點我們需要特別講解下,上邊的代碼中在創建Layer對象的時候傳入的handle函數為route.dispatch.bind(route),route.dispatch是通過next()獲取stack中的每一個layer來執行相應的路由中間件,這樣就保證了我們定義在路由上的多個中間件函數被按照定義的順序依次執行。

我們接下來來重新梳理一下router相關的所有內容,看看express究竟是如何對http請求進行路由的。

當客戶端發送一個http請求后,會先進入express實例對象對應的router.handle函數中,router.handle函數會通過next()遍歷stack中的每一個layer進行match,如果match返回true,則獲取layer.route,執行route.dispatch函數,route.dispatch同樣是通過next()遍歷stack中的每一個layer,然后執行layer.handle_request,也就是調用中間件函數。直到所有的中間件函數被執行完畢,整個路由處理結束。

5.View的實現

渲染模板使用的是 res.render(),它實現總體來說經過三次封裝,進行了一些配置,調用鏈條為
res.render() => app.render() =>view.render()=> require("jade")/reqiure("ejs").render()
首先看app.engine,將jade或ejs模板引擎的render函數存入了engines數組中

app.engine = function engine(ext, fn) {
  if (typeof fn !== 'function') {
    throw new Error('callback function required');
  }

  // get file extension
  var extension = ext[0] !== '.'
    ? '.' + ext
    : ext;

  // store engine
  this.engines[extension] = fn;

  return this;
};

app.defaultConfiguration()(application.js中初始化的一個函數),把View的構造函數保存。

 // default configuration
  this.set('view', View);

app.render()將其取出并調用,初始化一個View實例,并執行‘view.render()’渲染模板,注意初始化函數將engines傳入了View實例,里面保存了模板引擎的render函數。

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;

  /*此處省略部分代碼*/

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    if (!view.path) {
      var dirs = Array.isArray(view.root) && view.root.length > 1
        ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
        : 'directory "' + view.root + '"'
      var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
      err.view = view;
      return done(err);
    }

    // prime the cache
    if (renderOptions.cache) {
      cache[name] = view;
    }
  }

  // render
  tryRender(view, renderOptions, done);
};

view.render()執行的便是模板引擎的render函數,callback為渲染完成后的回調函數。

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

總結視圖渲染的流程為, res.render() 調用了 app.render() 。在 app.render() 中,先創建一個 view 對象(相關源碼為 view.js ),然后調用 view.render() 。如果允許緩存,即 app.enabled('view cache') 的話,則會優先檢查緩存,如果緩存中已有相關視圖,則直接取出;否則才會新創建一個視圖對象。

最后總結一下,其實整個Express執行過程就是往req、res不停地添加和修改屬性;中間件也是通過app作為回調,進而修改req、res;其中app.use和app.static用來添加中間件,app.handle則將每對[req,res]進行逐級分發,作用在每個定義好的路由及中間件上,直至最后完成分發。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容