基于react技術棧的單頁應用(SPA)搭建_快速入門實踐

概述

本篇文章使用create-react-app作為腳手架,結合react技術棧(react + redux + react-router),構建一個簡單的單頁面應用demo。文章會一步步地講解如何構建這么一個單頁應用。文章的最后也會給出相應的demo地址

本文主要是對SPA搭建的實踐過程講解,在對react、redux、react-router有了初步了解后,來運用這些技術構建一個簡單的單頁應用。這個應用包括了側邊導航欄與主體內容區域。下面簡單羅列了將會用到的一些框架與工具。

  • create-react-app:腳手架
  • react:負責頁面組件構建
  • react-router:負責單頁應用路由部分的控制
  • redux:負責管理整個應用的數據流
  • react-redux:將react與redux這兩部分相結合
  • redux-thunk:redux的一個中間件。可以使action creator返回一個function(而不僅僅是object),并且使得dispatch方法可以接收一個function作為參數,通過這種改造使得action支持異步(或延遲)操作
  • redux-actions:針對redux的一個FSA工具箱,可以相應簡化與標準化action與reducer部分

好了,話不多說,一起來構建你的單頁應用吧。

使用create-react-app腳手架

create-react-app是Facebook官方出品的腳手架。有了它,你只需要一行指令即可跳過webpack繁瑣的配置、npm繁多的引入等過程,迅速構建react項目。

首先安裝create-react-app

npm i -g create-react-app

安裝完成后,就可以使用create-react-app指令快速創建一個基于webpack的react應用程序

cd $your_dir
create-react-app react-redux-demo

這時你可以進入react-redux-demo這個目錄,運行npm start既可啟動該應用。

打開訪問localhost:3000看到下方對應的頁面,就說明項目基礎框架創建完畢了。

啟動頁面

創建React組件

修改目錄結構

下面在我們的react-redux-demo項目,查看一下相應的目錄結構

|--public
    |--index.html
    |-- ……
|--src
    |--App.js
    |--index.js
    |-- ……
|--node_modules

其中public中存放的內容不會被webpack編譯,所以可以放一些靜態頁面或圖片;src中存放的內容才會被webpack打包編譯,我們主要工作的目錄就是在src下。

了解react的同學肯定知道,在react中我們通過構建各種react component來實現一個新的世界。在我們的項目里,會基于此,將組件分為通用組件部分與頁面組件部分。通用組件也就是我們普遍意義上的組件,一些大型項目會維護一個自己的組件庫,其中的組件會被整個項目共享;頁面組件實際上就是我們項目中所呈現出來的各個頁面。因此,我們的目錄會變成這樣

|--public
      |--index.html
      |-- ……
|--src
    |--page
         |--welcome.js
         |--goods.js
    |--component
         |--nav
             |--index.js
             |--index.css
    |--App.js
    |--index.js
    |-- ……
|--node_modules

src目錄下新建了pagecomponent兩個目錄分別用于存放頁面組件和通用組件。頁面組件包括welcome.js和商品列表頁good.js,通用組件包括了一個導航欄nav

兩種組件形式

編寫頁面或組件,類似于靜態頁的開發。推薦的組件寫法有兩種:

1)純函數形式:該類組件為無狀態組件。由于使用函數來定義,因此不能訪問this對象,同時也沒有生命周期方法,只能訪問props。這類組件主要是一些純展示類的小組件,通過將這些小組件進行組合構成更為復雜的組件。例如:

const Title = props => (
    <h1>
        {props.title} - {props.subtitle}
    </h1>
)

2)es6形式的組件:該類組件一般為復雜的或有狀態組件。使用es6的class語法進行創建。需要注意的是,在頁面/組件中使用this注意其指向,必要時需要綁定。綁定方法可以使用bind函數或箭頭函數。創建方式如下:

class Title extends Component {
    constructor(props) {
        super(props);
        this.state = {
            shown: true
        };
    }
    
    render() {
        let style = {
            display: this.state.shown ? 'block' : none
        };
        return (
            <h1 style={style}>
                {props.title} - {props.subtitle}
            </h1>
        );
    }
}

下面是這兩種組件之間的對比:

Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

鑒于上面的分析,我們可以將導航欄nav編寫為無狀態組件,而page中的部分使用有狀態的組件。

導航欄組件nav

// component/nav/index.css
.nav {
    margin: 30px;
    padding: 0;
}
.nav li {
    border-left: 5px solid sandybrown;
    margin: 15px 0;
    padding: 6px 0;
    color: #333;
    list-style: none;
    background: #bbb;
}

// component/nav/index.js
import React from 'react';
import './index.css';

const Nav = props => (
    <ul className="nav">
        {
            props.list.map((ele, idx) => (
                <li key={idx}>{ele.text}</li>
            ))
        }
    </ul>
);

export default Nav;

修改后的App.jsApp.css

// App.css
.App {
    text-align: center;
}
.App::after {
    clear: both;
}
.nav_bar {
    float: left;
    width: 300px;
}
.conent {
    margin-left: 300px;
    padding: 30px;
}

// App.js
import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';

const LIST = [{
    text: 'welcome',
    url: '/welcome'
}, {
    text: 'goods',
    url: '/goods'
}];

const GOODS = [{
    name: 'iPhone 7',
    price: '6,888',
    amount: 37
}, {
    name: 'iPad',
    price: '3,488',
    amount: 82
}, {
    name: 'MacBook Pro',
    price: '11,888',
    amount: 15
}];

class App extends Component {
    render() {
        return (
            <div className="App">
                <div className="nav_bar">
                    <Nav list={LIST} />
                </div>
                <div className="conent">
                    <Welcome />
                    <Goods list={GOODS} />
                </div>
            </div>
        );
    }
}

export default App;

welcome頁面

// page/welcome.js
import React from 'react';

const Welcome = props => (
    <h1>Welcome!</h1>
);

export default Welcome;

goods頁面

// page/goods.js
import React, { Component } from 'react';

class Goods extends Component {
    render() {
        return (
            <ul className="goods">
                {
                    this.props.list.map((ele, idx) => (
                        <li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
                            <span>{ele.name}</span> | 
                            <span>¥ {ele.price}</span> | 
                            <span>剩余 {ele.amount} 件</span>
                        </li>
                    ))
                }
            </ul>
        );
    }
}

export default Goods;

現在我們的頁面是這樣的

使用redux來管理數據流

redux數據流示意圖

redux是flux架構的一種實現。圖中展示了,在react+redux框架下,一個點擊事件是如何進行交互的。

然而redux并不是完全依附于react的框架,實際上redux是可以和任何UI層框架相結合的。因此,為了更好得結合redux與react,對redux-flow中的store有一個更好的全局性管理,我們還需要使用react-redux

npm i --save redux
npm i --save react-redux

同時,為了更好地創建action和reducer,我們還會在項目中引入redux-actions:一個針對redux的一個FSA工具箱,可以相應簡化與標準化action與reducer部分。當然,這是可選的

npm i --save redux-actions

下面我們會以goods頁面為例,實現以下場景:goods頁面組件渲染完成后,發送請求,獲取商品列表。其中獲取數據的方法會使用mock數據。

為了實現這些功能,我們需要進一步調整目錄結構

|--public
      |--index.html
      |-- ……
|--src
    |--page
         |--welcome.js
         |--goods.js
    |--component
         |--nav
             |--index.js
             |--index.css
    |--action
         |--goods.js
    |--reducer
         |--goods.js
         |--index.js
    |--App.js
    |--index.js
    |-- ……
|--node_modules

首先,創建action

首先,我們要創建對應的action。

action是一個object類型,對于action的結構有Flux有相關的標準化建議FSA
一個action必須要包含type屬性,同時它還有三個可選屬性errorpayloadmeta

  • type屬性相當于是action的標識,通過它可以區分不同的action,其類型只能是字符串常量或Symbol
  • payload屬性是可選的,可以使任何類型。payload可以用來裝載數據;在error為true的時候,payload一般是用來裝載錯誤信息。
  • error屬性是可選的,一般當出現錯誤時其值為true;如果是其他值,不被理解為出現錯誤。
  • meta屬性可以使任何類型,它一般會包括一些不適合在payload中放置的數據。

我們可以創建一個獲取goods信息的action:

// action/goods.js
const getGoods = goods => {
    return {
        type: 'GET_GOODS',
        payload: goods
    };
}

這樣,我們就可以得到GET_GOODS這個action。

在項目中,使用redux-actions對actions進行創建與管理:

createAction(type, payloadCreator = Identity, ?metaCreator)

createAction相當于對action創建器的一個包裝,會返回一個FSA,使用這個返回的FSA可以創建具體的action。

payloadCreator是一個function,處理并返回需要的payload;如果空缺,會使用默認方法。如果傳入一個Error對象則會自動將action的error屬性設為true

example = createAction('EXAMLE', data => data);
// 和下面的使用效果一樣
example = createAction('EXAMLE');

因此上面的方式可以改寫為:

// action/goods.js
import {createAction} from 'redux-actions';
export const getGoods = createAction('GET_GOODS'); 

* 此外,還可以使用createActions同時創建多個action creators。

其次,創建state的處理方法——reducer

針對不同的action,會有不同的reducer對應進行state處理,它們通過type的值相互對應。
reducer是一個處理state的方法(function),該方法接收兩個參數,當前狀態state和對應的action。根據stateaction,reducer會進行處理并返回一個新的state(同時也是一個新的object,而不去修改原state)。可以通過簡單的switch操作來實現:

// reducer/goods.js
const goods = (state, action) => {
    switch (action.type) {
        case 'GET_GOODS':
            return {
                ...state,
                data: action.payload
            };
        // 其他action處理……
    }
}

對應createActionredux-actions也有相應的reducer方式:

handleAction(type, reducer | reducerMap = Identity, defaultState)

type可以是字符串,也可以是createAction返回的action創建器:

handleAction('GET_GOODS', {
    next(state, action) {...},
    throw(state, action) {...}
}, defaultState);

//或者可以是
handleAction(getGoods, {
    next(state, action) {...},
    throw(state, action) {...}
}, defaultState);

此外,有時候一些操作的一系列action可以在語義和業務邏輯上是有一定聯系的,我們希望將他們放在一起便于維護。可以通過handleActions方法將多個相關的reducer寫在一起,以便于后期維護:

handleActions(reducerMap, defaultState)

因此,我們使用redux-actions來改寫我們之前寫的reducer

// reducer/goods.js
import {handleActions} from 'redux-actions';

export const goods = handleActions({
    GET_GOODS: (state, action) => ({
        ...state,
        data: action.payload
    })
}, {
    data: []
});

然后,對reducer進行合并

因為在redux中會統一管理一個store,因此,需要將不用的reducer所處理的state進行合并。

redux為我們提供了combineReducers方法。當業務邏輯過多時,我們可以將多個reducer進行組合,生成一個統一的reducer。雖然現在我們只有一個reducer,但是為了拓展性和示范性,在這里還是創建了一個reducer/index.js文件來進行reducer的合并,生成一個rootReducer

// reducer/index.js
import {combineReducers} from 'redux';
import {goods} from './goods';

export const rootReducer = combineReducers({
    goods
});

之后,將頁面組件與數據流相結合

上面的部分已經將redux中的action與reducer創建完畢了,然而,現在的數據流和我們的組件仍然是處于分離狀態的,我們需要讓全局的state,即store,的變化能夠驅動頁面組件的變化,才能完成redux-flow中的最后一環。這就需要將store中的各部分state映射到組件的props上。

解決這個問題就要用到我們之前提到的react-redux工具了。

首先,我們需要基于rootReducer創建一個全局的store。在src目錄下新建一個store.js文件,調用redux的createStore方法:

// store.js
import {createStore} from 'redux';
import {rootReducer} from './reducer';
export const store = createStore(rootReducer);

然后,我們需要讓所有的組件都能訪問到store。最簡單的方式就是使用react-redux
提供的Provider對整個應用進行包裝。這樣就可以使所有的子頁面、子組件能訪問到store。因此需要改寫index.js

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Provider} from 'react-redux';
import {store} from './store';

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
document.getElementById('root'));

最后,才是進行組件與狀態的連接。將store中需要映射的部分connect到我們的組件上。使用其connect方法可以做到這一點:

connect(mapStateToProps)(component);

redux中存在一個全局的store,其中存儲了整個應用的狀態,對其進行統一管理。connect可以將這個狀態中的數據連接到頁面組件上。其中,mapStateToProps是store中狀態到該組件屬性的一個映射方式,component是需要連接的頁面組件。通過connect方法,一旦store發生變化,組件也就會相應更新。

我們需要修改原先page/goods.js

import React, { Component } from 'react';
import {connect} from 'react-redux';

class Goods extends Component {
    render() {
        return (
            <ul className="goods">
                {
                    this.props.list.map((ele, idx) => (
                        <li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
                            <span>{ele.name}</span> | 
                            <span>¥ {ele.price}</span> | 
                            <span>剩余 {ele.amount} 件</span>
                        </li>
                    ))
                }
            </ul>
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
    goods: state.goods.data
});
// -export default Goods;
export default connect(mapStateToProps)(Goods);

此外,也可以為組件中相應的方法映射對應的action的觸發:

const mapDispatchToProps = dispatch => ({
    onShownClick: () => dispatch($yourAction)
});

最后,在組件渲染完成后觸發整個flow

如果產生了一個需要狀態更新的交互,可以通過在組件中相應部分觸發action來實現狀態更新-->組件更新。觸發方式:

dispatch($your_action)

connect后的組件,其props里會有一個dispatch的屬性,就是個dispatch方法:

let dispatch = this.props.dispatch;

因此,最終的page/goods.js組件如下:

import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../action/goods';

const GOODS = [{
    name: 'iPhone 7',
    price: '6,888',
    amount: 37
}, {
    name: 'iPad',
    price: '3,488',
    amount: 82
}, {
    name: 'MacBook Pro',
    price: '11,888',
    amount: 15
}]; 

class Goods extends Component {
    componentDidMount() {
        let dispatch = this.props.dispatch;
        dispatch(actions.getGoods(GOODS));
    }
    render() {
        return (
            <ul className="goods">
                {
                    this.props.goods.map((ele, idx) => (
                        <li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
                            <span>{ele.name}</span> | 
                            <span>¥ {ele.price}</span> | 
                            <span>剩余 {ele.amount} 件</span>
                        </li>
                    ))
                }
            </ul>
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
    goods: state.goods.data
});

export default connect(mapStateToProps)(Goods);

注意到,組件中數據不再是由App.js中寫入的了,而是經過了完整的redux-flow的過程獲取并渲染的。注意同時修改App.js

import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';

const LIST = [{
    text: 'welcome',
    url: '/'
}, {
    text: 'goods',
    url: '/goods'
}];

class App extends Component {
    render() {
        return (
            <div className="App">
                <div className="nav_bar">
                    <Nav list={LIST} />
                </div>
                <div className="conent">
                    <Welcome />
                    <Goods />
                </div>
            </div>
        );
    }
}

export default App;

現在訪問頁面,雖然效果和之前一致,但是其內部構造和原理已經大不相同了。

最后一部分:添加路由系統

單頁應用中的重要部分,就是路由系統。由于不同普通的頁面跳轉刷新,因此單頁應用會有一套自己的路由系統需要維護。

我們當然可以手寫一個路由系統,但是,為了快速有效地創建于管理我們的應用,我們可以選擇一個好用的路由系統。本文選擇了react-router 4。這里需要注意,在v4版本里,react-router將WEB部分的路由系統拆分至了react-router-dom,因此需要npmreact-router-dom

npm i --save react-router-dom

本例中我們使用react-router中的BrowserRouter組件包裹整個App應用,在其中使用Route組件用于匹配不同的路由時加載不同的頁面組件。(也可以使用HashRouter,顧名思義,是使用hash來作為路徑)react-router推薦使用BrowserRouterBrowserRouter需要history相關的API支持。

首先,需要在App.js中添加BrowserRouter組件,并將Route組件放在BrowserRouter內。其中Route組件接收兩個屬性:pathcomponent,分別是匹配的路徑與加載渲染的組件

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Provider} from 'react-redux';
import {store} from './store';
import {BrowserRouter, Route} from 'react-router-dom';

ReactDOM.render(
    <Provider store={store}>
        <BrowserRouter>
            <Route path='/' component={App}/>
        </BrowserRouter>
    </Provider>,
document.getElementById('root'));

此時我們啟動服務器的效果和之前一直。因為此時路由匹配到了path='/',因此加載了App組件。

還記得我們在最開始部分創建的Nav導航欄組件么?現在,我們就要實現導航功能:點擊對應的導航欄鏈接,右側顯示不同的區域內容。這需要改造index.js中的content部分:我們為其添加兩個Route組件,分別在不同的路徑下加載不同的頁面組件(welcomegoods

// App.js
import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';
import {Route} from 'react-router-dom';

const LIST = [{
    text: 'welcome',
    url: '/welcome'
}, {
    text: 'goods',
    url: '/goods'
}];

class App extends Component {
    render() {
        return (
            <div className="App">
                <div className="nav_bar">
                    <Nav list={LIST} />
                </div>
                <div className="conent">
                    <Route path='/welcome' component={Welcome} />
                    <Route path='/goods' component={Goods} />
                </div>
            </div>
        );
    }
}

export default App;

現在,可以嘗試在地址欄輸入http://localhost:3000http://localhost:3000/welcomehttp://localhost:3000/goods來查看效果。

當然,實際項目里不可能是通過手動修改地址欄來“跳轉”頁面。所以需要用到Link這個組件。通過其中的to這個屬性來指明“跳轉”的地址。這個Link組件我們會添加到Nav組件中

// component/nav/index.js
import React from 'react';
import './index.css';
import {Link} from 'react-router-dom';

const Nav = props => (
    <ul className="nav">
        {
            props.list.map((ele, idx) => (
                <Link to={ele.url} key={idx}>
                    <li>{ele.text}</li>
                </Link>
            ))
        }
    </ul>
);

export default Nav;

最終頁面效果如下:

最終效果圖welcome頁面
最終效果圖goods頁面

現在在這個demo里,我們點擊左側的導航,右側內容發生變化,瀏覽器不會刷新。基于React+Redux+React-router,我們實現了一個最基礎版的SPA(單頁應用)。


點擊這里可以下載這個demo。


額外的部分,異步請求

如果你還記得在redux數據流部分,是怎么給goods頁面傳入數據的:dispatch(actions.getGoods(GOODS)),我們直接給getGoods這個action構造器傳入GOODS列表,作為加載的數據。但是,在實際的應用場景中,往往,我們會在action中發送ajax請求,從后端獲取數據;在等待數據獲取的過程中,可能還會有一個loading效果;最后收到了response響應,再渲染響應頁面。

基于以上的場景,重新整理一下我們的action內的思路:

  1. component渲染完成后,觸發一個action,dispatch(actions.getGoods())。這個action并不會帶列表的參數,而是向后端請求結果。
  2. getGoods()這個方法里,主要會做這三件數:首先,觸發一個requestGoods的action,用于表示現在正在請求數據;其次,會調用一個叫fetchData()的方法,這個就是向后端請求數據的方法;最后,在拿到數據后,再觸發一個receiveGoods的action,用于標識請求完成并帶上渲染的數據。
  3. 其他部分與之前類似。

這里就有一個問題,基于上面的討論,我們需要actions.getGoods()這個方法返回一個function來實現我們在步驟2里所說的三個功能;然而,目前項目中的dispatch()方法只能接受一個object類型作為參數。所以,我們需要改造dispatch()方法。

改造的手段就是使用redux-thunk這個中間件。可以使action creator返回一個function(而不僅僅是object),并且使得dispatch方法可以接收一個function作為參數,通過這種改造使得action支持異步(或延遲)操作。

那么如何來改造呢?首先為redux加入redux-thunk這個中間件

npm i --save redux-thunk

然后修改store.js

// store.js
import {createStore, applyMiddleware, compose} from 'redux';
import {rootReducer} from './reducer';
import thunk from 'redux-thunk';

const middleware = [thunk];
export const store = createStore(rootReducer, compose(
    applyMiddleware(...middleware)
));

然后,基于之前的思路,整理action中的代碼。在這里,我們使用setTimeout來模擬向后端請求數據:

// action/goods.js
import {createAction} from 'redux-actions';

const GOODS = [{
    name: 'iPhone 7',
    price: '6,888',
    amount: 37
}, {
    name: 'iPad',
    price: '3,488',
    amount: 82
}, {
    name: 'MacBook Pro',
    price: '11,888',
    amount: 15
}]; 

const requestGoods = createAction('REQUEST_GOODS');
const receiveGoods = createAction('RECEIVE_GOODS');

const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(GOODS);
        }, 1500);
    });
};

export const getGoods = () => async dispatch => {
    dispatch(requestGoods());
    let goods = await fetchData();
    dispatch(receiveGoods(goods));
};

相應地修改reducer中的代碼

// reducer/goods.js
import {handleActions} from 'redux-actions';

export const goods = handleActions({
    REQUEST_GOODS: (state, action) => ({
        ...state,
        isFetching: true
    }),
    RECEIVE_GOODS: (state, action) => ({
        ...state,
        isFetching: false,
        data: action.payload
    })
}, {
    isFetching: false,
    data: []
});

可以看到,我們添加了一個isFetching的狀態來表示數據是否加載完畢。

最后,還需要更新UI component層

// page/goods.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../action/goods';

class Goods extends Component {
    componentDidMount() {
        let dispatch = this.props.dispatch;
        dispatch(actions.getGoods());
    }
    render() {
        return this.props.isFetching ? (<h1>Loading…</h1>) : (
            <ul className="goods">
                {
                    this.props.goods.map((ele, idx) => (
                        <li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
                            <span>{ele.name}</span> | 
                            <span>¥ {ele.price}</span> | 
                            <span>剩余 {ele.amount} 件</span>
                        </li>
                    ))
                }
            </ul>
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
    isFetching: state.goods.isFetching,
    goods: state.goods.data
});

export default connect(mapStateToProps)(Goods);

最終,訪問http://localhost:3000/goods頁面會有一個大約1.5s的loading效果,然后等“后端”數據返回后渲染出列表。

loading效果
列表加載完畢

最后的最后,如果你還沒有走開

再介紹一個redux調試神器——redux-devTools,可以在chrome插件中可以找到

redux-devTools extension

在開發者工具中使用,可以很方便的進行redux的調試

redux-devTools調試界面
redux-devTools調試界面

當然,需要在代碼中進行簡單的配置。對store.js進行一些小修改

import {createStore, applyMiddleware, compose} from 'redux';
import {rootReducer} from './reducer';
import thunk from 'redux-thunk';

const middleware = [thunk];
// export const store = createStore(rootReducer, compose(
//     applyMiddleware(...middleware)
// ));
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const store = createStore(rootReducer, composeEnhancers(
    applyMiddleware(...middleware)
));

以上。

現在,你可以愉快地進行SPA的開發啦!本文中的demo可以點擊這里獲取


Happy Coding!


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

推薦閱讀更多精彩內容