[See How]全棧Node TS框架TSRPC實踐教程(一)

博客和公眾號

此文已同步到因卓誒博客,請大家關(guān)注同名公眾號

[See How]全棧Node TS框架TSRPC實踐教程(一)www.yinzhuoei.com

前言

某個普通的一天的早晨,水友群的小姐姐和我聊前端架構(gòu)涉馁,因為她們組最近要籌備一些新項目江咳,在做架構(gòu)的中途出現(xiàn)了很多問題返咱,所以我拿到了她們的架構(gòu)項目腳手架代碼氮帐。拿到代碼之后我發(fā)現(xiàn)深圳那邊的前端團(tuán)隊普遍做的很好,有先進(jìn)的架構(gòu)思想洛姑,也把ts用的很純粹上沐,最后沒幫人家解決問題,反倒是自己學(xué)到了不少楞艾。最后我們聊到了前后端全棧開發(fā)参咙,如何動態(tài)校驗協(xié)議參數(shù)等問題,因為熟悉我開源項目(劍指題解)的朋友都知道硫眯,我的后端代碼尤其是動態(tài)校驗?zāi)菈K寫的是真差蕴侧,為了ts而用ts,這也是目前很多用ts的小伙伴的通病两入,所以我一直打算重構(gòu)我的一部分后端代碼净宵,這個時候見多識廣的小姐姐就推薦給我了一個框架,這個框架也是[see how]系列第一篇教程的主角裹纳,這個框架就叫做TSRPC

關(guān)于專欄

關(guān)于see how是什么择葡,說來很巧,這也是TSRPC作者王大大對我的seho這個名字的猜測剃氧,其實我的一個名字也沒那么多深意敏储,然后被大佬解讀成了see how,所以我感覺這是一個不錯的idea朋鞍,那么本來就是想要出一個tsrpc的系列教程已添,和大家一起學(xué)習(xí)這個優(yōu)秀的框架,就正好作為see how 專欄的第一篇文章吧滥酥。

關(guān)于TSRPC

在正文開始之前更舞,我希望大家可以去自行先去簡單快速的瀏覽相關(guān)知識,tsrpc是一個ts的開源rpc框架坎吻,它是為了全棧項目而生的缆蝉,從我上手的第一天開始,我就對這個框架有了以下的第一印象:

  1. 天然二進(jìn)制傳輸
  2. 純粹的ts禾怠,規(guī)避了極大部分開發(fā)中的錯誤
  3. 強(qiáng)大的運行時復(fù)雜檢測
  4. 這種前后端開發(fā)模式返奉,我聞所未聞

官方文檔
視頻教程

前期準(zhǔn)備

學(xué)習(xí)tsrpc需要你有一些前置知識和其他準(zhǔn)備:

  1. 熟悉typescript基本語法
  2. 準(zhǔn)備一個mongodb數(shù)據(jù)庫

開發(fā)

使用tsrpc開發(fā)全棧應(yīng)用簡單到?jīng)]朋友,可以從官方提供的cli快速創(chuàng)建前后端一體項目:

npx create-tsrpc-app@latest

按照指引選擇瀏覽器應(yīng)用吗氏,等待完成安裝之后,你的目錄中會出現(xiàn)2個目錄:

- backend 后端- frontend 前端

我們直接一睹為快雷逆,在前端項目根目錄運行

官方的腳手架為我們準(zhǔn)備了一個簡單的todolist應(yīng)用

整個前后端的目錄結(jié)構(gòu)(摘抄官網(wǎng))

|- backend --------------------------- 后端項目    |- src        |- shared -------------------- 前后端共享代碼(同步至前端)            |- protocols ------------- 協(xié)議定義        |- api ----------------------- API 實現(xiàn)        index.ts|- frontend -------------------------- 前端項目    |- src        |- shared -------------------- 前后端共享代碼(只讀)            |- protocols        |- index.ts

誒弦讽,你可能會疑問了,為啥會有一個莫名其妙的shared目錄,還要給前端項目去分享這個目錄往产。是因為在shared這個目錄我們要定義協(xié)議被碗,啥玩意是協(xié)議呢?我們通過一個小小的接口來給大家解釋什么是協(xié)議仿村;

export interface ReqAddPost {  newPost: {     name: string;  };}export interface ResAddPost {  insertedId: string;}

我們可以在shared/protocols中新建了一個文件PtlAddPost.ts锐朴,我們必須以Ptl進(jìn)行開頭定義協(xié)議,協(xié)議是用來描述一個接口的請求和響應(yīng)的結(jié)構(gòu)體的文件蔼囊,你可以這么理解焚志。協(xié)議文件通過shared目錄共享到前端,你知道會發(fā)生什么事情嗎畏鼓?造成了我們前端在對接口的時候酱酬,全程代碼提示以及嚴(yán)格和請求和返回類型校驗。

那么我們接著后端繼續(xù)聊云矫,協(xié)議定義之后該如何做呢膳沽?

npm run proto 每當(dāng)協(xié)議更改后,需要重新運行這個命令

tsrpc的設(shè)計是協(xié)議和api分離让禀,我們必須要清楚挑社,api在我的認(rèn)知里就是一個異步函數(shù),tsrpc可以幫助我們根據(jù)我們剛剛寫的協(xié)議生成api巡揍,比如剛剛我們實現(xiàn)的PtlAddPost.ts滔灶,我們運行

在api目錄中會多出一個ApiAddPost.ts

import { ApiCall } from "tsrpc";import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {  }

我們通過call這個方法獲取請求參數(shù)以及響應(yīng)給客戶端一些信息,我們來一個簡單的例子:

export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {  if(call.req.newpost.name){     call.success({         msg: "hello," + call.req.newpost.name     })  }else{     call.error('Invalid name');  }}

我們的第一個api已經(jīng)寫完了吼肥,我們需要正常的過一次test录平,然后我們在讓前端去調(diào)用。

tsrpc使用的是mocha這個測試框架缀皱。

import { HttpClient } from "tsrpc";import { serviceProto } from "../../src/shared/protocols/serviceProto";describe("api 測試", async function () {  let client = new HttpClient(serviceProto, {    server: "http://127.0.0.1:3000",    logger: console,  });  let ret = await client.callApi("AddTest", {    newPost: {      name: "seho"    },  });});

這是我們后端的一個簡單的測試用例斗这,在運行這個測試用例之前,您必須要開啟后端的服務(wù):

然后可以再開啟一個窗口運行npm run test啤斗,如果一切正常表箭,你可以看到下面的控制臺輸出:

粗略計算了一下,我們從開始定義協(xié)議到api測試完成钮莲,一個簡單的接口不到5分鐘就已經(jīng)完成免钻。

這個時候我們可以把這個接口放到前端再繼續(xù)測試一下。

當(dāng)然在此之前崔拥,我們需要運行以下命令:

我們之前提到過极舔,前后端有一個共享的目錄,運行此命令我們就可以把協(xié)議等信息同步過來链瓦,這個時候我們可以在前端的index.ts文件中拆魏,可以獲得非常完善的代碼提示盯桦。

import { HttpClient } from "tsrpc-browser";import { serviceProto } from "./shared/protocols/serviceProto";let client = new HttpClient(serviceProto, {  server: "http://127.0.0.1:3000",  logger: console,});client.callApi("AddTest", {  newPost: {    name: "hello, seho"  },});

當(dāng)我們回到瀏覽器前端頁面上時,這個請求就會發(fā)出渤刃,如果你仔細(xì)觀察控制臺拥峦,會看到以下的場景:

我們的請求體被二進(jìn)制序列化了,這也是tsrpc的特點之一卖子,我們會在稍后的段落中對tsrpc各個特性做介紹略号,但是此時此刻我們已經(jīng)完成了一個api的后端開發(fā)->test測試->前端調(diào)用。

完善我們的程序

上一個部分相信大家已經(jīng)學(xué)會了如何使用tsrpc開發(fā)第一個api洋闽,這個部分結(jié)合了tsrpc的視頻教程中的案例玄柠,我們需要做一個簡單的CRUD,使用mongoDB喊递。

我們需要在本地啟動我們的mongoDB服務(wù)随闪,然后我們需要添加一些代碼到后端backend項目中。

代碼開始之前骚勘,我們需要安裝mongoDB的依賴铐伴,我們可以更方便的引入類型定義以及各種數(shù)據(jù)庫方法。

為了和視頻教程統(tǒng)一俏讹,我們的工具類/架構(gòu)方式当宴,將直接挪用視頻教程中的代碼:

  • 寫一個數(shù)據(jù)庫的表模型,名為Post.ts
export interface Post {  _id: string;  author: string;  title: string;  content: string;  visitedNum: number;  create: {    uid: string;    time: Date;  };  update?: {    uid: string;    time: Date;  };}

我們的數(shù)據(jù)庫模型是需要共享到前端的泽疆,方便前端工程能夠復(fù)用户矢,但是為了確保后端的類型安全,我們需要在模型上多做一層處理殉疼。mongodb的id屬性不是string梯浪,而是ObjectID,所以我們需要在后端對模型進(jìn)行類型重寫(只重寫id字段)瓢娜。

關(guān)于為什么要在后端多做一層封裝是因為不可能在前端引入mongodb中的objectID

import { ObjectID } from "mongodb";import { Overwrite } from "tsrpc";import { Post } from "../Post";export type DbPost = Overwrite<Post, {    _id: ObjectID}>

我們使用tsrpc提供的Overwrite泛型對剛剛寫的Post類型進(jìn)行改寫挂洛,將mongodb中的objectID類型引入進(jìn)來進(jìn)行替換,然后我們后端工程就要使用這個Dbpost類型眠砾,而不是剛剛我們寫的Post類型虏劲。

  • 數(shù)據(jù)庫相關(guān)配置
export const BackConfig = {    mongoDb: "mongodb://localhost:27017/test",};
  • 定義數(shù)據(jù)庫初始化類
import { Collection, Db, MongoClient } from "mongodb";import { Logger } from "tsrpc";import { BackConfig } from "./BackConfig";import { DbPost } from "./dbItems/DbPost";export class Global {  static db: Db;  static async init(logger?: Logger) {    logger?.log(`Start connecting db...`);    const client = await new MongoClient(BackConfig.mongoDb).connect();    logger?.log(`Db connected successfully...`);    this.db = client.db();  }  static collection<T extends keyof DbCollectionType>(    col: T  ): Collection<DbCollectionType[T]> {    return this.db.collection(col);  }}export interface DbCollectionType {  Post: DbPost;}
  • 改寫后端index.ts
import { Global } from "../src/shared/protocols/models/Global";async function main() {        await server.autoImplementApi(path.resolve(__dirname, 'api'));        await Global.init(server.logger);    await server.start();};

ok,截止到目前,我們把第一張表的相關(guān)配置已經(jīng)搞定了,請確保數(shù)據(jù)庫已打開且配置正確,然后我們直接運行一下服務(wù)器:

如果你運氣好(狗頭)褒颈,那么你應(yīng)該是成功開啟這個服務(wù)器柒巫,并且控制臺能看到連接成功的信息:

然后我們快速開發(fā)一下新增API,其他的更新和刪除API谷丸,希望能大家舉一反三堡掏,自行開發(fā)。

import { Post } from "./models/Post";export interface ReqAddPost {  newPost: Omit<Post, "_id" | "create" | "update" | "visitedNum">;}export interface ResAddPost {  insertedId: string;}

我們規(guī)定的請求類型是只能讓客戶端傳遞除了id淤井,create布疼,update摊趾,visitedNum的Post類型币狠。然后我們還是運行那幾個熟悉的命令:

npm run protonpm run apiimport { ApiCall } from "tsrpc";import { Global } from "../shared/protocols/models/Global";import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {  let op = await Global.collection("Post").insertOne({    ...call.req.newPost,    create: {      uid: "xxx",      time: new Date(),    },    visitedNum: 0,  });  call.succ({    insertedId: op.insertedId.toHexString(),  });}

這一part完成~

如何做到動態(tài)類型校驗

之前我們就提到過游两,前端在調(diào)用后端的api時候,會給出完整的代碼提示漩绵,從api名稱到api的請求體類型等等贱案,那么這一定程度上杜絕了開發(fā)中常見的接口聯(lián)調(diào)不細(xì)心的問題。在傳統(tǒng)的前后端開發(fā)中止吐,尤其是分離模式宝踪,有一個非常常見的問題就是動態(tài)類型校驗。每個語言/框架都有自己類型校驗的手段碍扔,比如springmvc我們可以通過注解的方式來校驗(下面展示了控制器中的校驗瘩燥,還有其他校驗手段):

@Controller@RequestMapping("valid")@Slf4jpublic class ValidateController { private static final String BASE_PATH = "/valid/"; @RequestMapping("index") public String index(@Validated() Student student,BindingResult result){         return BASE_PATH + "index";    }}

那么tsrpc是如何保證數(shù)據(jù)傳輸?shù)恼_性的呢,首先我們?nèi)绻谇岸耸褂胻srpc的瀏覽器請求包不同,我們調(diào)用api時候不僅會在開發(fā)中提示開發(fā)者這個字段是錯誤的厉膀,而且會在請求發(fā)出之前做前端方面的遏制。在后端請求到達(dá)異步函數(shù)之前二拐,也會去做第三次校驗服鹅;所以我們在后端異步函數(shù)中使用到的參數(shù)一定是類型安全,完全不需要擔(dān)心安全問題百新。

市面上有很多js領(lǐng)域解決動態(tài)校驗的方案企软;最常見應(yīng)該就是json schema,可以基于json自己實現(xiàn)一套校驗方法可以在運行時來做校驗饭望。但是仍然有很多缺點仗哨,比如不能在前端進(jìn)行運行時提示且可能重復(fù)寫很多類型定義。那么tsrpc核心中使用到了一個庫(這個庫也是同個作者開發(fā)的):

tsrpc-buffer

為了實現(xiàn)ts動態(tài)類型校驗铅辞,不可能把整個ts加進(jìn)去厌漂,因為那有足足60m多,這是不現(xiàn)實的巷挥。所以作者開發(fā)了這個庫桩卵。tsrpc依賴了這個庫,它對ts的語法進(jìn)行了兼容倍宾,目前支持了大部分的ts的寫法雏节,包括我們常用的string,number等高职,還支持一些復(fù)雜的泛形钩乍。

如果你想細(xì)細(xì)了解這方面,可以看一下文檔支持的ts類型有哪些

當(dāng)然怔锌,隨著ts的更新寥粹,這個buffer也會支持更多的ts類型变过,可以做更完善的全棧應(yīng)用。而且我們可以使用tsrpc進(jìn)行原汁原味的ts開發(fā)涝涤,市面上的第三方工具/框架需要借助另外編程語言/DSL媚狰,tsrpc-buffer完全讓你使用ts,你不會感覺到一絲違和感阔拳。

二進(jìn)制序列化

tsrpc的二進(jìn)制序列化機(jī)制是由我們上文中提到的tsrpc-buffer中實現(xiàn)的崭孤,那么這個特性帶給我們的是比json更小的傳輸體積且支持更多的數(shù)據(jù)類型,ArrayBuffer, Date等糊肠。這意味著使用tsrpc的全棧應(yīng)用在應(yīng)對上傳圖片這種業(yè)務(wù)的時候簡直就像是小兒科辨宠,我們可以用一個例子來證明。

export interface ReqUpload {    fileName: string,    fileData: Uint8Array}export interface ResUpload {    url: string;}

我們通過剛剛學(xué)到的一些命令货裹,來生成協(xié)議以及api

npm run protonpm run apiimport { ApiCall } from "tsrpc";import { ReqUpload, ResUpload } from "../shared/protocols/PtlUpload";import fs from "fs/promises";export async function ApiUpload(call: ApiCall<ReqUpload, ResUpload>) {  await fs.writeFile("uploads/" + call.req.fileName, call.req.fileData);  call.succ({    url: "http://127.0.0.1:3000/uploads/" + call.req.fileName,  });}

為了讓前端調(diào)用嗤形,同步shared下的協(xié)議

寫一個簡單的file選擇器在index.html中

<input type="file" id="fileInput">import { HttpClient } from "tsrpc-browser";import { serviceProto } from "./shared/protocols/serviceProto";let client = new HttpClient(serviceProto, {  server: "http://127.0.0.1:3000",  logger: console,});const input = document.getElementById("fileInput") as HTMLInputElement;input.addEventListener("change", async () => {  if (input.files) {    const fileData = await loadFile(input.files?.[0]);    upload(fileData, input.files?.[0].name);  }});const upload = async (fileData: Uint8Array, fileName: string) => {  const fr = new FileReader();  client.callApi("Upload", {    fileData,    fileName,  });};function loadFile(file: File): Promise<Uint8Array> {  return new Promise((rs) => {    let reader = new FileReader();    reader.onload = (e) => {      rs(new Uint8Array(e.target!.result as ArrayBuffer));    };    reader.readAsArrayBuffer(file);  });}

開發(fā)完畢,我們可以仔細(xì)看一下控制臺:

盡管我們在日常開發(fā)中會用到一些組件庫弧圆,組件庫幫助我們做了上傳的大部分工作赋兵,所以我們寫原生的上傳可能在代碼量上更多,但是省去了前后端轉(zhuǎn)換Formdata的時間墓阀。

向后兼容http(json)和WebSocket

tsrpc也向后支持json毡惜,我們可以在客戶端進(jìn)行一個簡單的配置,發(fā)送的請求就是json啦:

let client = new HttpClient(serviceProto, {  server: "http://127.0.0.1:3000",  json: true,  logger: console,});

我其實暫時沒有想到非要使用json的場景斯撮,使用二進(jìn)制序列化比json體積更小傳輸更快经伙,本地開發(fā)的日志也在控制臺隨時打印,所以我還是建議大家使用默認(rèn)的二進(jìn)制序列化的傳輸模式勿锅。

tsrpc設(shè)計之初是為了游戲帕膜,因為傳輸特性能讓websocket更高效,我們可以用tsrpc簡單做一個websocket-demo溢十,具體實現(xiàn)我參考了官網(wǎng)的實現(xiàn)垮刹,如果你想直接了解官網(wǎng)的這一part的內(nèi)容,直接移步:

websocket實時服務(wù)-tsrpc

tsrpc的實現(xiàn)和協(xié)議無關(guān)张弛,意味著咱們之前寫的代碼都可以用荒典,僅僅做一個簡單的調(diào)整替換即可。

websocket的消息是tsrpc傳輸中最小單元吞鸭,我們需要用另外一個方法去定義協(xié)議寺董,我們的websocket例子如下:

客戶端發(fā)起一個請求,服務(wù)端接收并且向所有客戶端發(fā)送一個消息

首先我們需要定義一個MsgHello.ts這樣的協(xié)議:

export interface MsgHello {  time: Date;  content: string;}

這個協(xié)議規(guī)定了前后端通訊的請求體刻剥。

我們需要改寫后端backend中的index.ts遮咖,將原先的HTTP服務(wù),改成Websocket服務(wù)

import { HttpServer, WsServer } from "tsrpc";export const server = new WsServer(serviceProto, {    port: 3000,    logMsg: true});

這里導(dǎo)出server是有用意的造虏,我們將在之后的代碼中會用到這個server御吞。

改寫frontend前端中的index.ts

import { HttpClient, WsClient } from "tsrpc-browser";let ws = new WsClient(serviceProto, {  server: "ws://127.0.0.1:3000",  logger: console,});const init = async () => {    let result = await ws.connect();};init();

我們需要一個api來觸發(fā)后端給client發(fā)送websocket消息:

export interface ReqSend {  content: string;}export interface ResSend {  time: Date;}

定義成功后麦箍,我們運行以下幾個命令:

npm run protonpm run api

運行成功,我們可以在api文件夾下的ApiSend.ts中寫入以下內(nèi)容:

import { ApiCall } from "tsrpc";import { server } from "..";import { ReqSend, ResSend } from "../shared/protocols/PtlSend";export async function ApiSend(call: ApiCall<ReqSend, ResSend>) {  const time = new Date();  call.succ({    time,  });    server.broadcastMsg("Hello", {    content: call.req.content,    time,  });}

我們的后端邏輯寫完了陶珠,我們運行以下命令挟裂,將協(xié)議同步到前端

我們進(jìn)一步改寫前端frontend/src/index.ts:

import { HttpClient, WsClient } from "tsrpc-browser";import { serviceProto } from "./shared/protocols/serviceProto";let ws = new WsClient(serviceProto, {  server: "ws://127.0.0.1:3000",  logger: console,});const init = async () => {  let result = await ws.connect();  console.log(result)  if (result.isSucc) {    // ws.callApi    ws.callApi("Send", {      content: "hello websocket",    });  }};init();

我們在頁面初始化的時候,向后端發(fā)送剛剛寫好的SendApi背率,這個時候我們既能收到api的返回话瞧,也能收到websocket的消息推送嫩与。

可以看到websocket傳輸也是二進(jìn)制的寝姿,我們在開發(fā)中,也能發(fā)現(xiàn)划滋,無論是callApi和發(fā)送websocket通知饵筑,從始至終都有類型推導(dǎo),永遠(yuǎn)不會在傳輸中出現(xiàn)類型上的錯誤处坪,這就是tsrpc的強(qiáng)大之處根资。

多平臺

tsrpc支持多個平臺,支持瀏覽器/小程序/原生ios 安卓/nodejs同窘,甚至它還支持serverless玄帕,可以使用tsrpc開發(fā)基于阿里云/騰訊云的云函數(shù);在后續(xù)我也會對tsrpc生態(tài)開發(fā)更多插件想邦,使其兼容uniapp&unicloud裤纹,讓他嚴(yán)格嚴(yán)格意義上跨多端,我相信tsrpc可以改變unicloud的開發(fā)習(xí)慣丧没,讓全棧應(yīng)用更簡單鹰椒。

結(jié)語

本篇文章所有的知識點均在官網(wǎng)&視頻教程有體現(xiàn),視頻教程在文章開始之前就有鏈接呕童,非常希望大家能夠先去看一下那個視頻漆际。tsrpc的教程還會出,下一篇關(guān)于tsrpc文章主要還是講一下如何和serverless(unicloud)融合夺饲。這篇文章正在寫大綱奸汇,相信也會在這個月之內(nèi)能和大家見到。

本文使用 文章同步助手 同步

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末往声,一起剝皮案震驚了整個濱河市擂找,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烁挟,老刑警劉巖婴洼,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撼嗓,居然都是意外死亡柬采,警方通過查閱死者的電腦和手機(jī)欢唾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門粉捻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來礁遣,“玉大人,你說我怎么就攤上這事肩刃∷罨簦” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵盈包,是天一觀的道長沸呐。 經(jīng)常有香客問我,道長呢燥,這世上最難降的妖魔是什么崭添? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮叛氨,結(jié)果婚禮上呼渣,老公的妹妹穿的比我還像新娘。我一直安慰自己寞埠,他們只是感情好屁置,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仁连,像睡著了一般蓝角。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怖糊,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天帅容,我揣著相機(jī)與錄音,去河邊找鬼伍伤。 笑死并徘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扰魂。 我是一名探鬼主播麦乞,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼劝评!你這毒婦竟也來了姐直?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒋畜,失蹤者是張志新(化名)和其女友劉穎声畏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡插龄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年愿棋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片均牢。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡糠雨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徘跪,到底是詐尸還是另有隱情甘邀,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布垮庐,位于F島的核電站松邪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏突硝。R本人自食惡果不足惜测摔,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望解恰。 院中可真熱鬧,春花似錦浙于、人聲如沸护盈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腐宋。三九已至,卻和暖如春檀轨,著一層夾襖步出監(jiān)牢的瞬間胸竞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工参萄, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留卫枝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓讹挎,卻偏偏與公主長得像校赤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子筒溃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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