構(gòu)建組件庫

開始之前

構(gòu)建一個組件庫需要考慮哪些問題

  • 代碼結(jié)構(gòu)
  • 樣式解決方案
  • 組件需求分析和編碼
  • 自建測試用例分析和編碼
  • 代碼的打包和發(fā)布
  • CI/CD籽御,文檔生成

創(chuàng)建組件庫的色彩體系

色彩兩大體系:

系統(tǒng)色板:

  • 符合Vision的各種顏色
  • 中性色板:黑白灰三色

產(chǎn)品色板:

  • 一到兩個主要色彩(品牌色/primary color)
  • 一到兩個次要顏色(secondary color)
  • 一系列功能色

組件庫樣式變量分類

  • 基礎(chǔ)色彩系統(tǒng)
    • 基本色彩
    • 功能色彩
  • 字體系統(tǒng)
    • Font Family
    • Font Size
    • Font Weight
    • Line Height
    • Header Size
    • Link
    • Body
  • 表單
  • 按鈕
  • 邊框和陰影
  • 可配置開關(guān)

如何編寫組件測試

jest——JavaScript通用測試庫

jest會自動將以下三類文件視為測試文件:

  • __tests__ 文件夾中的.js后綴文件
  • .test.js后綴的文件
  • .spec.js后綴的文件

jest斷言案例

test('test common matcher', ()=>{
  expect(2 + 2).toBe(4)
  expect(2 + 2).not.toBe(5)
})

test('test to be true or false', function () {
  expect(1).toBeTruthy()
  expect(0).toBeFalsy()
})

test('test object', function () {
  expect({name: 'llr'}).toEqual({name: 'llr'})
})

React目前推薦的測試框架——@testing-library/react

作為React組件測試框架的后起之秀偏竟,@testing-library/react已經(jīng)被create-react-app內(nèi)置在生成的項目中了

@testing-library/jest-dom同樣被內(nèi)置在CRA生成的項目中,不同于jest普通的斷言拇泛,它提供了一系列方便的DOM斷言兴使,比如:toBeEmpty, toHaveClass, toContainHTML, toContainElement...

組件單元測試疤估,測什么灾常?

  1. 測試能不能保持正常行為,比如Button組件能work as a button铃拇,可以添加onClick事件監(jiān)聽等
  2. 測試渲染的結(jié)果是不是期望的HTML元素:tagName === BUTTON 钞瀑?
  3. 測試樣式屬性——根據(jù)屬性值的不同,能不能得到相應(yīng)的className(樣式是否被正確添加)
  4. 測試特殊屬性的的作用:disable or 改變HTML類型的屬性能否達成期望

Button組件的編寫

Button類型:

  • primary
  • default
  • danger
  • link button & icon button
// 使用enum管理按鈕的類型
export enum ButtonType {
    Primary = "primary",
    Default = "default",
    Danger = "danger",
    Link = "link",
}

Button大忻:

  • normal
  • small
  • large
// 使用enum管理按鈕的大小
export enum ButtonSiz {
    Large = "lg",
    Small = "sm",
}

Button狀態(tài)

  • disable
// 不同的按鈕類型仔戈,disable的表現(xiàn)是不一樣的,button標簽自帶diabled屬性拧廊,a標簽沒有disabled屬性
    const classes = classNames('btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
        'disabled': (btnType === ButtonType.Link) && disable
    })
    if (btnType === ButtonType.Link && href) {
        return <a 
            className={classes}
            href={href}
            {...restProps}
        >{children}</a>
    } else {
        return <button 
            className={classes}
            disabled={disable}
            {...restProps}
        >{children}</button>
    }

Button組件的屬性

自定義屬性
interface BaseButtonProps {
    className?: string;
    disable?: boolean;
    href?: string;
    size?: ButtonSiz;
    btnType?: ButtonType;
    children: React.ReactNode;
}
內(nèi)置的屬性监徘,如常見的onClick方法

button標簽:React.ButtonHTMLAttributes<HTMLElement>
a標簽:React.AnchorHTMLAttributes<HTMLElement>

使用ts類型別名定義交叉類型
// 最終Button標簽的類型為ButtonProps,使用Partial interface包裹是將類型屬性都設(shè)置為可選參數(shù)
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
Partial的作用

假設(shè)我們有一個定義 user 的接口:

interface IUser {
  name: string
  age: number
}

經(jīng)過 Partial 類型轉(zhuǎn)化后得到:

type optional = Partial<IUser>

// optional的結(jié)果如下
type optional = {
    name?: string | undefined;
    age?: number | undefined;
}

Button組件的測試

測試用例設(shè)計:

  • should render the default button
  • should render the correct HTML tag with correct className when render Button given btnType is Primary, size is Large, additional class is klass
  • should render disabled button when render Button given disabled attribute true
  • should render a link when render Button given btnType is link and href is provided
  • should render a disabled link when render Button given btnType is link and href is provided and disabled attribute true

Menu組件

Menu組件語義分析(偽代碼):

<Menu defaultIndex={0} onSelect={} mode="vertical">
  <Menu.Item>
    title one
  </Menu.Item>
  <Menu.Item disabled>
    disabled menu item
  </Menu.Item>
  <Menu.Item>
    <a href="#">Link in menu</a>
  </Menu.Item>
</Menu>

Menu組件的屬性分析

使用 string-literal-types來限制組件屬性值的范圍吧碾,比enum更好用

interface MenuProps {
  defaultIndex: number;
  mode: string;
  onSelect: (selectedIndex: number) => void;
  className: String
}
interface MenuItemProps {
  index: number;
  disabled: boolean;
  className: String
}
通過屬性來生成Menu組件的className

Menu組件

const Menu: FC<MenuProps> = (props)=>{
    const {defaultIndex, className, mode, style, children, onSelect} = props
    
    const classes = classNames('tui-menu', className, {
        'menu-vertical': mode === 'vertical'
    })

    return <ul className={classes} style={style}>
        <MenuContext.Provider value={passedContext}>
            {children}
        </MenuContext.Provider>
    </ul>
}

MenuItem組件

const MenuItem: FC<MenuItemProps> = (props) => {
    const {index, disabled, className, style, children} = props

    const classes = classNames('tui-menu-item', className, {
        'is-disabled': disabled
    })

    return (
        <li className={classes} style={style}>
            {children}
        </li>
    )
}
useState記錄Menu組件activeItem狀態(tài)凰盔,通過useContext hook與子組件共享該狀態(tài)

聲明、構(gòu)造MenuContext

// 定義MenuContext類型接口
interface IMenuContext {
    index: number;
    onSelect?: SelectCallback;
}
// 聲明Context
export const MenuContext = createContext<IMenuContext>({index: 0})

const Menu: FC<MenuProps> = (props)=>{
    ...
    
    const [currentActive, setActive] = useState(defaultIndex)

    const handleClick = (index: number) => {
        setActive(index)
        if(onSelect){
            onSelect(index)
        }
    }

    // 將currentActive與onSelect方法綁定到Context中
    const passedContext: IMenuContext = {
        index: currentActive ? currentActive : 0,
        onSelect: handleClick
    }

    // 使用MenuContext.Provider包裹children
    return <ul className={classes} style={style} data-testid="test-menu">
        <MenuContext.Provider value={passedContext}>
            {children}
        </MenuContext.Provider>
    </ul>
}

MenuItem組件中獲取MenuContext

import {MenuContext} from "./menu";

const MenuItem: FC<MenuItemProps> = (props) => {
    ...
    // 獲取useContext
    const context = useContext(MenuContext)
    
    const classes = classNames('tui-menu-item', className, {
        'is-disabled': disabled,
        // 根據(jù)context中的index值判斷當前MenuItem是否為active狀態(tài)
        'is-active': context.index === index
    })
    
    // 調(diào)用context中的onSelect方法
    const handleClick = ()=>{
        if (context.onSelect && !disabled && (typeof index === "number")) {
            context.onSelect(index)
        }
    }

    return (
        <li className={classes} style={style} onClick={handleClick}>
            {children}
        </li>
    )
}
限制Menu組件的children只能為MenuItem倦春,自動為MenuItem添加index值

使用React.Children.map遍歷Menu組件下的子組件

使用React.cloneElement將數(shù)組index值注入到Menu子組件MenuItem中

const Menu: FC<MenuProps> = (props)=>{
    const {...} = props
    
    ...
    
    const renderChildren = ()=>{
        return React.Children.map(children, ((child, index) => {
            const childElement = child as React.FunctionComponentElement<MenuItemProps>
            const { displayName } = childElement.type
            if (displayName === 'MenuItem') {
                // ??:將index作為自組件的props注入
                return React.cloneElement(childElement, {index})
            }else {
            // ??:如果子組件的displayName不對户敬,報警告
                console.error("Warning: Menu has a child which is not a MenuItem component")
            }
        }))
    }

    return <ul className={classes} style={style}>
        <MenuContext.Provider value={passedContext}>
            {renderChildren()}
        </MenuContext.Provider>
    </ul>
}

Menu組件的測試

測試用例設(shè)計:

  • should render correct html tags when render Menu given default props
  • should change active menu item and call the right callback when click Menu component item
  • should render vertical mode when render Menu component given mode is vertical

Menu組件需求升級——Menu中支持下拉列表

期望得到的組件功能語以化表達:

<Menu mode="vertical" defaultIndex="0">
   <MenuItem>
        menu 1
    </MenuItem>
    <MenuItem disabled>
        menu 2
    </MenuItem>
    <SubMenu title="dropdown">
        <MenuItem>
            dropdown 1
        </MenuItem>
        <MenuItem>
            dropdown 2
        </MenuItem>
    </SubMenu>
    <MenuItem>
        menu 3
    </MenuItem>
</Menu>

Menu組件需要修改的部分:

  • 子組件可以支持SubMenu
  • MenuItem的index有多層結(jié)構(gòu)落剪,需要修改為數(shù)據(jù)類型為string,下拉菜單中的MenuItem index表現(xiàn)為類似"1-0"

封裝subMenu:

  • 根據(jù)menu的mode判斷下拉列表的toggle形式
    • 橫向menu通過鼠標的hover開關(guān)
    • 縱向的menu通過點擊事件來開關(guān)下拉菜單
export interface SubMenuProps {
    index?: string;
    title?: string;
    className?: string;
}

const SubMenu: FC<SubMenuProps> = (props) => {
    const {index, title, className, children} = props
    const [open, setOpen] = useState(false)
    const context = useContext(MenuContext)
    const classes = classNames('tui-menu-item tui-submenu-item', className, {
        'is-active': context.index === index
    })

    const handleClick = (e: React.MouseEvent)=>{
        e.preventDefault()
        setOpen(!open)
    }

    let timer: any
    const handleMouse = (e: React.MouseEvent, toggle: boolean)=>{
        clearTimeout(timer)
        e.preventDefault()
        timer = setTimeout(()=>{
            setOpen(toggle)
        }, 300)
    }

    const clickEvents = context.mode === 'vertical'? {
        onClick: handleClick
    }:{}
    const hoverEvents = context.mode !== 'vertical'? {
        onMouseEnter: (e: React.MouseEvent)=>{handleMouse(e,true)},
        onMouseLeave: (e: React.MouseEvent)=>{handleMouse(e,false)}
    }:{}

    const renderChildren = () => {
        const subMenuClasses = classNames('tui-submenu', {
            'menu-opened': open
        })
        const childrenComponent = React.Children.map(children, ((child, i) => {
            const childElement = child as React.FunctionComponentElement<MenuItemProps>
            const {displayName} = childElement.type
            if (displayName === 'MenuItem') {
                return React.cloneElement(childElement, {
                    index: `${index}-${i}`
                })
            } else {
                console.error("Warning: SubMenu has a child which is not a MenuItem component")
            }
        }))
        return <ul className={subMenuClasses}>
            {childrenComponent}
        </ul>
    }

    return (
        <li key={index} className={classes} {...hoverEvents}>
            <div className="submenu-title" {...clickEvents}>{title}</div>
            {renderChildren()}
        </li>
    )
};

組件的調(diào)試與文檔——StoryBook

安裝storyBook

npx -p @storybook/cli sb init

配置讀取ts

.storybook/main.js

module.exports = {
  stories: ['../src/**/*.stories.tsx', '../src/**/*.stories.js'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
};

配置全局樣式

config.ts

import { configure } from "@storybook/react";
import '../src/styles/index.scss'

configure(require.context('../src', true, /\.stories\.tsx$/), module);

編寫B(tài)utton組件的story

當前storybook版本是5.3尿庐,5.2以上的版本已經(jīng)推薦使用CSF語法編寫story:
button.stories.tsx

import {action} from "@storybook/addon-actions";
import React from "react";
import Button from "./button";

const styles: React.CSSProperties = {
    textAlign: "center"
}
const CenterDecorator = (storyFn: any) => <div style={styles}>{storyFn()}</div>

export default {
    title: 'Button',
    component: Button,
    decorators: [CenterDecorator],
};

export const DefaultButton = () =>
    <Button onClick={action('clicked')}>Default Button</Button>;

DefaultButton.story = {
    name: '默認按鈕',
};

export const buttonWithDifferentSize = () =>
    <>
        <Button size="lg">Large Button</Button>
        <Button>Default Button</Button>
        <Button size="sm">Small Button</Button>
    </>

export const buttonWithDifferentType = () =>
    <>
        <Button btnType="primary">Primary Button</Button>
        <Button btnType="default">Default Button</Button>
        <Button btnType="danger">Danger Button</Button>
        <Button btnType="link"  target="_blank">Link Button</Button>
    </>

StoryBook的插件

配置addon-info插件忠怖,豐富組件的文檔信息: .storybook/config.tsx

import {configure, addDecorator, addParameters} from "@storybook/react";
import '../src/styles/index.scss'
import React from "react";
import {withInfo} from "@storybook/addon-info";

const wrapperStyles: React.CSSProperties = {
    padding: '20px 40px'
}

const storyWrapper = (storyFn: any) => (
    <div style={wrapperStyles}>
        <h3>Component Demo</h3>
        {storyFn()}
    </div>
)

addDecorator(storyWrapper)
addDecorator(withInfo)
addParameters({info: {inline: true, header: false}})

configure(require.context('../src', true, /\.stories\.tsx$/), module);

配置react-docgen-typescript-loader webpack loader使react-docgen支持ts,同時配置過濾器抄瑟,過濾掉html自帶的props凡泣,只在文檔中展示自定義的props.storybook/main.js

module.exports = {
  stories: ['../src/**/*.stories.tsx'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.tsx$/,
      use: [{
          loader: require.resolve("react-docgen-typescript-loader"),
          options: {
            shouldExtractLiteralValuesFromEnum: true,
            propFilter: (prop) => {
              if (prop.parent) {
                return !prop.parent.fileName.includes('node_modules')
              }
              return true
            }
          }
        }],
    });
    config.resolve.extensions.push('.ts', '.tsx');
    return config;
  },
};

在組件實現(xiàn)代碼加上注釋,可以完善react-gendoc的描述:

import React, {AnchorHTMLAttributes, ButtonHTMLAttributes, FC} from "react";
import classNames from 'classnames'

type ButtonSiz = 'lg' | 'sm'
type ButtonType = 'primary' | 'default' | 'danger' | 'link'

interface BaseButtonProps {
    className?: string;
    /** Setting Button's disable*/
    disable?: boolean;
    href?: string;
    /** Setting Button's size*/
    size?: ButtonSiz;
    /** Setting Button's type*/
    btnType?: ButtonType;
    children: React.ReactNode;
}

type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>

/**
 *
 The most commonly used button elements on the page, suitable for completing specific interactions
 * ### Reference method
 *
 * ~~~js
 * import { Button } from 'thought-ui'
 * ~~~
 */
export const Button: FC<ButtonProps> = (props) => {
    const {
        btnType,
        className,
        disable,
        size,
        children,
        href,
        ...restProps
    } = props

    const classes = classNames('btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
        'disabled': (btnType === 'link') && disable
    })
    if (btnType === 'link' && href) {
        return <a className={classes} href={href} {...restProps}>{children}</a>
    } else {
        return <button className={classes} disabled={disable} {...restProps}>{children}</button>
    }
}

Button.defaultProps = {
    disable: false,
    btnType: 'default'
}

export default Button

組件庫打包

模塊的歷史

  • 全局變量與命名空間皮假、自執(zhí)行函數(shù):jQuery
    • 依賴全局變量
    • 污染全局變量鞋拟、不安全
    • 手動管理依賴、控制執(zhí)行順序
    • 上線之前手動合并
  • common.js規(guī)范:為服務(wù)器端誕生的惹资,不符合前端規(guī)范
    • require & module.exports
  • AMD:為前端模塊化誕生的贺纲,解決了common.js規(guī)范的問題
    • define(function (){ const bar = require('../bar') })
    • 沒辦法使用直接使用,也需要模塊打包工具require.js
  • Es6 module
    • import & export
    • 也沒辦法直接在瀏覽器中使用褪测,需要bundle為es5的代碼

模塊打包的流程

Typescript Files------tsc---->ES6 Modules Files----入口文件index.tsx----Bundler: webpack猴誊、rollup...----->瀏覽器可直接執(zhí)行的文件

選擇Javascript的模塊格式

UMD(Universal Module Definition)是一種可以直接在瀏覽器中使用的模塊格式,這種方式可以支持用戶直接使用script標簽引用模塊侮措。

ES模塊:
ES模塊是官方標準稠肘,可以進行代碼靜態(tài)分析,從而實現(xiàn)tree-shaking的優(yōu)化萝毛,并提供諸如循環(huán)引用和動態(tài)綁定等高級功能。

所以:ES模塊作為打包的輸出結(jié)果

創(chuàng)建組件庫模塊的入口文件

components/Button/index.tsx:

import Button from "./button";

export default Button;

components/Menu/index.tsx:

import {FC} from 'react'

import Menu, {MenuProps} from "./menu";
import MenuItem, {MenuItemProps} from "./menuItem";
import SubMenu, {SubMenuProps} from "./subMenu";

export type IMenuComponent = FC<MenuProps> & {
    Item: FC<MenuItemProps>,
    SubMenu: FC<SubMenuProps>,
}

const FinalMenu = Menu as IMenuComponent;

FinalMenu.Item = MenuItem;
FinalMenu.SubMenu = SubMenu;

export default FinalMenu;

組件庫模塊的入口文件src/index.tsx:

export {default as Button} from './components/Button'
export {default as Menu} from './components/Menu'

使用tsc打包ts文件為ES文件

tsconfig.build.json

{
  "compilerOptions": {
    "outDir": "build",
    "module": "ESNext",
    "target": "ES5",
    "declaration": true,
    "jsx": "react",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": [
    "src"
  ],
  "exclude": [
    "src/**/*.test.tsx",
    "src/**/*.stories.tsx"
  ]
}

package.json

"scripts": {
    ...
    "build-ts": "tsc -p tsconfig.build.json"
  },

使用node-sass打包樣式文件

package.json

"scripts": {
  "build": "npm run build-ts && npm run build-css",
  "build-ts": "tsc -p tsconfig.build.json",
  "build-css": "node-sass ./src/styles/index.scss ./build/index.css"
}

打包上傳到npm

語義化版本號

自動化publish滑黔、commit之前的自動化測試與lint

// package.json scripts
{
    "lint": "eslint --ext js,ts,tsx src --max-warning 1",
    "test:nowatch": "cross-env CI=true react-scripts test",
    "build": "npm run clean && npm run build-ts && npm run build-css",
    "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build"

}

配置husky的config:

{
  "husky": {
    "hooks": {
      "pre-commit": "npm run test:nowatch && npm run lint"
    }
  }
}

登錄npm并執(zhí)行npm run push

git addusr

npm run publish

配置circle ci自動化部署storybook到gh-pages

.circlecl/config.yml:

version: 2.1
orbs:
  node: circleci/node@1.1.6
  gh-pages: sugarshin/gh-pages@0.0.6
jobs:
  build-and-test:
    executor:
      name: node/default
    steps:
      - checkout
      - node/with-cache:
          steps:
            - run: yarn install
            - run: yarn run test:nowatch
  deploy-sb-ghpages:
    executor:
      name: node/default
    steps:
      - checkout
      - run: yarn install
      - run: yarn run build-storybook
      - gh-pages/deploy:
          build-dir: storybook-static
          ssh-fingerprints: xxx

workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build-and-test
      - deploy-sb-ghpages:
          requires:
            - build-and-test
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末略荡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汛兜,更是在濱河造成了極大的恐慌巴粪,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肛根,死亡現(xiàn)場離奇詭異漏策,居然都是意外死亡派哲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門掺喻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芭届,“玉大人褂乍,你說我怎么就攤上這事持隧√悠” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵题诵,是天一觀的道長。 經(jīng)常有香客問我赠潦,道長草冈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任哩俭,我火速辦了婚禮拳恋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隙赁。我一直安慰自己梆暖,他們只是感情好,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布厚掷。 她就那樣靜靜地躺著级解,像睡著了一般。 火紅的嫁衣襯著肌膚如雪薛闪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天豁延,我揣著相機與錄音昙篙,去河邊找鬼苔可。 笑死袋狞,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的苟鸯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼湾蔓,長吁一口氣:“原來是場噩夢啊……” “哼默责!你這毒婦竟也來了咸包?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤媒熊,失蹤者是張志新(化名)和其女友劉穎坟比,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡注竿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年魂贬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣谈。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡键科,死狀恐怖漩怎,靈堂內(nèi)的尸體忽然破棺而出嗦嗡,到底是詐尸還是另有隱情侥祭,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布谈宛,位于F島的核電站胎署,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏硝拧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一滋恬、第九天 我趴在偏房一處隱蔽的房頂上張望抱究。 院中可真熱鬧鼓寺,春花似錦、人聲如沸妈候。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幔虏。三九已至,卻和暖如春陷谱,著一層夾襖步出監(jiān)牢的瞬間瑟蜈,已是汗流浹背渣窜。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工图毕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眷唉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓蛤虐,卻偏偏與公主長得像肝陪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子氯窍,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359