DvaJS构建配置React项目与使用

1.1,介绍

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以dva是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。是由阿里架构师 sorrycc 带领 team 完成的一套前端框架。

1.2,需求

快速搭建基于react的项目(PC端,移动端)。

二,DvaJS构建项目

2.1,初始化项目

第一步:安装node

第二步:安装最新版本dva-cli

1 $ npm install dva-cli -g

2 $ dva -v

第三步:dva new 创建新应用

1 $ dva new myapp

也可以在创建项目目录myapp后,用dva init初始化项目

1 $ dva init

第四步:运行项目

1 $ cd myapp

2 $ npm start

浏览器会自动打开一个窗口

2.2,项目架构介绍

|-mock            //存放用于 mock 数据的文件

|-node_modules            //项目包

|-public            //一般用于存放静态文件,打包时会被直接复制到输出目录(./dist)

|-src              //项目源代码

  |  |-asserts        //用于存放静态资源,打包时会经过 webpack 处理

  |  |-caches        //缓存

  |  |-components    //组件 存放 React 组件,一般是该项目公用的无状态组件

  |  |-entries        //入口

  |  |-models        //数据模型 存放模型文件

  |  |-pages          //页面视图

  |  |-routes        //路由 存放需要 connect model 的路由组件

  |  |-services      //服务 存放服务文件,一般是网络请求等

  |  |-test          //测试

  |  |-utils          //辅助工具 工具类库

|-package.json      //包管理代码

|-webpackrc.js  //开发配置

|-tsconfig.json    /// ts配置

|-webpack.config.js //webpack配置

|-.gitignore //Git忽略文件

在dva项目目录中主要分3层,models,services,components,其中models是最重要概念,这里放的是各种数据,与数据交互的应该都是在这里。services是请求后台接口的方法。components是组件了。

三,DvaJS的使用

3.1,DvaJS的五个Api

复制代码

1 import dva from 'dva';

2 import {message} from 'antd';

3 import './index.css';

4

5 // 1. Initialize 创建 dva 应用实例

6 const app = dva();

7

8 // 2. Plugins 装载插件(可选)

9 app.use({

10  onError: function (error, action) {

11    message.error(error.message || '失败', 5);

12  }

13 });

14

15 // 3. Model 注册model

16  app.model(require('../models/example').default);

17

18 // 4. Router 配置路由

19 app.router(require('../routes/router').default);

20

21 // 5. Start 启动应用

22 app.start('#root');

23

24 export default app._store; // eslint-disable-line 抛出

复制代码

1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)​

在opts可以配置所有的hooks

复制代码

1 const app = dva({

2      history,

3      initialState,

4      onError,

5      onHmr,

6 });

复制代码

这里比较常用的是,history的配置,一般默认的是hashHistory,如果要配置 history 为 browserHistory,可以这样:

1 import dva from 'dva';

2 import createHistory from 'history/createBrowserHistory';

3 const app = dva({

4  history: createHistory(),

5 });

initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在modal里面设置相应的state。

2,app.use(Hooks):配置 hooks 或者注册插件。

1 app.use({

2  onError: function (error, action) {

3    message.error(error.message || '失败', 5);

4  }

5 });

可以根据自己的需要来选择注册相应的插件

3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。

复制代码

1 export default {

2

3  namespace: 'example',//model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace

4

5  state: {},//表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值)

6

7  subscriptions: {//语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action

8    setup({ dispatch, history }) {  // eslint-disable-line

9    },

10  },

11

12  effects: {//Effect 被称为副作用,最常见的就是异步操作

13    *fetch({ payload }, { call, put }) {  // eslint-disable-line

14      yield put({ type: 'save' });

15    },

16  },

17

18  reducers: {//reducers 聚合积累的结果是当前 model 的 state 对象

19    save(state, action) {

20      return { ...state, ...action.payload };

21    },

22  },

23

24 };

复制代码

4,app.router(Function):注册路由表,我们做路由跳转的地方

复制代码

1 import React from 'react';

2 import { routerRedux, Route ,Switch} from 'dva/router';

3 import { LocaleProvider } from 'antd';

4 import App from '../components/App/App';

5 import Flex from '../components/Header/index';

6 import Login from '../pages/Login/Login';

7 import Home from '../pages/Home/Home';

8 import zhCN from 'antd/lib/locale-provider/zh_CN';

9 const {ConnectedRouter} = routerRedux;

10

11 function RouterConfig({history}) {

12  return (

13    <ConnectedRouter history={history}>

14      <Switch>

15        <Route path="/login"  component={Login} />

16        <LocaleProvider locale={zhCN}>

17        <App>

18          <Flex>

19            <Switch>

20            <Route path="/"  exact component={Home} />

21            </Switch>

22          </Flex>

23        </App>

24        </LocaleProvider>

25      </Switch>

26    </ConnectedRouter>

27  );

28 }

29

30 export default RouterConfig;

复制代码

5,app.start([HTMLElement], opts)

启动我们自己的应用

3.2,DvaJS的十个概念

1,Model

model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发

复制代码

1 import Model from 'dva-model';

2 // import effect from 'dva-model/effect';

3 import queryString from 'query-string';

4 import pathToRegexp from 'path-to-regexp';

5 import {ManagementPage as namespace} from '../../utils/namespace';

6 import {

7  getPages,

8 } from '../../services/page';

9

10 export default Model({

11  namespace,

12  subscriptions: {

13    setup({dispatch, history}) {  // eslint-disable-line

14      history.listen(location => {

15        const {pathname, search} = location;

16        const query = queryString.parse(search);

17        const match = pathToRegexp(namespace + '/:action').exec(pathname);

18        if (match) {

19            dispatch({

20              type:'getPages',

21            payload:{

22                s:query.s || 10,

23                p:query.p || 1,

24                j_code:parseInt(query.j,10) || 1,

25              }

26            });

27        }

28

29      })

30    }

31  },

32  reducers: {

33    getPagesSuccess(state, action) {

34      const {list, total} = action.result;

35      return {...state, list, loading: false, total};

36    },

37  }

38 }, {

39  getPages,

40 })

复制代码

2,namespace

model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace

3,State(状态)

初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState

复制代码

1 // dva()初始化

2 const app = dva({

3  initialState: { count: 1 },

4 });

5

6 // modal()定义事件

7 app.model({

8  namespace: 'count',

9  state: 0,

10 });

复制代码

Model中state的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的

4,Subscription

Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等

复制代码

1 subscriptions: { //触发器。setup表示初始化即调用。

2    setup({dispatch, history}) {

3      history.listen(location => {//listen监听路由变化 调用不同的方法

4        if (location.pathname === '/login') {

5          //清除缓存

6        } else {

7          dispatch({

8            type: 'fetch'

9          });

10        }

11      });

12    },

13  },

复制代码

5,Effects

用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action 交给 reducer 的地方。其中它用到了redux-saga里面有几个常用的函数。

put  用来发起一条action

call 以异步的方式调用函数

select 从state中获取相关的数据

take 获取发送的数据

复制代码

1 effects: {

2    *login(action, saga){

3      const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 用户调用异步逻辑 支持Promise

4      if (data && data.token) {

5        yield saga.put(routerRedux.replace('/home'));//put 用于触发action 什么是action下面会讲到

6      }

7    },

8    *logout(action, saga){

9      const state = yield saga.select(state => state);//select 从state里获取数据

10    },

11 

12  },

复制代码

复制代码

1 reducers: {

2    add1(state) {

3      const newCurrent = state.current + 1;

4      return { ...state,

5        record: newCurrent > state.record ? newCurrent : state.record,

6        current: newCurrent,

7      };

8    },

9    minus(state) {

10      return { ...state, current: state.current - 1};

11    },

12  },

13  effects: {

14    *add(action, { call, put }) {

15      yield put({ type: 'add1' });

16      yield call(delayDeal, 1000);

17      yield put({ type: 'minus' });

18    },

19  },

复制代码

如果effect与reducers中的add方法重合了,这里会陷入一个死循环,因为当组件发送一个dispatch的时候,model会首先去找effect里面的方法,当又找到add的时候,就又会去请求effect里面的方法。

这里的 delayDeal,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js

复制代码

1 /**

2  *超时函数处理

3  * @param timeout  :timeout超时的时间参数

4  * @returns {*} :返回样式值

5  */

6 export function delayDeal(timeout) {

7  return new Promise((resolve) => {

8    setTimeout(resolve, timeout);

9  });

10 }

复制代码

接着我们在 models/example.js 导入这个 utils.js

1 import { delayDeal} from '../utils/utils';

6,Reducer

以key/value 格式定义 reducer,用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。其实一个纯函数。

1  reducers: {

2    loginSuccess(state, action){

3      return {...state, auth: action.result, loading: false};

4    },

5  }

7,Router

Router 表示路由配置信息,项目中的 router.js

8,RouteComponent

RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据

9,Action:表示操作事件,可以是同步,也可以是异步

action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload则表示这个 action 将要传递的数据

复制代码

1 {

2      type: namespace + '/login',

3      payload: {

4          userName: payload.userName,

5          password: payload.password

6        }

7  }

复制代码

构建一个Action 创建函数,如下:

复制代码

1 function goLogin(payload) {

2 let loginInfo = {

3            type: namespace + '/login',

4            payload: {

5              userName: payload.userName,

6              password: payload.password

7            }

8          }

9  return loginInfo

10 }

11

12 //我们直接dispatch(goLogin()),就发送了一个action。

13 dispatch(goLogin())

复制代码

10,dispatch

type dispatch = (a: Action) => Action

dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:

1 dispatch({

2    type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace

3  payload: {}, // 需要传递的信息

4 });

reducers 处理数据

effects  接收数据

subscriptions 监听数据

3.3,使用antd

先安装 antd 和 babel-plugin-import

1 npm install antd babel-plugin-import --save

2 # 或

3 yarn add antd babel-plugin-import

babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:

复制代码

1 {

2  "extraBabelPlugins": [

3    ["import", {

4      "libraryName": "antd",

5      "libraryDirectory": "es",

6      "style": true

7    }]

8  ]

9 }

复制代码

现在就可以按需引入 antd 的组件了,如 import { Button } from 'antd',Button 组件的样式文件也会自动帮你引入。

3.4,配置.webpackrc

1,entry是入口文件配置

单页类型:

1 entry: './src/entries/index.js',

多页类型:

1 "entry": "src/entries/*.js"

2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。

3,env针对特定的环境进行配置。dev 的环境变量是?development,build 的环境变量是?production。

复制代码

1 "extraBabelPlugins": ["transform-runtime"],

2 "env": {

3  development: {

4      extraBabelPlugins: ['dva-hmr'],

5    },

6    production: {

7      define: {

8        __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }

9    }

10 }

复制代码

开发环境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"],而生产环境下是?["transform-runtime"]

4,配置 webpack 的?externals?属性

1 // 配置 @antv/data-set和 rollbar 不打入代码

2 "externals": {

3    '@antv/data-set': 'DataSet',

4    rollbar: 'rollbar',

5 }

5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:

复制代码

1  proxy: {

2    "/api": {

3      // "target": "http://127.0.0.1/",

4      // "target": "http://127.0.0.1:9090/",

5      "target": "http://localhost:8080/",

6      "changeOrigin": true,

7      "pathRewrite": { "^/api" : "" }

8    }

9  },

复制代码

6,disableDynamicImport

禁用 import() 按需加载,全部打包在一个文件里,通过 babel-plugin-dynamic-import-node-sync 实现。

7,publicPath

配置 webpack 的 output.publicPath 属性。

8,extraBabelIncludes

定义额外需要做 babel 转换的文件匹配列表,格式为数组

9,outputPath

配置 webpack 的 output.path 属性。

打包输出的文件

1 config["outputPath"] = path.join(process.cwd(), './build/')

10,根据需求完整配置如下:

文件名称是:.webpackrc.js,可根据实际情况添加如下代码:

复制代码

1 const path = require('path');

2

3 const config = {

4  entry: './src/entries/index.js',

5  extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],

6  env: {

7    development: {

8      extraBabelPlugins: ['dva-hmr'],

9    },

10    production: {

11      define: {

12        __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }

13    }

14  },

15  externals: {

16    '@antv/data-set': 'DataSet',

17    rollbar: 'rollbar',

18  },

19  lessLoaderOptions: {

20    javascriptEnabled: true,

21  },

22  proxy: {

23    "/api": {

24      // "target": "http://127.0.0.1/",

25      // "target": "http://127.0.0.1:9090/",

26      "target": "http://localhost:8080/",

27      "changeOrigin": true,

28    }

29  },

30  es5ImcompatibleVersions:true,

31  disableDynamicImport: true,

32  publicPath: '/',

33  hash: false,

34  extraBabelIncludes:[

35    "node_modules"

36  ]

37 };

38 if (module.exports.env !== 'development') {

39  config["outputPath"] = path.join(process.cwd(), './build/')

40 }

41 export default config

复制代码

更多 .webpackrc 的配置请参考 roadhog 配置。

3.5,使用antd-mobile

先安装 antd-mobile 和 babel-plugin-import

1 npm install antd-mobile babel-plugin-import --save # 或

2 yarn add antd-mobile babel-plugin-import

babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:

1 {

2  "plugins": [

3    ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件

4  ]

5 }

现在就可以按需引入antd-mobile 的组件了,如 import { DatePicker} from 'antd-mobile',DatePicker 组件的样式文件也会自动帮你引入。

四,整体架构

我们根据 url 访问相关的 Route-Component,在组件中我们通过 dispatch 发送 action 到 model 里面的 effect 或者直接 Reducer

当我们将action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect 会发送相应的 action 给 reducer,由唯一能改变 state 的 reducer 改变 state ,然后通过connect重新渲染组件。

当我们将action发送给reducer,那直接由 reducer 改变 state,然后通过 connect 重新渲染组件。如下图所示:

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State

重置models里的数据:

1 dispatch({type:namespace+'/set',payload:{mdata:[]}});

  set是内置的方法

Dva官方文档              nginx代理部署Vue与React项目

五,问题记录

5.1,路由相关的问题

1,使用match后的路由跳转问题,版本routerV4

match是一个匹配路径参数的对象,它有一个属性params,里面的内容就是路径参数,除常用的params属性外,它还有url、path、isExact属性。

问题描述:不能跳转新页面或匹配跳转后,刷新时url所传的值会被重置掉

不能跳转的情况

复制代码

1 const {ConnectedRouter} = routerRedux;

2

3 function RouterConfig({history}) {

4 const tests =({match}) =>(

5    <div>

6      <Route exact path={`${match.url}/:tab`} component={Test}/>

7      <Route exact path={match.url} component={Test}/>

8    </div>

9

10  );

11  return (

12    <ConnectedRouter history={history}>

13      <Switch>

14        <Route path="/login" component={Login}/>

15        <LocaleProvider locale={zhCN}>

16          <App>

17            <Flex>

18              <Switch>

19                <Route path="/test" component={tests }/>

20                <Route exact path="/test/bindTest" component={BindTest}/>

21           

22              </Switch>

23            </Flex>

24          </App>

25        </LocaleProvider>

26      </Switch>

27    </ConnectedRouter>

28  );

29 }

复制代码

路由如上写法,使用下面方式不能跳转,但是地址栏路径变了

复制代码

1 import { routerRedux} from 'dva/router';

2 ...

4 this.props.dispatch(routerRedux.push({

5      pathname: '/test/bindTest',

6      search:queryString.stringify({

7        // ...query,

8        Code: code,

9        Name: name

10      })

11    }));

12

13 ...

复制代码

能跳转,但是刷新所传的参数被重置

复制代码

1 const {ConnectedRouter} = routerRedux;

2

3 function RouterConfig({history}) {

4 const tests =({match}) =>(

5    <div>

6      <Route exact path={`${match.url}/bindTest`} component={BindTest}/>

7      <Route exact path={`${match.url}/:tab`} component={Test}/>

8      <Route exact path={match.url} component={Test}/>

9    </div>

10

11  );

12  return (

13    <ConnectedRouter history={history}>

14      <Switch>

15        <Route path="/login" component={Login}/>

16        <LocaleProvider locale={zhCN}>

17          <App>

18            <Flex>

19              <Switch>

20                <Route path="/test" component={tests }/>

21              </Switch>

22            </Flex>

23          </App>

24        </LocaleProvider>

25      </Switch>

26    </ConnectedRouter>

27  );

28 }

复制代码

路由如上写法,使用下面方式可以跳转,但是刷新时所传的参数会被test里所传的参数重置

复制代码

1 ...

2

3 this.props.dispatch(routerRedux.push({

4        pathname: '/test/bindTest',

5        search:queryString.stringify({

6          // ...query,

7          Code: code,

8          Name: name

9        })

10 }));

11

12 ...

复制代码

解决办法如下:地址多加一级,跳出以前的界面

路由配置

复制代码

1 const {ConnectedRouter} = routerRedux;

2

3 function RouterConfig({history}) {

4 const tests =({match}) =>(

5    <div>

6      <Route exact path={`${match.url}/bind/test`} component={BindTest}/>

7      <Route exact path={`${match.url}/:tab`} component={Test}/>

8      <Route exact path={match.url} component={Test}/>

9    </div>

10

11  );

12  return (

13    <ConnectedRouter history={history}>

14              <Switch>

15                <Route path="/test" component={tests }/>

16              </Switch>

17    </ConnectedRouter>

18  );

19 }

复制代码

调用

复制代码

1 ...

3 this.props.dispatch(routerRedux.push({

4      pathname: '/test/bind/test1',

5      search:queryString.stringify({

6        // ...query,

7        Code: code,

8        Name: name

9      })

10    }));

11

12 ...

东莞网站建设www.zg886.cn

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容