Redux從入門到跳樓

參考鏈接:

目錄

應用場景

React設計理念之一為單向數據流,這從一方面方便了數據的管理。但是React本身只是view,并沒有提供完備的數據管理方案。隨著應用的不斷復雜化,如果用react構建前端應用的話,就要應對紛繁復雜的數據通信和管理,js需要維護更多的狀態(state),這些state可能包括用戶信息、緩存數據、全局設置狀態、被激活的路由、被選中的標簽、是否加載動效或者分頁器等等。

這時,Flux架構應運而生,Redux是其最優雅的實現,Redux是一個不依賴任何庫的框架,但是與react結合的最好,其中react-redux等開源組件便是把react與redux組合起來進行調用開發。

備注:

1.如果你不知道是否需要 Redux,那就是不需要它

2.只有遇到 React 實在解決不了的問題,你才需要 Redux

Redux使用場景:

  • 某個組件的狀態,需要共享
  • 某個狀態需要在任何地方都可以拿到
  • 一個組件需要改變全局狀態
  • 一個組件需要改變另一個組件的狀態

比如,論壇應用中的夜間設置、回到頂部、userInfo全局共享等場景。redux最終目的就是讓狀態(state)變化變得可預測.

使用的三原則

  • 單一數據源

整個應用的state,存儲在唯一一個object中,同時也只有一個store用于存儲這個object.

  • 狀態是只讀的

唯一能改變state的方法,就是觸發action操作。action是用來描述正在發生的事件的一個對象

  • 通過純函數修改State

純函數的問題,也是來自于函數式編程思想,我們在中學時學的函數就是純函數,對于同一個輸入,必然有相同的輸出。這就保證了數據的可控性,這里的純函數就是reducer

redux狀態管理的流程及相關概念

image
  • store

Store 就是保存數據的地方,保存著本程序所有的redux管理的數據,你可以把它看成一個容器。整個應用只能有一個 Store。(一個store是一個對象, reducer會改變store中的某些值)

Redux 提供createStore這個函數,用來生成 Store。

import { createStore } from 'redux';
const store = createStore(fn);

上面代碼中,createStore函數接受另一個函數作為參數,返回新生成的 Store 對象。這個fn就是reducer純函數,通常我們在開發中也會使用中間件,來優化架構,比如最常用的異步操作插件,redux-thunk,如果配合redux-thunk來創建store的話,代碼示例:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';

let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
let store = createStoreWithMiddleware(rootReducer);

redux-thunk的源碼及其簡單,如下:

// 判斷action是否是函數,如果是,繼續執行遞歸式的操作。所以在redux中的異步,只能出現在action中,而且還需要有中間件的支持。
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

同步action與異步action最大的區別是:

同步只返回一個普通action對象。而異步操作中途會返回一個promise函數。當然在promise函數處理完畢后也會返回一個普通action對象。thunk中間件就是判斷如果返回的是函數,則不傳導給reducer,直到檢測到是普通action對象,才交由reducer處理。


Store 有以下職責:

  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通過 subscribe(listener) 注冊監聽器;
  • 通過 subscribe(listener) 返回的函數注銷監聽器。

一般情況下,我們只需要getState()和dispatch()方法即可,即可以解決絕大部分問題。

我們可以自定義中間件

比如我們自定義一個可以打印出當前的觸發的action以及出發后的state變化的中間件,代碼改動如下:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/rootReudcer';

let logger = store => next => action => {
    if(typeof action === 'function') {
        console.log('dispatching a function')
    }else{
        console.log('dispatching', action);
    }
    
    let result = next(action);
    // getState() 可以拿到store的狀態, 也就是所有數據
    console.log('next state', store.getState());
    return result;
}

let middleWares = {
    logger, 
    thunk
}
// ... 擴展運算符
let createStoreWithMiddleware = applyMiddleware(...middleWares)(createStore);

let store = createStoreWithMiddleware(rootReducer);

補充:我們自定義的中間件,也有對應的開源插件,redux-logger,人家的更厲害。

如果,app中涉及到登錄問題的時候,可以使用redux-persist第三方插件,這個第三方插件來將store對象存儲到本地,以及從本地恢復數據到store中,比如說保存登錄信息,下次打開應用可以直接跳過登錄界面,因為我們目前的應用屬于內嵌程序,不登陸的話也進不來,所以不用它。

  • Action

Action 是一個對象,描述了觸發的動作,僅此而已。我們約定,action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作。通常它長一下這個樣子:

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

除了 type 字段外,action 對象的結構完全由你自己決定,來看一個復雜點的:

{
    type: 'SET_SCREEN_LAST_REFRESH_TIME',
    screenId,
    lastRefreshTime,
    objectId
}

通常我們會添加一個新的模塊文件來存儲這些actions types,比如我們新建一個actionTypes.js來保存:

//主頁actions
export const FETCH_HOME_LIST = 'FETCH_HOME_LIST';
export const RECEIVE_HOME_LIST = 'RECEIVE_HOME_LIST';
//分類頁actions
export const FETCH_CLASS_LIST = 'FETCH_CLASS_LIST';
export const RECEIVE_CLASS_LIST = 'RECEIVE_CLASS_LIST';
//分類詳細頁actions
export const FETCH_CLASSDITAL_LIST = 'FETCH_CLASSDITAL_LIST';
export const RECEIVE_CLASSDITAL_LIST = 'RECEIVE_CLASSDITAL_LIST';
export const RESET_CLASSDITAL_STATE = 'RESET_CLASSDITAL_STATE'; 
// 設置頁actions
export const CHANGE_SET_SWITCH = 'CHANGE_SET_SWITCH';
export const CHANGE_SET_TEXT = 'CHANGE_SET_TEXT';
// 用戶信息
export const USER_INFO = 'USER_INFO';

引用的時候,可以:

import * as types from './actionTypes';
  • Action 創建函數(Action Creator)

Action 創建函數 就是生成 action 的方法。“action” 和 “action 創建函數” 這兩個概念很容易混在一起,使用時最好注意區分。在 Redux 中的 action 創建函數只是簡單的返回一個 action。

import * as types from './actionTypes';
// 設置詳情頁內容文字主題
let changeText = (theme) => {
    return {
        type: types.CHANGE_SET_TEXT,
        theme
    }
}   

// 函數changeText就是一個簡單的action creator。

完整的action文件(setAction.js)

import * as types from './actionTypes';

let setTitle = (value) => {
    return (dispatch, getState) => {
        dispatch(changeValue(value))
    }
}

let setText = (text) => {
    return dispatch => {
        dispatch(changeText(text))
    }
}

// 修改標題顏色主題
let changeValue = (titleTheme) => {
    return {
        type: types.CHANGE_SET_SWITCH,
        titleTheme
    }
}

// 設置詳情頁內容文字顏色
let changeText = (textColor) => {
    return {
        type: types.CHANGE_SET_TEXT,
        textColor
    }
}

export {
    setText,
    setTitle
};

可以看到上述setTitle、setText函數,返回的并不是一個action對象,而是返回了一個函數,這個默認redux是沒法處理的,這就需要使用中間件處理了,redux-thunk中間件用于處理返回函數的函數,上面也介紹了redux-thunk的使用基本方式。

  • Reducer

Store 收到 Action 以后,必須給出一個新的 State,這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。
Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State。

函數簽名:

(previousState, action) => newState

Reducer必須保持絕對純凈,永遠不要在 reducer 里做這些操作:

  • 修改傳入參數;
  • 執行有副作用的操作,如 API 請求和路由跳轉;
  • 調用非純函數,如 Date.now() 或 Math.random();

完整的Reducer方法,(setReducer.js):

import * as types from '../actions/actionTypes';

const initialState = {
    titleTheme: false,
    textColor: false
}
// 這里一個技巧是使用 ES6 參數默認值語法 來精簡代碼
let setReducer = (state = initialState, action) => {

    switch(action.type){
        case types.CHANGE_SET_SWITCH:
            return Object.assign({}, state, {
                titleTheme: action.titleTheme,
            })

        case types.CHANGE_SET_TEXT:
            return Object.assign({}, state, {
                textColor: action.textColor
            })

        default:
            return state;
    }
}

export default setReducer

注意:

  • 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, {
    titleTheme: action.titleTheme,
    }),因為它會改變第一個參數的值。你必須把第一個參數設置為空對象。你也可以開啟對ES7提案對象展開運算符的支持, 從而使用 { ...state, ...newState } 達到相同的目的。
  • 在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state

關于拆分Reducer

這里只是舉例了一個簡單的功能的reducer,如果有不同的功能,需要設計很多reducer方法,注意每個 reducer 只負責管理全局 state 中它負責的一部分。每個 reducer 的 state 參數都不同,分別對應它管理的那部分 state 數據。

比如我們這個項目的reducer文件結構:

image.png

其中rootReducer.js就是一個根reducer文件,使用了Redux 的 combineReducers() 工具類來進行封裝整合。

/**
 * rootReducer.js
 * 根reducer
 */
import { combineReducers } from 'redux';
import Home from './homeReducer';
import Class from './classReducer';
import ClassDetial from './classDetialReducer';
import setReducer from './setReducer';
import userReducer from './userReducer';

export default rootReducer = combineReducers({
    Home,
    Class,
    ClassDetial,
    setReducer,
    userReducer,
})

這樣根據這個根reducer,可以生成store,請看上文store的創建過程。

redux如何與組件結合

以上部分介紹了Redux 涉及的基本概念,下面介紹與組件交互的工作流程。

梳理一下Redux的工作流程:

image

1.首先,用戶發出 Action。

store.dispatch(action);

2.Store 自動調用 Reducer,并且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。

let nextState = todoApp(previousState, action);

3.state一旦有變化,Store就會調用監聽函數,組件可以感知state的變化,更新View。

let newState = store.getState();
component.setState(newState);

具體示例1:

fsdf.gif

設置頁面有個switch按鈕,可以全局設置標題欄的主題。

代碼拆分:

1.設置按鈕所在組件:

// SetContainer.js

import React from 'react';
import {connect} from 'react-redux';
import SetPage from '../pages/SetPage';

class SetContainer extends React.Component {
    render() {
        return (
            <SetPage {...this.props} />
        )
    }
}

export default connect((state) => {
    
    const { setReducer } = state;
    return {
        setReducer
    }

})(SetContainer);

這是容器組件,將SetPage組件與redux結合起來,其中最重要的方法是connect,這個示例中是將setReducer作為屬性傳給SetPage組件,關于connect的詳解,請移步到connect()

2.SetPage組件


import React, {
    Component
} from 'react';
import {
    StyleSheet,
    Text,
    Image,
    ListView,
    TouchableOpacity,
    View,
    Switch,
    InteractionManager,
} from 'react-native';

import Common from '../common/common';
import Loading from '../common/Loading';
import HeaderView from '../common/HeaderView';

import {setText,setTitle} from '../actions/setAction';

export default class SetPage extends Component {
    constructor(props){
        super(props);
        this.state = {
            switchValue: false,
            textValue: false
        }

        this.onValueChange = this.onValueChange.bind(this);
        this.onTextChange = this.onTextChange.bind(this);
    }

    componentDidMount() {
        // console.log(this.props)
    }

    onValueChange(bool) {
        const { dispatch } = this.props;
        this.setState({
            switchValue: bool
        })
        dispatch(setTitle(bool));
    }

    onTextChange(bool) {
        const { dispatch } = this.props;

        this.setState({
            textValue: bool
        });

        dispatch(setText(bool));
    }

    render() {
        return (
            <View>
                <HeaderView
                  titleView= {'設置'}
                  />

                <View>
                    <View style={styles.itemContainer}>
                        <Text style={{fontSize: 16}}>全局設置標題主題</Text>
                        <Switch 
                            onValueChange={this.onValueChange}
                            value={this.state.switchValue}
                        />
                    </View>

                    <View style={styles.itemContainer}>
                        <Text style={{fontSize: 16}}>設置詳情頁文字主題</Text>
                        <Switch 
                            onValueChange={this.onTextChange}
                            value={this.state.textValue}
                        />
                    </View>
                </View>
            </View>
        )
    }
}

const styles = StyleSheet.create({
    itemContainer:{
        paddingLeft: 20,
        paddingRight: 20,
        height: 40,
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center'
    }
})

可以只看全局設置標題主題這個方法,設置詳情頁文字顏色和他同理。這里可以清晰的看到,用戶切換主題switch按鈕的時候,觸發的方法:

dispatch(setTitle(bool));

3.我們查看一下setTitle這個action的源碼:

// setAction.js
import * as types from './actionTypes';

let setTitle = (value) => {
    return (dispatch, getState) => {
        dispatch(changeValue(value))
    }
}

let setText = (text) => {
    return dispatch => {
        dispatch(changeText(text))
    }
}

// 修改標題主題
let changeValue = (titleTheme) => {
    return {
        type: types.CHANGE_SET_SWITCH,
        // 這里將titleTheme狀態返回
        titleTheme
    }
}

// 設置詳情頁內容文字主題
let changeText = (textColor) => {
    return {
        type: types.CHANGE_SET_TEXT,
        textColor
    }
}

export {
    setText,
    setTitle
};

4.action只是負責發送事件,并不會返回一個新的state供頁面組件調用,它是在reducer中返回的:

// setReducer.js

import * as types from '../actions/actionTypes';

const initialState = {
    titleTheme: false,
    textColor: false
}

let setReducer = (state = initialState, action) => {

    switch(action.type){
        case types.CHANGE_SET_SWITCH:
            return Object.assign({}, state, {
                titleTheme: action.titleTheme,
            })

        case types.CHANGE_SET_TEXT:
            return Object.assign({}, state, {
                textColor: action.textColor
            })

        default:
            return state;
    }
}

export default setReducer

最簡單的reducer,就是根據初始值和action對象,返回一個新的state,提供給store,這樣,頁面里可以從store中獲取到這些全局的state,用于更新組件。

我們只是寫了怎樣發送action和接收action發出newState的,下面來看這個標題組件是怎樣和redux結合的。

5.HeaderView組件

/**
 * Created by ljunb on 16/5/8.
 * 導航欄標題
 */
import React from 'react';
import {
    StyleSheet,
    View,
    Text,
    Image,
    TouchableOpacity,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Common from '../common/common';
import {connect} from 'react-redux';

class HeaderView extends React.Component {

    constructor(props){
        super(props);

        this.state = {

        }
    }

    render() {
        // 這里,在這里
        const { titleTheme } = this.props.setReducer;
        let NavigationBar = [];

        // 左邊圖片按鈕
        if (this.props.leftIcon != undefined) {
            NavigationBar.push(
                <TouchableOpacity
                    key={'leftIcon'}
                    activeOpacity={0.75}
                    style={styles.leftIcon}
                    onPress={this.props.leftIconAction}
                    >
                    <Icon color="black" size={30} name={this.props.leftIcon}/>
                </TouchableOpacity>
            )
        }

        // 標題
        if (this.props.title != undefined) {
            NavigationBar.push(
                <Text key={'title'} style={styles.title}>{this.props.title}</Text>
            )
        }

        // 自定義標題View
        if (this.props.titleView != undefined) {
            let Component = this.props.titleView;

            NavigationBar.push(
                <Text key={'titleView'} style={[styles.titleView, {color: titleTheme ? '#FFF' : '#000'}]}>{this.props.titleView}</Text>
            )
        }


        return (
            <View style={[styles.navigationBarContainer, {backgroundColor: titleTheme ? 'blue' : '#fff'}]}>
                {NavigationBar}
            </View>
        )
    }
}

const styles = StyleSheet.create({

    navigationBarContainer: {
        marginTop: 20,
        flexDirection: 'row',
        height: 44,
        justifyContent: 'center',
        alignItems: 'center',
        borderBottomColor: '#ccc',
        borderBottomWidth: 0.5,
        backgroundColor: 'white'
    },

    title: {
        fontSize: 15,
        marginLeft: 15,
    },
    titleView: {
        fontSize: 15,
    },
    leftIcon: {
       left: -Common.window.width/2+40,
    },
})


export default connect((state) => {
    
    const { setReducer } = state;
    return {
        setReducer
    }

})(HeaderView);

這個組件同樣利用connect方法綁定了redux,變成了容器組件(container component)。

connect真的很關鍵,請詳細查看官方文檔,上面有鏈接。

其他不相關的內容忽略,核心代碼是:

// 拿到全局的state 當有變化的時候,會馬上修改
const { titleTheme } = this.props.setReducer;

具體示例2:

image.png

利用redux來請求數據、下拉刷新、上拉加載更多。

1.首先,封裝action。

import * as types from './actionTypes';
import Util from '../common/utils'; 
// action創建函數,此處是渲染首頁的各種圖片
export let home = (tag, offest, limit, isLoadMore, isRefreshing, isLoading) => {
    let URL = 'http://api.huaban.com/fm/wallpaper/pins?limit=';
    if (limit) URL += limit;
    offest ? URL += '&max=' + offest : URL += '&max=';
    tag ? URL += '&tag=' + encode_utf8(tag) : URL += '&tag='
    
    return dispatch => {
        // 分發事件  不修改狀態   action是 store 數據的唯一來源。
        dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));
        return Util.get(URL, (response) => {
            // 請求數據成功后
            dispatch(receiveHomeList(response.pins))
        }, (error) => {
            // 請求失敗
            dispatch(receiveHomeList([]));
        });

    }

}

function encode_utf8(s) {
    return encodeURIComponent(s);
}

// 我們約定,action 內必須使用一個字符串類型的 type 字段來表示將要執行的動作。
let feachHomeList = (isLoadMore, isRefreshing, isLoading) => {
    return {
        type: types.FETCH_HOME_LIST,
        isLoadMore: isLoadMore,
        isRefreshing: isRefreshing,
        isLoading: isLoading,
    }
}

let receiveHomeList = (homeList) => {
    return {
        type: types.RECEIVE_HOME_LIST,
        homeList: homeList,
    }
}
  • feachHomeList表示正在請求數據的動作;
  • receiveHomeList表示請求數據完后的動作;
  • dispatch(feachHomeList(isLoadMore, isRefreshing, isLoading));表示分發請求數據的動作;

2.封裝reducer函數

import * as types from '../actions/actionTypes';
// 設置初始狀態
const initialState = {
    HomeList: [],
    isLoading: true,
    isLoadMore: false,
    isRefreshing: false,
};

let homeReducer = (state = initialState, action) => {
    
    switch (action.type) {
        case types.FETCH_HOME_LIST:
            return Object.assign({}, state, {
                isLoadMore: action.isLoadMore,
                isRefreshing: action.isRefreshing,
                isLoading: action.isLoading
            })
            
        case types.RECEIVE_HOME_LIST:
            // 如果請求成功后,返回狀態給組件更新數據
            return Object.assign({}, state, {
            // 如果是正在加載更多,那么合并數據
                HomeList: state.isLoadMore ? state.HomeList.concat(action.homeList) : action.homeList,
                isRefreshing: false,
                isLoading: false,
            })

        case types.RESET_STATE: // 清除數據
            return Object.assign({},state,{
                HomeList:[],
                isLoading:true,
            })
        default:
            return state;
    }
}

export default homeReducer;
  • 這里并沒有處理沒有更多數據的情況。

3.容器組件

import React from 'react';
import {connect} from 'react-redux';
import Home from '../pages/Home';

class HomeContainer extends React.Component {
    render() {
        return (
            <Home {...this.props} />
        )
    }
}

export default connect((state) => {
    const { Home } = state;
    return {
        Home
    }
})(HomeContainer);
  • 這里主要是利用connect函數將Home state綁定到Home組件中,并作為它的props;

4.UI組件

  • 組件掛載請求數據
...
let limit = 21;
let offest = '';
let tag = '';
let isLoadMore = false;
let isRefreshing = false;
let isLoading = true;
...
componentDidMount() {
    InteractionManager.runAfterInteractions(() => {
      const {dispatch} = this.props;
      // 觸發action 請求數據
      dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
    })
}
...
  • 下拉刷新
// 下拉刷新
  _onRefresh() {
    if (isLoadMore) {
      const {dispatch, Home} = this.props;
      isLoadMore = false;
      isRefreshing = true;
      dispatch(home('', '', limit, isLoadMore, isRefreshing, isLoading));
    }
  }
  • 上拉加載更多
// 上拉加載
  _onEndReach() {

    InteractionManager.runAfterInteractions(() => {
      const {dispatch, Home} = this.props;
      let homeList = Home.HomeList;
      isLoadMore = true;
      isLoading = false;
      isRefreshing = false;
      offest = homeList[homeList.length - 1].seq
      dispatch(home(tag, offest, limit, isLoadMore, isRefreshing, isLoading));
    })

  }
  • render方法
render() {
    // 這里可以拿到Home狀態
    const { Home,rowDate } = this.props;
     tag = rowDate;
    
    let homeList = Home.HomeList;
    let titleName = '最新';
    return (
      <View>
        <HeaderView
          titleView= {titleName}
          leftIcon={tag ? 'angle-left' : null}
          />
        {Home.isLoading ? <Loading /> :
          <ListView
            dataSource={this.state.dataSource.cloneWithRows(homeList) }
            renderRow={this._renderRow}
            contentContainerStyle={styles.list}
            enableEmptySections={true}
            initialListSize= {10}
            onScroll={this._onScroll}
            onEndReached={this._onEndReach.bind(this) }
            onEndReachedThreshold={10}
            renderFooter={this._renderFooter.bind(this) }
            style={styles.listView}
            refreshControl={
              <RefreshControl
                refreshing={Home.isRefreshing}
                onRefresh={this._onRefresh.bind(this) }
                title="正在加載中……"
                color="#ccc"
                />
            }
            />
        }
      </View>

    );

  }

至此,一個簡單的Reducer程序完成了,我們稍微總結一下:

  • 整個應用只有一個store,用來保存所有的狀態,視圖不需要自己維護狀態。
  • 視圖通過connect函數綁定到store,當store狀態變化后,store會通知視圖刷新。
  • 觸發一個action之后,會經過可能N個reducers處理,最后根reducer會將所有reducers處理之后的狀態合并,然后交給store,store再通知視圖刷新。

本文的源碼地址: 案例Demo

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

推薦閱讀更多精彩內容