【超詳細(xì)】Zod 入門教程

Zod 是一個(gè)以 TypeScript 為首的模式聲明和驗(yàn)證庫 ,彌補(bǔ)了 TypeScript 無法在運(yùn)行時(shí)進(jìn)行校驗(yàn)的問題

Zod 既可以用在服務(wù)端也可以運(yùn)行在客戶端,以保障 Web Apps 的類型安全

接下來會用十個(gè)有趣的例子,帶你快速入門 Zod,體會 Zod 的強(qiáng)大和便利 ~ 感謝 Matt Pocock 大神提供的 示例

本文倉庫地址 傳送門 ??

提示:本文 Star Wars API 有時(shí)會有超時(shí)情況,如遇超時(shí)則重試幾遍哈

01 - 使用 Zod 進(jìn)行運(yùn)行時(shí)類型校驗(yàn)

問題

TypeScript 是一個(gè)非常有用的類型工具,用于檢查代碼中變量的類型

但是我們不能總是保證代碼中變量的類型,比如這些變量來自 API 接口或者表單輸入

Zod 庫使得我們能夠在 運(yùn)行時(shí) 檢查變量的類型,它對于我們的大部分項(xiàng)目都是有用的

初探運(yùn)行時(shí)檢查

看看這個(gè) toString 函數(shù):

export const toString = (num: unknow) => {
    return String(num)
}

我們將 num 的入?yún)⒃O(shè)置為 unknow

這意味著我們可以在編碼過程中給 toString 函數(shù)傳遞任何類型的參數(shù),包括 object 類型或者 undefined :

toString('blah')
toString(undefined)
toString({name: 'Matt'})

到目前為止還是沒有報(bào)錯(cuò)的,但我們想要在 運(yùn)行時(shí) 防止這樣的事情發(fā)生

如果我們給 toString 傳入一個(gè)字符串,我們想要拋出一個(gè)錯(cuò)誤,并提示預(yù)期傳入一個(gè)數(shù)字但是接收到一個(gè)字符串

it("當(dāng)入?yún)⒉皇菙?shù)字的時(shí)候,需要拋出一個(gè)錯(cuò)誤", () => {
  expect(() => toString("123")).toThrowError(
    "Expected number, received string",
  );
});

如果我們傳入一個(gè)數(shù)字,toString 是能夠正常運(yùn)行的

it("當(dāng)入?yún)⑹菙?shù)字的時(shí)候,需要返回一個(gè)字符串", () => {
    expect(toString(1)).toBeTypeOf("string");
});

解決方案

創(chuàng)建一個(gè) numberParser

各種 Parser 是 Zod 最基礎(chǔ)的功能之一

我們通過 z.number() 來創(chuàng)建一個(gè) numberParser

它創(chuàng)建了 z.ZodNumber 對象,這個(gè)對象提供了一些有用的方法

const numberParser = z.number();

如果數(shù)據(jù)不是數(shù)字類型的話,那么將這些數(shù)據(jù)傳進(jìn) numberParser.parse() 后會報(bào)錯(cuò)

這就意味著,所有傳進(jìn) numberParser.parse() 的變量都會被轉(zhuǎn)成數(shù)字,然后我們的測試才能夠通過。

添加 numberParser , 更新 toString 方法


const numberParser = z.number();

export const toString = (num: unknown) => {
  const parsed = numberParser.parse(num);
  return String(parsed);
};

嘗試不同的類型

Zod 也允許其他的類型檢驗(yàn)

比如,如果我們要接收的參數(shù)不是數(shù)字而是一個(gè) boolean 值,那么我們可以把 numberParser 修改成 z.boolean()

當(dāng)然,如果我們只修改了這個(gè),那么我們原有的測試用例就會報(bào)錯(cuò)哦

Zod 的這種技術(shù)為我們提供了堅(jiān)實(shí)的基礎(chǔ)。 隨著我們的深入使用,你會發(fā)現(xiàn) Zod 模仿了很多你在 TypeScript 中習(xí)慣的東西。

可以在 這里 查看 Zod 完整的基礎(chǔ)類型

02 - 使用 Object Schema 對未知的 API 進(jìn)行校驗(yàn)

問題

Zod 經(jīng)常被用于校驗(yàn)未知的 API 返回內(nèi)容

在下面這個(gè)例子中,我們從 Star Wars API 中獲取一個(gè)人物的信息

export const fetchStarWarsPersonName = async (id: string) => {
  const data = await fetch("<https://swapi.dev/api/people/>" + id).then((res) =>
    res.json(),
  );

  const parsedData = PersonResult.parse(data);

  return parsedData.name;
};

注意到現(xiàn)在 PersonResult.parser() 處理的數(shù)據(jù)是從 fetch 請求來的

PersonResult 變量是由 z.unknown() 創(chuàng)建的,這告訴我們數(shù)據(jù)是被認(rèn)為是 unknown 類型因?yàn)槲覀儾恢肋@些數(shù)據(jù)里面包含有什么

const PersonResult = z.unknown();

運(yùn)行測試

如果我們是用 console.log(data) 打印出 fetch 函數(shù)的返回值,我們可以看到這個(gè) API 返回的內(nèi)容有很多,不僅僅有人物的 name ,還有其他的比如 eye_color,skin_color 等等一些我們不感興趣的內(nèi)容

接下來我們需要修復(fù)這個(gè) PersonResult 的 unknown 類型

解決方案

使用 z.object 來修改 PersonResult

首先,我們需要將 PersonResult 修改為 z.object

它允許我們使用 key 和類型來定義這些 object

在這個(gè)例子中,我們需要定義 name 成為字符串

const PersonResult = z.object({
  name: z.string(),
});

注意到這里有點(diǎn)像我們在 TypeScript 中創(chuàng)建 interface

interface PersonResult {
    name: string;
}

檢查我們的工作

fetchStarWarsPersonName 中,我們的 parsedData 現(xiàn)在已經(jīng)被賦予了正確的類型,并且擁有了一個(gè) Zod 能識別的結(jié)構(gòu)

重新調(diào)用 API 我們依然能夠看到返回的數(shù)據(jù)里面有很多我們不感興趣的信息

現(xiàn)在如果我們用 console.log 打印 parsedData,我們可以看到 Zod 已經(jīng)幫我們過濾掉我們不感興趣的 Key 了,只給我們 name 字段

更多

任何額外加入 PersonResult 的 key 都會被添加到 parsedData

能夠顯式的指明數(shù)據(jù)中每個(gè) key 的類型是 Zod 中一個(gè)非常有用的功能

03 - 創(chuàng)建自定義類型數(shù)組

問題

在這個(gè)例子中,我們依然使用 Star Wars API,但是這一次我們要拿到 所有 人物的數(shù)據(jù)

一開始的部分跟我們之前看到的非常類似,StarWarsPeopleResults 變量會被設(shè)置為 z.unknown()

const StarWarsPeopleResults = z.unknown();

export const fetchStarWarsPeople = async () => {
  const data = await fetch("https://swapi.dev/api/people/").then((res) =>
    res.json(),
  );

  const parsedData = StarWarsPeopleResults.parse(data);

  return parsedData.results;
};

跟之前類似,添加 console.log(data) 到 fetch 函數(shù)中,我們可以看到數(shù)組中有很多數(shù)據(jù)即使我們只對數(shù)組中的 name 字段感興趣

如果這是一個(gè) TypeScript 的 interface,它可能是需要寫成這樣

interface Results {
  results: {
    name: string;
  }[];
}

作業(yè)

通過使用 object schema 更新 StarWarsPeopleResults ,來表示一個(gè) StarWarsPerson 對象的數(shù)組

可以參考這里的文檔來獲得幫助

解決方案

正確的解法就是創(chuàng)建一個(gè)對象來飲用其他的對象。在這個(gè)例子中,StarWarsPeopleResults 將是一個(gè)包含 results 屬性的 z.object

關(guān)于 results,我們使用 z.array 并提供 StarWarsPerson 作為參數(shù)。我們也不用重復(fù)寫 name: z.string() 這部分了

這個(gè)是之前的代碼

const StarWarsPeopleResults = z.unknown()

修改之后

const StarWarsPeopleResults = z.object({
  results: z.array(StarWarsPerson),
});

如果我們 console.log 這個(gè) parsedData ,我們可以獲得期望的數(shù)據(jù)

像上面這樣聲明數(shù)組的 object 是 z.array() 最常用的的功能一直,特別是當(dāng)這個(gè) object 已經(jīng)創(chuàng)建好了。

04 - 提取對象類型

問題

現(xiàn)在我們使用 console 函數(shù)將 StarWarsPeopleResults 打印到控制臺

const logStarWarsPeopleResults = (data: unknown) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};

再一次,data 的類型是 unknown

為了修復(fù),可能會嘗試使用下面這樣的做法:

const logStarWarsPeopleResults = (data: typeof StarWarsPeopleResults)

然而這樣還是會有問題,因?yàn)檫@個(gè)類型代表的是 Zod 對象的類型而不是 StarWarsPeopleResults 類型

作業(yè)

更新 logStarWarsPeopleResults 函數(shù)去提取對象類型

解決方案

更新這個(gè)打印函數(shù)

使用 z.infer 并且傳遞 typeof StarWarsPeopleResults 來修復(fù)問題

const logStarWarsPeopleResults = (
  data: z.infer<typeof StarWarsPeopleResults>,
) => {
  ...

現(xiàn)在當(dāng)我們在 VSCode 中把鼠標(biāo) hover 到這個(gè)變量上,我們可以看到它的類型是一個(gè)包含了 results 的 object

當(dāng)我們更新了 StarWarsPerson 這個(gè) schema,函數(shù)的 data 也會同步更新

這是一個(gè)很棒的方式,它做到使用 Zod 在運(yùn)行時(shí)進(jìn)行類型檢查,同時(shí)也可以在構(gòu)建時(shí)獲取數(shù)據(jù)的類型

一個(gè)替代方案

當(dāng)然,我們也可以把 StarWarsPeopleResultsType 保存為一個(gè)類型并將它從文件中導(dǎo)出

export type StarWarsPeopleResultsType = z.infer<typeof StarWarsPeopleResults>;

logStarWarsPeopleResults 函數(shù)則會被更新成這樣

const logStarWarsPeopleResults = (data: StarWarsPeopleResultsType) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};

這樣別的文件也可以獲取到 StarWarsPeopleResults 類型,如果需要的話

05 - 讓 schema 變成可選的

問題

Zod 在前端項(xiàng)目中也同樣是有用的

在這個(gè)例子中,我們有一個(gè)函數(shù)叫做 validateFormInput

這里 values 的類型是 unknown,這樣做是安全的因?yàn)槲覀儾皇翘貏e了解這個(gè) form 表單的字段。在這個(gè)例子中,我們收集了 namephoneNumber 作為 Form 對象的 schema

const Form = z.object({
  name: z.string(),
  phoneNumber: z.string(),
});

export const validateFormInput = (values: unknown) => {
  const parsedData = Form.parse(values);

  return parsedData;
};

目前的狀況來說,我們的測試會報(bào)錯(cuò)如果 phoneNumber 字段沒有被提交

作業(yè)

因?yàn)?phoneNumber 不總是必要的,需要想一個(gè)方案,不管 phoneNumber 是否有提交,我們的測試用例都可以通過

解決方案

在這種情況下,解決方案非常直觀!
phoneNumber schema 后面添加 .optional(),我們的測試將會通過

const Form = z.object({ name: z.string(), phoneNumber: z.string().optional(), });

我們說的是, name 字段是一個(gè)必填的字符串,phoneNumber 可能是一個(gè)字符串或者 undefined

我們不需要再做更多什么額外的事情,讓這個(gè) schema 變成可選的就是一個(gè)非常不錯(cuò)的方案

06 - 在 Zod 中設(shè)置默認(rèn)值

問題

我們的下一個(gè)例子跟之前的很像:一個(gè)支持可選值的 form 表單輸入校驗(yàn)器

這一次,Form 有一個(gè) repoName 字段和一個(gè)可選數(shù)組字段 keywords

const Form = z.object({
  repoName: z.string(),
  keywords: z.array(z.string()).optional(),
});

為了使實(shí)際表單更容易,我們希望對其進(jìn)行設(shè)置,以便不必傳入字符串?dāng)?shù)組。

作業(yè)

修改 Form 使得當(dāng) keywords 字段為空的時(shí)候,會有一個(gè)默認(rèn)值(空數(shù)組)

解決方案

Zod 的 default schema 函數(shù),允許當(dāng)某個(gè)字段沒有傳參時(shí)提供一個(gè)默認(rèn)值

在這個(gè)例子中,我們將會使用 .default([]) 設(shè)置一個(gè)空數(shù)組

修改前

keywords: z.array(z.string()).optional()

修改后

keywords: z.array(z.string()).default([])

因?yàn)槲覀兲砑恿四J(rèn)值,所以我們不需要再使用 optional() ,optional 已經(jīng)是被包含在其中了。

修改之后,我們的測試可以通過了

輸入不同于輸出

在 Zod 中,我們已經(jīng)做到了輸入與輸出不同的地步。

也就是說,我們可以做到基于輸入生成類型也可以基于輸出生成類型

比如,我們創(chuàng)建 FormInputFormOutput 類型

type FormInput = z.infer<typeof Form>
type FormOutput = z.infer<typeof Form>

介紹 z.input

就像上面寫的,輸入不完全正確,因?yàn)楫?dāng)我們在給 validateFormInput 傳參數(shù)時(shí),我們沒有必要一定要傳遞 keywords 字段

我們可以使用 z.input 來替代 z.infer 來修改我們的 FormInput

如果驗(yàn)證函數(shù)的輸入和輸出之間存在差異,則為我們提供了另外一種生成的類型的方法。

type FormInput = z.input<typeof Form>

07 - 明確允許的類型

問題

在這個(gè)例子中,我們將再一次校驗(yàn)表單

這一次,F(xiàn)orm 表單有一個(gè) privacyLevel 字段,這個(gè)字段只允許 private 或者 public 這兩個(gè)類型

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.string(),
});

如果是在 TypeScript 中,我們會這么寫

type PrivacyLevel = 'private' | 'public'

當(dāng)然,我們可以在這里使用 boolean 類型,但如果將來我們還需要往 PrivacyLevel 中添加新的類型,那就不太合適了。在這里,使用聯(lián)合類型或者枚舉類型是更加安全的做法。

作業(yè)

第一個(gè)測試報(bào)錯(cuò)了,因?yàn)槲覀兊?validateFormInput 函數(shù)有除了 "private" 或 "public" 以外的其他值傳入 PrivacyLevel 字段

it("如果傳入一個(gè)非法的 privacyLevel 值,則需要報(bào)錯(cuò)", async () => {
  expect(() =>
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "something-not-allowed",
    }),
  ).toThrowError();
});

你的任務(wù)是要找到一個(gè) Zod 的 API 來允許我們明確入?yún)⒌淖址愋停源藖碜寽y試能夠順利通過。

解決方案

聯(lián)合 (Unions) & 字面量 (Literals)

第一個(gè)解決方案,我們將使用 Zod 的 聯(lián)合函數(shù),再傳一個(gè)包含 "private" 和 "public" 字面量 的數(shù)組

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.union([z.literal("private"), z.literal("public")]),
});

字面量可以用來表示:數(shù)字,字符串,布爾類型;不能用來表示對象類型

我們能使用 z.infer 檢查我們 Form 的類型

type FormType = z.infer<typeof Form>

在 VS Code 中如果你把鼠標(biāo)移到 FormType 上,我們可以看到 privacyLevel 有兩個(gè)可選值:"private" 和 "public"

可認(rèn)為是更加簡潔的方案:枚舉

通過 z.enum 使用 Zod 枚舉,也可以做到相同的事情,如下:

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.enum(["private", "public"]),
});

我們可以通過語法糖的方式來解析字面量,而不是使用 z.literal

這個(gè)方式不會產(chǎn)生 TypeScript 中的枚舉類型,比如

enum PrivacyLevcel {
    private,
    public
}

一個(gè)新的聯(lián)合類型會被創(chuàng)建

同樣,我們通過把鼠標(biāo)移到類型上面,我們可以看到一個(gè)新的包含 "private" 和 "public" 的聯(lián)合類型

08 - 復(fù)雜的 schema 校驗(yàn)

問題

到目前為止,我們的表單校驗(yàn)器函數(shù)已經(jīng)可以檢查各種值了

表單擁有 name,email 字段還有可選的 phoneNumber 和 website 字段

然而,我們現(xiàn)在想對一些值做強(qiáng)約束

需要限制用戶不能輸入不合法的 URL 以及電話號碼

作業(yè)

你的任務(wù)是尋找 Zod 的 API 來為表單類型做校驗(yàn)

電話號碼需要是合適的字符,郵箱地址和 URL 也需要正確的格式

解決方案

Zod 文檔的字符串章節(jié)包含了一些校驗(yàn)的例子,這些可以幫助我們順利通過測試

現(xiàn)在我們的 Form 表單 schema 會是寫成這樣

const Form = z.object({
  name: z.string().min(1),
  phoneNumber: z.string().min(5).max(20).optional(),
  email: z.string().email(),
  website: z.string().url().optional(),
});

name 字段加上了 min(1),因?yàn)槲覀儾荒芙o它傳空字符串

phoneNumber 限制了字符串長度是 5 至 20,同時(shí)它是可選的

Zod 有內(nèi)建的郵箱和 url 校驗(yàn)器,我們可以不需要自己手動編寫這些規(guī)則

可以注意到,我們不能這樣寫 .optional().min(), 因?yàn)閛ptional 類型沒有 min 屬性。這意味著我們需要將 .optional() 寫在每個(gè)校驗(yàn)器后面

還有很多其他的校驗(yàn)器規(guī)則,我們可以在 Zod 文檔中找到

09 - 通過組合 schema 來減少重復(fù)

問題

現(xiàn)在,我們來做一些不一樣的事情

在這個(gè)例子中,我們需要尋找方案來重構(gòu)項(xiàng)目,以減少重復(fù)代碼

這里我們有這些 schema,包括:User, PostComment

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});

我們看到, id 在每個(gè) schema 都出現(xiàn)了

Zod 提供了許多方案可以將 object 對象組織到不同的類型中,使得我們可以讓我們的代碼更加符合 DRY 原則

作業(yè)

你的挑戰(zhàn)是,需要使用 Zod 進(jìn)行代碼重構(gòu),來減少 id 的重復(fù)編寫

關(guān)于測試用例語法

你不用擔(dān)心這個(gè)測試用例的 TypeScript 語法,這里有個(gè)快速的解釋:

Expect<
  Equal<z.infer<typeof Comment>, { id: string; text: string }>
>

在上面的代碼中,Equal 是確認(rèn) z.infer<typeof Comment>{id: string; text: string} 是相同的類型

如果你刪除掉 Commentid 字段,那么在 VS Code 中可以看到 Expect 會有一個(gè)報(bào)錯(cuò),因?yàn)檫@個(gè)比較不成立了

解決方案

我們有很多方法可以重構(gòu)這段代碼

作為參考,這是我們開始的內(nèi)容:

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});

簡單的方案

最簡單的方案是抽取 id 字段保存成一個(gè)單獨(dú)的類型,然后每一個(gè) z.object 都可以引用它

const Id = z.string().uuid();

const User = z.object({
  id: Id,
  name: z.string(),
});

const Post = z.object({
  id: Id,
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: Id,
  text: z.string(),
});

這個(gè)方案挺不錯(cuò),但是 id: ID 這段仍然是一直在重復(fù)。所有的測試都可以通過,所以也還行

使用擴(kuò)展(Extend)方法

另一個(gè)方案是創(chuàng)建一個(gè)叫做 ObjectWithId 的基礎(chǔ)對象,這個(gè)對象包含 id 字段

const ObjectWithId = z.object({
  id: z.string().uuid(),
});

我們可以使用擴(kuò)展方法去創(chuàng)建一個(gè)新的 schema 來添加基礎(chǔ)對象

const ObjectWithId = z.object({
  id: z.string().uuid(),
});

const User = ObjectWithId.extend({
  name: z.string(),
});

const Post = ObjectWithId.extend({
  title: z.string(),
  body: z.string(),
});

const Comment = ObjectWithId.extend({
  text: z.string(),
});

請注意,.extend() 會覆蓋字段

使用合并(Merge)方法

跟上面的方案類似,我們可以使用合并方法來擴(kuò)展基礎(chǔ)對象 ObjectWithId :

const User = ObjectWithId.merge(
  z.object({
    name: z.string(),
  }),
);

使用 .merge() 會比 .extend() 更加冗長。我們必須傳一個(gè)包含 z.string()z.object() 對象

合并通常是用于聯(lián)合兩個(gè)不同的類型,而不是僅僅用來擴(kuò)展單個(gè)類型

這些是在 Zod 中將對象組合在一起的幾種不同方式,以減少代碼重復(fù)量,使代碼更加符合 DRY,并使項(xiàng)目更易于維護(hù)!

10 - 通過 schema 轉(zhuǎn)換數(shù)據(jù)

問題

Zod 的另一個(gè)十分有用的功能是控制從 API 接口響應(yīng)的數(shù)據(jù)

現(xiàn)在我們翻回去看看 Star Wars 的例子

想起我們創(chuàng)建了 StarWarsPeopleResults , 其中 results 字段是一個(gè)包含 StarWarsPerson schema 的數(shù)組

當(dāng)我們從 API 獲取 StarWarsPersonname,我們獲取的是他們的全稱

現(xiàn)在我們要做的是為 StarWarsPerson 添加轉(zhuǎn)換

作業(yè)

你的任務(wù)是為這個(gè)基礎(chǔ)的 StarWarsPerson 對象添加一個(gè)轉(zhuǎn)換,將 name 字段按照空格分割成數(shù)組,并將數(shù)組保存到 nameAsArray 字段中

測試用例大概是這樣的:

it("需要解析 name 和 nameAsArray 字段", async () => {
  expect((await fetchStarWarsPeople())[0]).toEqual({
    name: "Luke Skywalker",
    nameAsArray: ["Luke", "Skywalker"],
  });
});

解決方案

提醒一下,這是 StarWarsPerson 在轉(zhuǎn)換前的樣子:

const StarWarsPerson = z.object({
  name: z.string()
});

添加一個(gè)轉(zhuǎn)換 (Transformation)

當(dāng)我們在 .object() 中的 name 字段時(shí),我們可以獲取 person 參數(shù),然后轉(zhuǎn)換它并添加到一個(gè)新的屬性中

const StarWarsPerson = z
  .object({
    name: z.string(),
  })
  .transform((person) => ({
    ...person,
    nameAsArray: person.name.split(" "),
  }));

.transform() 內(nèi)部,person 是上面包含 name 的對象。

這也是我們添加滿足測試的 nameAsArray 屬性的地方。

所有這些都發(fā)生在 StarWarsPerson 這個(gè)作用域中,而不是在 fetch 函數(shù)內(nèi)部或其他地方。

另一個(gè)例子

Zod 的轉(zhuǎn)換 API 適用于它的任何原始類型。

比如,我們可以轉(zhuǎn)換 namez.object 的內(nèi)部

const StarWarsPerson = z
  .object({
    name: z.string().transform((name) => `Awesome ${name}`)
  }),
  ...

現(xiàn)在我們擁有一個(gè) name 字段包含 Awesome Luke Skywalker 和一個(gè) nameAsArray 字段包含 ['Awesome', 'Luke', 'Skywalker']

轉(zhuǎn)換過程在最底層起作用,可以組合,并且非常有用

總結(jié)

以上就是教程的所有內(nèi)容,后續(xù)還會一直補(bǔ)充更多的實(shí)用例子,建議收藏 ~ 也歡迎各位小伙伴看完之后能跟我一起討論有關(guān)于 Zod 的相關(guān)問題,提出寶貴意見 ~

引用文獻(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評論 2 370

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