英雄指南——HTTP

版本:4.0.0+2

在本章,你会做以下改进。

  • 从一个服务器获取英雄数据。
  • 让用户添加、编辑和删除英雄。
  • 保存改变到服务器。

你会教给应用发起到一个远程服务器的 web API 的相应的 HTTP 请求。

当你按照本章做完这一切,应用看起来应该这样——在线示例 (查看源码)。

你离开的地方

前一章中,你学会了在仪表盘和固定的英雄列表之间导航,并编辑选定的英雄。这也就是本章的起点。

在继续英雄指南之前,验证你是否有如下结构。

angular_tour_of_heroes/
|___lib/
|   |___app_component.{css,dart}
|___src/
|   |   |___dashboard_component.{css,dart,html}
|   |   |___hero.dart
|   |   |___hero_detail_component.{css,dart,html}
|   |   |___hero_service.dart
|   |   |___heroes_component.{css,dart,html}
|   |   |___mock_heroes.dart
|___test/
|   |___app_test.dart
|   |___...
|___web/
|   |___index.html
|   |___main.dart
|   |___styles.css
|___pubspec.yaml

如果应用还没有运行,使用pub serve启动应用。当你做出改变时,通过刷新浏览器窗口,使其保持运行。

提供 HTTP 服务

你将使用 Dart的 http 包的客户端类来与服务器通信。

更新 Pubspec

通过添加Dart httpstream_transform 包来更新包依赖。

// {toh-5 → toh-6}/pubspec.yaml

dependencies:            
    angular: ^4.0.0            
    angular_forms: ^1.0.0            
    angular_router: ^1.0.2            
+   http: ^0.11.0            
+   stream_transform: ^0.0.6

注册 HTTP 服务

在应用可以使用BrowserClient之前,你必须把它当一个服务提供器注册。

你在应用的任何地方都应该能访问BrowserClient服务。因此在bootstrap 调用中注册它,那是你启动应用和根组件AppComponent的地方。

// web/main.dart (v1)

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';
import 'package:angular_tour_of_heroes/app_component.dart';
import 'package:http/browser_client.dart';
void main() {
  bootstrap(AppComponent, [
    ROUTER_PROVIDERS,
    // Remove next line in production
    provide(LocationStrategy, useClass: HashLocationStrategy),
    provide(BrowserClient, useFactory: () => new BrowserClient(), deps: [])
  ]);
}

注意,你在bootstrap方法的第二个参数列表中提供BrowserClient。这和在@Component注解中的providers列表中有同样的效果。

注意:除非你有一个适当配置的后端服务器(或一个模拟服务器),否则,这个应用并不工作。接下来的部分展示如何与一个后端服务器模拟交互。

模拟 Web API

在你有一个处理英雄数据请求的 Web 服务器之前,HTTP 客户端会从一个模拟服务器——内存 web API 获取并保存数据。

使用模拟服务器的版本来更新web/main.dart

// web/main.dart (v2)

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';
import 'package:angular_tour_of_heroes/app_component.dart';
import 'package:angular_tour_of_heroes/in_memory_data_service.dart';
import 'package:http/http.dart';
void main() {
  bootstrap(AppComponent, [
    ROUTER_PROVIDERS,
    // Remove next line in production
    provide(LocationStrategy, useClass: HashLocationStrategy),
    provide(Client, useClass: InMemoryDataService),
    // Using a real back end?
    // Import browser_client.dart and change the above to:
    // [provide(Client, useFactory: () => new BrowserClient(), deps: [])]
  ]);
}

你想要使用内存 Web API 服务代替BrowserClient,与远程服务器进行会话。内存 Web API 服务如下所示,是通过http库的MockClient类来实现的。所有的http客户端实现都共享了一个通用的Client接口。所以你要在应用中使用Client 类型,以便在各个实现之间自由地切换。

// lib/in_memory_data_service.dart (init)

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:angular/angular.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'src/hero.dart';
@Injectable()
class InMemoryDataService extends MockClient {
  static final _initialHeroes = [
    {'id': 11, 'name': 'Mr. Nice'},
    {'id': 12, 'name': 'Narco'},
    {'id': 13, 'name': 'Bombasto'},
    {'id': 14, 'name': 'Celeritas'},
    {'id': 15, 'name': 'Magneta'},
    {'id': 16, 'name': 'RubberMan'},
    {'id': 17, 'name': 'Dynama'},
    {'id': 18, 'name': 'Dr IQ'},
    {'id': 19, 'name': 'Magma'},
    {'id': 20, 'name': 'Tornado'}
  ];
  static List<Hero> _heroesDb;
  static int _nextId;
  static Future<Response> _handler(Request request) async {
    if (_heroesDb == null) resetDb();
    var data;
    switch (request.method) {
      case 'GET':
        final id =
            int.parse(request.url.pathSegments.last, onError: (_) => null);
        if (id != null) {
          data = _heroesDb
              .firstWhere((hero) => hero.id == id); // throws if no match
        } else {
          String prefix = request.url.queryParameters['name'] ?? '';
          final regExp = new RegExp(prefix, caseSensitive: false);
          data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList();
        }
        break;
      case 'POST':
        var name = JSON.decode(request.body)['name'];
        var newHero = new Hero(_nextId++, name);
        _heroesDb.add(newHero);
        data = newHero;
        break;
      case 'PUT':
        var heroChanges = new Hero.fromJson(JSON.decode(request.body));
        var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id);
        targetHero.name = heroChanges.name;
        data = targetHero;
        break;
      case 'DELETE':
        var id = int.parse(request.url.pathSegments.last);
        _heroesDb.removeWhere((hero) => hero.id == id);
        // No data, so leave it as null.
        break;
      default:
        throw 'Unimplemented HTTP method ${request.method}';
    }
    return new Response(JSON.encode({'data': data}), 200,
        headers: {'content-type': 'application/json'});
  }
  static resetDb() {
    _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList();
    _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1;
  }
  static String lookUpName(int id) =>
      _heroesDb.firstWhere((hero) => hero.id == id, orElse: null)?.name;
  InMemoryDataService() : super(_handler);
}

这个文件代替了mock_heroes.dart,它现在可以安全的删除了。

作为通用的 Web API 服务,模拟内存服务将以 JSON 格式进行编码和解码英雄,所以给Hero类增加这些功能:

// lib/src/hero.dart

class Hero {
  final int id;
  String name;
  Hero(this.id, this.name);
  factory Hero.fromJson(Map<String, dynamic> hero) =>
      new Hero(_toInt(hero['id']), hero['name']);
  Map toJson() => {'id': id, 'name': name};
}
int _toInt(id) => id is int ? id : int.parse(id);

英雄与 HTTP

在当前的HeroService的实现中,返回一个 Future 类型的模拟英雄。

Future<List<Hero>> getHeroes() async => mockHeroes;

这是最终使用 HTTP 客户端来获取英雄的预期实现,那一定是一个异步操作。

现在将getHeroes()转换为使用 HTTP 的形式 。

// lib/src/hero_service.dart (updated getHeroes and new class members)

  static const _heroesUrl = 'api/heroes'; // URL to web API

final Client _http;

HeroService(this._http);

Future<List<Hero>> getHeroes() async {
  try {
    final response = await _http.get(_heroesUrl);
    final heroes = _extractData(response)
        .map((value) => new Hero.fromJson(value))
        .toList();
    return heroes;
  } catch (e) {
    throw _handleError(e);
  }
}

dynamic _extractData(Response resp) => JSON.decode(resp.body)['data'];

Exception _handleError(dynamic e) {
  print(e); // for demo purposes only
  return new Exception('Server error; cause: $e');
}

更新导入语句如下:

// lib/src/hero_service.dart (updated imports)

import 'dart:async';
import 'dart:convert';

import 'package:angular/angular.dart';
import 'package:http/http.dart';

import 'hero.dart';

刷新浏览器。英雄数据应该从模拟服务器中成功加载。

HTTP Future

为获取到英雄列表,首先异步调用http.get()。然后使用_extractData助手方法来解码响应的正文。

响应的 JSON 有一个单一的data属性,保存了调用者想要的英雄列表。所以提取这个列表,并将它作为解析后的 Future 值返回。

注意这个由服务器返回的数据的形态。这个特殊的内存 Web API 的实例返回一个带有data属性的对象。你的 API 可能返回其它东西。调整代码以匹配你的 Web API。

调用者并不知道从(模拟)服务器获取英雄。和以前一样它接收一个英雄的 Future。

错误处理

getHeroes()的最后,你catch了服务器的失败信息,并把它们传给了错误处理器:

} catch (e) {
  throw _handleError(e);
}

这是一个关键的步骤!你必须预料到 HTTP 请求会失败,因为有太多超出控制的原因可能导致它们频繁发生。

Exception _handleError(dynamic e) {
  print(e); // for demo purposes only
  return new Exception('Server error; cause: $e');
}

这个示例服务把错误记录到控制台中;在真实世界中,你应该在代码中处理错误。作为一个展示,它能工作就够了。

这个代码还包含一个通过传播异常给调用者的错误,以便调用者可以给用户显示恰当的错误信息。

通过 id 获取英雄

HeroDetailComponent请求HeroService来获取一个英雄时,HeroService当前获取所有英雄并通过匹配id来过滤这一个。对于一个模拟环境是很好的,但当你只想要一个时,却向真实的服务器请求所有的英雄是浪费的。多数的 web APIs 支持这样的格式api/hero/:id(例如api/hero/11)的 get-by-id 请求。

使用 get-by-id 请求更新HeroService.getHero()方法:

// lib/src/hero_service.dart (getHero)

Future<Hero> getHero(int id) async {
  try {
    final response = await _http.get('$_heroesUrl/$id');
    return new Hero.fromJson(_extractData(response));
  } catch (e) {
    throw _handleError(e);
  }
}

这个请求和getHeroes()几乎一样。在 URL中的英雄 id 标识服务器应该更新哪个英雄。

另外,响应中的data是一个单一的英雄对象而不是一个列表。

未改变的 getHeroes API

尽管你对getHeroes()getHero()做了一些重要的内部修改,但公共签名并没有改变。从这两个方法仍然返回一个 Future。你不需要更新任何调用了它们的组件。

现在是时候添加创建和删除英雄的功能了。

更新英雄详情

尝试在英雄详情视图编辑一个英雄的名字了。随着你的输入,英雄名字也会随之在视图的标题更新。但如果你点击了 Back(后退)按钮,这些修改就丢失了。

之前更新是不会丢失的。有什么被改变了?当应用使用模拟英雄列表时,更新直接被应用到了单一的、全应用范围共享的列表中的英雄对象。现在你从一个服务器获取数据,如果你想要保存这些更改,你必须把它们写回到服务器。

添加保存英雄详情的功能

在英雄详情模板的结尾添加一个带click事件绑定的保存按钮,事件绑定会调用组件中一个名为save()的新方法。

// lib/src/hero_detail_component.html (save)

<button (click)="save()">Save</button>

添加下面的save()方法,它使用英雄服务的update()方法来保存英雄名的改变,然后导航回前一个视图。

// lib/src/hero_detail_component.dart (save)

Future<Null> save() async {
  await _heroService.update(hero);
  goBack();
}

给英雄服务添加 update() 方法

update()方法的总体结构和getHeroes()很相似,但它使用 HTTPput()来把修改保存到服务器端。

// lib/src/hero_service.dart (update)

static final _headers = {'Content-Type': 'application/json'};

Future<Hero> update(Hero hero) async {
  try {
    final url = '$_heroesUrl/${hero.id}';
    final response =
        await _http.put(url, headers: _headers, body: JSON.encode(hero));
    return new Hero.fromJson(_extractData(response));
  } catch (e) {
    throw _handleError(e);
  }
}

为了确定服务器应该更新哪个英雄,英雄id被编码进 URL 中。putt()body 参数是通过调用JSON.encode获得的英雄的 JSON 字符串编码。body 的内容类型(application/json)被标记在请求头中。

刷新浏览器,改变一个英雄的名字,保存你的修改,并点击浏览器的后退按钮。修改现在应该保存了。

增加添加英雄的功能

要添加一个英雄,应用需要这个英雄的名字。你可以使用一个input元素搭配一个添加按钮。

在 heroes 组件的 HTML 中,紧跟标题的后面,插入如下内容:

// lib/src/heroes_component.html (add)

<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>

响应一个点击事件,调用组件的点击处理器,然后清空这个输入框,以便准备好输入另一个名字。

// lib/src/heroes_component.dart (add)

Future<Null> add(String name) async {
  name = name.trim();
  if (name.isEmpty) return;
  heroes.add(await _heroService.create(name));
  selectedHero = null;
}

当给定的名字不为空时,处理器委托英雄服务来创建这个命名英雄,然后把这个新英雄添加到列表中。

HeroService类中实现这个create()方法。

// lib/src/hero_service.dart (create)

Future<Hero> create(String name) async {
  try {
    final response = await _http.post(_heroesUrl,
        headers: _headers, body: JSON.encode({'name': name}));
    return new Hero.fromJson(_extractData(response));
  } catch (e) {
    throw _handleError(e);
  }
}

刷新浏览器,并创建一些英雄。

增加删除英雄的功能

在英雄视图中的每个英雄都应该有一个删除按钮。

把下面的按钮元素添加到 heroes 组件的 HTML 中,把它放在重复的<li>元素里英雄名的后面。

<button class="delete"
  (click)="delete(hero); $event.stopPropagation()">x</button>

<li>元素应该看起来像这样:

// lib/src/heroes_component.html (li element)

 <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
    [class.selected]="hero === selectedHero">
  <span class="badge">{{hero.id}}</span>
  <span>{{hero.name}}</span>
  <button class="delete"
    (click)="delete(hero); $event.stopPropagation()">x</button>
</li>

除了调用组件的delete()方法,这个删除按钮的点击处理器代码阻止点击事件冒泡——你并不希望<li>的点击事件处理器被触发,因为这样做想要选择英雄时用户会删除这个英雄。

delete()处理器的逻辑有点棘手:

// lib/src/heroes_component.dart (delete)

Future<Null> delete(Hero hero) async {
  await _heroService.delete(hero.id);
  heroes.remove(hero);
  if (selectedHero == hero) selectedHero = null;
}

你当然委托给英雄服务来删除英雄,但该组件仍然负责更新显示:如果有必要,它从列表中移除 已删除的英雄,并重置所选英雄。

添加如下 CSS 把删除按钮放在英雄条目的最右边:

// 
lib/src/heroes_component.css (additions)

button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}

英雄服务的 delete() 方法

添加英雄服务的delete()方法,使用 HTTP 的delete()方法从服务器上移除英雄:

// lib/src/hero_service.dart (delete)

Future<Null> delete(int id) async {
  try {
    final url = '$_heroesUrl/$id';
    await _http.delete(url, headers: _headers);
  } catch (e) {
    throw _handleError(e);
  }
}

刷新浏览器,并试试这个新的删除功能。

数据流

回想一下,HeroService.getHeroes()等待一个http.get()响应,并生成一个List<Hero>Future。当你只对一个单一的结果感兴趣时,这很不错。

但是请求并不总是只发起一次。你可能开始发起一个请求,然后取消,并在服务器对第一个请求作出响应前发起一个不同的请求。一个请求-取消-新请求的序列难以通过Futures 实现,但使用 Streams 却很容易。

增加按名搜索的功能

你将为英雄指南添加一个英雄搜索 的特性。当用户在搜索框中输入名字时,你会重复发起根据名字过滤英雄的 HTTP 请求。

先创建HeroSearchService服务,它会把搜索查询发送到服务器的 Web API。

// lib/src/hero_search_service.dart

import 'dart:async';
import 'dart:convert';

import 'package:angular/angular.dart';
import 'package:http/http.dart';

import 'hero.dart';

@Injectable()
class HeroSearchService {
  final Client _http;

  HeroSearchService(this._http);

  Future<List<Hero>> search(String term) async {
    try {
      final response = await _http.get('app/heroes/?name=$term');
      return _extractData(response)
          .map((json) => new Hero.fromJson(json))
          .toList();
    } catch (e) {
      throw _handleError(e);
    }
  }

  dynamic _extractData(Response resp) => JSON.decode(resp.body)['data'];

  Exception _handleError(dynamic e) {
    print(e); // for demo purposes only
    return new Exception('Server error; cause: $e');
  }
}

HeroSearchService中的_http.get()调用和HeroService中的那一个类似,尽管现在这个 URL 带了查询字符串。

HeroSearchComponent

创建一个HeroSearchComponent,它调用这个新的HeroSearchService

组件模板很简单,就是一个输入框和一个相匹配的搜索结果列表。

// lib/src/hero_search_component.html

<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box"
         (change)="search(searchBox.value)"
         (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>

同时,给这个新组件添加样式。

// lib/src/hero_search_component.css

.search-result {
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 20px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}
#search-box {
  width: 200px;
  height: 20px;
}

当用户在搜索框中输入时,一个 keyup 事件绑定调用该组件的search()方法,并传入新的搜索框的值。如果用户使用鼠标粘贴文本,change 事件绑定会被触发。

不出所料,*ngFor从该组件的heroes属性重复 hero 对象。

但你很快就会看到,现在heroes属性是一个英雄列表的 Stream,而不再只是一个英雄列表。*ngFor 不能使用Stream做任何事,除非你通过async管道(AsyncPipe)联通它。async管道订阅Stream,并为*ngFor生成一个英雄列表。

创建HeroSearchComponent类及其元数据。

// lib/src/hero_search_component.dart

import 'dart:async';
import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';
import 'package:stream_transform/stream_transform.dart';
import 'hero_search_service.dart';
import 'hero.dart';
@Component(
  selector: 'hero-search',
  templateUrl: 'hero_search_component.html',
  styleUrls: const ['hero_search_component.css'],
  directives: const [CORE_DIRECTIVES],
  providers: const [HeroSearchService],
  pipes: const [COMMON_PIPES],
)
class HeroSearchComponent implements OnInit {
  HeroSearchService _heroSearchService;
  Router _router;
  Stream<List<Hero>> heroes;
  StreamController<String> _searchTerms =
      new StreamController<String>.broadcast();
  HeroSearchComponent(this._heroSearchService, this._router) {}
  // Push a search term into the stream.
  void search(String term) => _searchTerms.add(term);
  Future<Null> ngOnInit() async {
    heroes = _searchTerms.stream
        .transform(debounce(new Duration(milliseconds: 300)))
        .distinct()
        .transform(switchMap((term) => term.isEmpty
            ? new Stream<List<Hero>>.fromIterable([<Hero>[]])
            : _heroSearchService.search(term).asStream()))
        .handleError((e) {
      print(e); // for demo purposes only
    });
  }
  void gotoDetail(Hero hero) {
    var link = [
      'HeroDetail',
      {'id': hero.id.toString()}
    ];
    _router.navigate(link);
  }
}
搜索条目

聚焦_searchTerms

StreamController<String> _searchTerms =
    new StreamController<String>.broadcast();

// Push a search term into the stream.
void search(String term) => _searchTerms.add(term);

StreamController,顾名思义,是一个 Stream 的控制器,比如它允许你通过给它添加数据来操作潜在的数据流。

在这个例子中,字符串的潜在的数据流(_searchTerms.stream),表示与用户输入的搜索词匹配的英雄名。每次调用search(),都会通过调用控制器的add(),把新的字符串放进数据流。

初始化 heroes 属性(ngOnInit

你可以把搜索条目的数据流转换成Hero列表的数据流,并把结果赋值给heroes属性。

Stream<List<Hero>> heroes;

Future<Null> ngOnInit() async {
  heroes = _searchTerms.stream
      .transform(debounce(new Duration(milliseconds: 300)))
      .distinct()
      .transform(switchMap((term) => term.isEmpty
          ? new Stream<List<Hero>>.fromIterable([<Hero>[]])
          : _heroSearchService.search(term).asStream()))
      .handleError((e) {
    print(e); // for demo purposes only
  });
}

每次用户按键都立刻传给HeroSearchService,就会创建海量的 HTTP 请求,浪费服务器资源并消耗大量网络流量。

相反,你可以使用链式调用Stream操作符,减少流向字符串Stream的请求。你将发起较少的HeroSearchService调用,并且仍然及时地获得结果。做法如下:

  • transform(debounce(... 300))):在传递最终的字符串之前,会一直等待,直到搜索条目暂停输入300毫秒。你发起请求的频率永远不会超过300ms。
  • distinct():确保只在过滤文本变化时才发送请求。
  • transform(switchMap(...)):为每个已经通过debounce()distinct()的搜索条目调用搜索服务。它取消并丢弃之前的搜索,仅返回最新的搜索服务流元素。
  • handleError():处理错误。这个简单的例子只是把错误信息打印到控制台;实际的应用应该做得更好。

为仪表盘添加搜索组件

把英雄搜索的 HTML 元素添加到DashboardComponent模版的底部。

// 
lib/src/dashboard_component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes"  [routerLink]="['HeroDetail', {id: hero.id.toString()}]"  class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>
<hero-search></hero-search>

最后,从hero_search_component.dart中导入HeroSearchComponent,并把它添加到directives列表中。

// lib/src/dashboard_component.dart (search)

import 'hero_search_component.dart';

@Component(
  selector: 'my-dashboard',
  templateUrl: 'dashboard_component.html',
  styleUrls: const ['dashboard_component.css'],
  directives: const [CORE_DIRECTIVES, HeroSearchComponent, ROUTER_DIRECTIVES],
)

再次运行应用。在仪表盘中的搜索框中输入一些文字。如果你输入的字符匹配到了任何现有英雄名,你将会看到如下效果:

应用的结构与代码

回顾本章示例的源代码 在线示例 (查看源码)。验证你是否有如下结构:

angular_tour_of_heroes/
|___lib/
|    |___app_component.{css,dart}
|    |___in_memory_data_service.dart (new)
|    |___src/
|    |     |___dashboard_component.{css,dart,html}
|    |     |___hero.dart
|    |     |___hero_detail_component.{css,dart,html}
|    |     |___hero_search_component.{css,dart,html} (new)
|    |     |___hero_search_service.dart (new)
|    |     |___hero_service.dart
|    |     |___heroes_component.{css,dart,html}
|___test/
|    |___app_test.dart
|    |___...
|___web/
|    |___main.dart
|    |___index.html
|    |___styles.css
|___pubspec.yaml

最后冲刺

旅程即将结束,不过你已经收获颇丰。

  • 添加了在应用中使用 HTTP 时必要的依赖。
  • 重构了HeroService,从一个 Web API 来加载英雄。
  • 扩展了HeroService,以支持post()put()delete()方法。
  • 更新了组件,以允许英雄的添加、编辑和删除。
  • 配置了一个内存 Web API。
  • 学会了如何使用 Streams。

下一步

回到学习路径,在哪里你可以阅读更多关于在本教程中的概念和练习。

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

推荐阅读更多精彩内容