前端:React+React-router+Redux+ant Design+TypeScript+Scss 后端:SpringBoot+Mybatis实现登录的一次尝试


\color{red}{写在前面}

首先,我在日常项目中常用vue,毕竟相比于React,vue更容易上手,学习成本也比较低。但其实对React,我早就有心研究一番了,最近几天闲着没事就开始着手研究了一下,也算是刚刚入门。此博客仅供各位想要入坑react和typeScript的同学参考,有什么不足之处请各位不吝指出,小弟十分感谢!下面话不多说进入正题吧。。。

注:开始之前请先安装node环境,至于使用yarn还是npm就看各位的心情了,本文使用npm,安装依赖推荐使用淘宝镜像(cnpm),安装方法不会的同学请自行百度。。

一、创建React+TypeScript+ant Design项目

第一步
npm install -g create-react-app //全局安装create-react-app
 
第二步
create-react-app antd-demo-ts --scripts-version=react-scripts-ts //创建一个使用react、ant Desgin及typescript的项目

创建项目完成图示如下:


创建项目完成图示.png

然后我们进入项目并启动:

cd antd-demo-ts //进入项目

npm start //启动应用

此时浏览器会访问 http://localhost:3000/ ,看到 Welcome to React 的界面就算成功了。

二、测试ant Desgin引用是否成功

cnpm install antd --save-dev  //使用cnpm安装ant design依赖并写入package.json文件中

安装成功后,我们先修改 src/App.tsx 文件

import * as React from 'react';
import Button from 'antd/lib/button';
import './App.css';
 
import logo from './logo.svg';
 
class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
        <Button type="primary">AntDesgin按钮</Button>
      </div>
    );
  }
}
 
export default App;

然后修改 src/App.css 引入 antd 的样式:

@import '~antd/dist/antd.css'; //在App.css文件顶部引入
 
.App {
  text-align: center;
}
 
...

修改完成后 npm start 启动项目查看效果。。

\color{red}{但是~~,此时你会发现编译报错了,错误信息如下图:}
error.png

本菜当时看到这个问题的时候也是一脸懵逼,什么情况呢?上网找答案之后发现原来是ts校验规则的问题,这点我实在非常想要吐槽,import引入居然要按照字母排序,十分不适应,后续还会有更多的ts校验规则的问题,在此就先解决再继续往下进行。

\color{red}{解决方法:}

修改项目根目录下 tslint.json 文件

{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts"
    ]
  },
  "rules": {
      "ordered-imports": false,
      "prefer-const": false,
      "no-console":false,
      "no-debugger":false,
      "await-promise":false,
      "curly":false,
      "no-empty":false,
      "no-for-in-array":false,
      "no-invalid-this":false,
      "no-var-keyword":false,
      "member-access": false,
      "max-classes-per-file":false,
      "prefer-for-of":false,
      "triple-equals":false,
      "no-unused-expression": false,
      "prefer-readonly": false,
      "comment-format":false,
      "no-unnecessary-initializer": false,
      "no-unused-variable": false,
      "object-literal-sort-keys": false,
      "no-string-literal": false,
      "no-default-export":false
  }
}

将 tslint.json文件内容替换为上述代码即可解决问题,规则解释如下(官方文档 https://palantir.github.io/tslint/rules/ ,参考文档 https://www.cnblogs.com/wyy5552/p/8796695.html):
以上解决了ts校验规则的问题后,重新运行项目发现编译成功,运行效果图如下:

run.png

此时我们成功引用进来了ant Desgin
三、实际项目中的ant design按需引入的性能优化
\color{red}{(注:此步骤不是必须,可选择性跳过)}

我们现在已经把组件成功运行起来了,但是在实际开发过程中还有很多问题,例如上面的例子实际上加载了全部的 antd 组件的样式(对前端性能是个隐患)。

此时我们需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired(一个对 create-react-app 进行自定义配置的社区解决方案)。

引入 react-app-rewired 并修改 package.json 里的启动配置:

cnpm install react-app-rewired --dev //cnpm 安装 react-app-rewired
/* package.json */
"scripts": {
-   "start": "react-scripts-ts start", //原代码
+   "start": "react-app-rewired start --scripts-version react-scripts-ts", //修改后
-   "build": "react-scripts-ts build", //原代码
+   "build": "react-app-rewired build --scripts-version react-scripts-ts", //修改后
-   "test": "react-scripts-ts test --env=jsdom", //原代码
+   "test": "react-app-rewired test --env=jsdom --scripts-version react-scripts-ts", //修改后
}

然后在项目根目录创建一个 config-overrides.js 用于修改默认配置:

module.exports = function override(config, env) {
  // 等会将要修改的代码块
  return config;
};

然后安装 ts-import-plugin
ts-import-plugin 是一个用于按需加载组件代码和样式的 TypeScript 插件,现在我们尝试安装它并修改 config-overrides.js 文件:

cnpm install add ts-import-plugin --dev // cnpm 安装 ts-import-plugin

安装成功后,修改 config-overrides.js 文件:

/* config-overrides.js */
const tsImportPluginFactory = require('ts-import-plugin')
const { getLoader } = require("react-app-rewired");
 
module.exports = function override(config, env) {
  const tsLoader = getLoader(
    config.module.rules,
    rule =>
      rule.loader &&
      typeof rule.loader === 'string' &&
      rule.loader.includes('ts-loader')
  );
 
  tsLoader.options = {
    getCustomTransformers: () => ({
      before: [ tsImportPluginFactory({
        libraryDirectory: 'es',
        libraryName: 'antd',
        style: 'css',
      }) ]
    })
  };
 
  return config;
}

然后移除前面在 src/App.css 里全量添加的 @import '~antd/dist/antd.css'; 样式代码,并且修改 src/App.tsx 文件,按照下述模式引入模块,为了更直观显示效果,我增加引入了icon图标:

import * as React from 'react';
import { Button,Icon } from 'antd'; //引入了ant design的图标和按钮
import './App.css';
 
import logo from './logo.svg';
 
class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
        <Button type="primary">AntDesgin按钮</Button>
        <Icon style={{color:'green',fontSize:'24px',marginLeft:'50px'}} type="check-circle-o" />
      </div>
    );
  }
}
 
export default App;

上述修改完成后,运行项目,发现达到了我们预期的效果,图示如下:

run.png

至此我们对ant Design的引入就告一段落了。
注:以上参考ant design官方文档 https://ant.design/docs/react/use-in-typescript-cn

四、修改项目目录

按照个人习惯,我们新建assets文件夹以便于管理,修改后项目目录如下图所示:
注:修改完成后,别忘记修改 App.tsx、index.tsx 文件中的import路径哦

list.png

五、引入scss\color{red}{(不想使用scss可跳过本步骤)}

由于本菜喜欢scss的书写方便,由此引入,习惯css的同学可以不使用
1、安装 sass-loader node-sass 依赖:

cnpm install add sass-loader node-sass --save-dev //安装 sass-loader node-sass 依赖

\color{red}{但是~~(又来了),此时我们会发现,项目中并没有 webpack.config.js 这个文件,那么这个文件到底在哪呢?稍等,我去截个图。。。}

source.png

path.png

好了,通过上述步骤我们找到了webpack的配置文件,其实我们通过package.json中的start命令可以发现点端倪,其实webpack只是被封装起来了,
接下来我们就修改 webpack.config.dev.js 文件:

{
   test: /\.css$/,
   use: [
      ...
   ],
},
{
    test: /\.scss$/,
    loaders: ['style-loader', 'css-loader', 'sass-loader'],
}, //增加scss的规则

修改完成之后我们在 assets 文件夹下创建新的 scss 文件夹,并手动创建 App.scss 文件及 index.scss 文件:

创建完成后将 App.cssindex.scss 文件中的代码直接复制进对应的scss文件,然后删除css文件夹,操作完成后的项目目录如下:

\color{red}{注:别忘记修改App.tsx及index.tsx中的引用哦}

image.png

上述修改完成后,重启项目发现scss成功引入!

六、创建Spring-Boot项目并整合Mybatis

本菜在此就不做详细赘述了,想必用java的同学都会,不会的话参考下面链接:
1、spring-boot 1.5.*版本项目搭建及整合mybatis (https://blog.csdn.net/winter_chen001/article/details/77249029
2、spring-boot 2.0+版本项目搭建及整合mybatis (https://blog.csdn.net/Winter_chen001/article/details/80010967

七、编写登录所需接口

代码如下(仅贴出controller层~):

package com.ycgame.controller;
 
import com.ycgame.model.Result;
import com.ycgame.model.User;
import com.ycgame.service.user.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
 
import static com.ycgame.utils.SetResultInfo.setResultInfo;
 
/**
 * Created by Administrator on 2018/7/16.
 */
@Controller
@RequestMapping(value = "/user")
public class UserController {
 
    @Autowired
    private UserService userService;
 
    @ResponseBody
    @PostMapping("/login")
    public Result login(@RequestBody User user, HttpServletRequest request){
        System.out.println(user.getUsername());
        Result result=userService.findByUsername(user);
        if(result.getCode()==0){
            HttpSession session=request.getSession();
            session.setAttribute("user",user);
        }
        return result;
    }
 
    @ResponseBody
    @GetMapping("/loginState")
    public Result loginState(HttpServletRequest request){
        HttpSession session=request.getSession();
        User user=(User)session.getAttribute("user");
        Result result;
        if(user!=null){
            List<Object> dataList=new ArrayList<Object>();
            dataList.add(user);
            result=setResultInfo(0,dataList,"已登录");
        }else{
            result=setResultInfo(103,null,"未登录");
        }
        return result;
    }
 
    @ResponseBody
    @GetMapping("/loginOut")
    public Result loginOut(HttpServletRequest request){
        HttpSession session=request.getSession();
        session.removeAttribute("user");
        Result result=setResultInfo(0,null,null);
        return result;
    }
}

上述接口功能分别为:登录获取登录状态退出登录

八、引入React-router及axios

安装 react-router 依赖,本菜使用的是V4版本,各位可自行选择

cnpm install react-router-dom --save-dev //cnpm安装react-router-dom(react-router V4版本)
 
cnpm install @types/react-router-dom --save-dev 

为了实现类似 vue-router 中编程式导航的功能,本菜参考(https://segmentfault.com/a/1190000011137828)的第三种方案(主要刚接触,第一种方法不太会。。而且貌似挺麻烦。。勿喷。。)

1、安装history依赖
cnpm install @types/history --save-dev //安装histroy依赖
2、自己创建一个 history.ts/.js 文件,文件类型和位置看个人喜好,代码如下:
import createHistory from 'history/createBrowserHistory';
 
export default createHistory();

经过以上步骤,我们的 react-router 就算成功引入了。下面开始引入axios

cnpm install axios --save-dev //安装axios

\color{red}{(可跳过)}安装完成后,本菜对axios发送请求做了简单的封装(utils.ts),代码如下:

//utils.ts
 
import axios from 'axios'
 
const utils ={
    axiosMethod: (config:any) => {
        axios({
            method: config.method,
            url: config.url,
            params: config.params ? config.params : null,
            data: config.data ? config.data : null,
        }).then(config.callback).catch(config.catch ? config.catch : () => {})
    }
}
 
export default utils

\color{red}{注:axios发送请求时,post请求与get请求参数对象名是不一样的,post请求的参数对象为data,而get为params~}

\color{red}{(可跳过)}本菜为了请求统一管理新建了一个 axiosRequestConfig.ts 文件,代码如下:

//axiosRequestConfig.ts
 
const config={
    //登录校验
    doLoginConfig:{
        method:'post',
        url:'/user/login'
    },
    //登录状态验证
    loginStateConfig:{
        method:'get',
        url:'/user/loginState'
    },
    //退出登录
    loginOutConfig:{
        method:'get',
        url:'/user/loginOut'
    }
}
 
export default config

经过上述步骤,我们成功引入了react-routeraxios,本菜当前的项目目录如下:

image.png

九、配置webpack代理

由于spring-boot运行在8080端口,而我们的react项目则是3000端口,因此存在跨域问题,那么我们如何配置webpack的代理呢,下面上代码:

首先按照 第五步 的方法找到 webpackDevServer.config.js 文件(node_modules =>react-scripts-ts =>config=>webpackDevServer.config.js ),修改为以下内容:

...
 
https: protocol === 'https',
    host: host,
    overlay: true,
    historyApiFallback: {
      // Paths with dots should still use the history fallback.
      // See https://github.com/facebookincubator/create-react-app/issues/387.
      disableDotRule: true,
    },
    public: allowedHost,
    proxy: {
      '/user/*': {
        target: 'http://localhost:8080/', //本地后台的地址
        secure: false,
        changeOrigin: true
    },
},
 
before(app){
 
...

经过上述步骤,我们就能放心的撸代码了,再也不用担心跨域请求的问题啦~~

十、开始撸码

1、新建 Login.tsx 文件,文件位置看个人习惯,本菜直接放在src下面,使用ant design表单组件,代码如下:

//Login.tsx
 
import * as React from 'react';
import './assets/scss/Login.scss';
import utils from './utils/utils'
import requestConfig from './utils/axiosRequestConfig'
import { Form, Icon, Input, Button, Checkbox } from 'antd';
import history from './utils/history';
 
 
const FormItem = Form.Item;
 
class LoginForm extends React.Component {
  constructor(props: any) {
    super(props);
    this.state = {
      loginForm: {
        username: '',
        password: ''
      }
    }
    this.handleInputChange = this.handleInputChange.bind(this);
    this.submitForm = this.submitForm.bind(this);
  }
 
  handleInputChange(event: any): void {
    const target = event.target;
    const value = target.value;
    const name = target.name;
    const tempObj = { ...this.state['loginForm'] };
    tempObj[name] = value;
    this.setState({
      loginForm: tempObj
    })
  }
 
  submitForm(e: any) {
    e.preventDefault();
    this.props['form'].validateFields((err: any, values: any) => {
      if (!err) {
        let doLoginConfig = requestConfig.doLoginConfig;
        let config = {
          data: { t: new Date().getTime(), ...this.state['loginForm'] },
          callback: (response: any) => {
            if (response.data.code == 0) {
              history.push('/home');
            }
          }
        }
        let finalConfig = { ...doLoginConfig, ...config };
        utils.axiosMethod(finalConfig);
      }
    });
  }
 
  render(): any {
    const { getFieldDecorator } = this.props['form'];
    return (
      <Form onSubmit={this.submitForm} className="login-form">
        <FormItem>
          {getFieldDecorator('userName', {
            rules: [{ required: true, message: '请输入用户名', whitespace: true }],
          })(
            <Input name='username' onChange={this.handleInputChange} prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="请输入用户名" />
          )}
        </FormItem>
        <FormItem>
          {getFieldDecorator('password', {
            rules: [{ required: true, message: '请输入密码', whitespace: true }],
          })(
            <Input name='password' onChange={this.handleInputChange} prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="请输入密码" />
          )}
        </FormItem>
        <FormItem>
          {getFieldDecorator('remember', {
            valuePropName: 'checked',
            initialValue: true,
          })(
            <Checkbox style={{ color: 'white' }}>记住我</Checkbox>
          )}
          <a className="login-form-forgot" href="">忘记密码</a>
          <Button type="primary" htmlType="submit" className="login-form-button">
            登录
          </Button>
          <a href="">前往注册</a>
        </FormItem>
      </Form>
    );
  }
}
 
const LoginFormComponent = Form.create()(LoginForm);
 
class Login extends React.Component {
  render() {
    return (
      <div className="loginMain">
        <div className="login">
          <h2 style={{ color: 'white', textAlign: 'center' }}>用户登录</h2>
          <LoginFormComponent />
        </div>
      </div>
    );
  }
}
 
export default Login;

2、在assets/scss文件夹下新建 Login.scss 的样式文件,代码如下:

//Login.scss
 
h2 {
    font-size: 20px;
}
 
.loginMain{
    height: 100%;
    background: url('../images/bg.jpg') no-repeat center center fixed;
    background-size: cover;
}
 
.login{
    width:350px;
    height:330px;
    background: rgba($color: #000, $alpha: .4);
    border-radius: 20px;
    margin: auto;
    padding: 25px;
    color: white;
    position: absolute;
    left: 0 ;
    right: 0;
    top:25%;
}
 
.login-form {
    max-width: 300px;
}
 
.login-form-forgot {
    float: right;
}
 
.login-form-button {
    width: 100%;
}

3、修改 index.tsx 文件,代码如下:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import Login from './Login';
import registerServiceWorker from './registerServiceWorker';
import { Router, Route } from 'react-router-dom';
import history from './utils/history';
import requestConfig from './utils/axiosRequestConfig'
import utils from './utils/utils'
 
import './assets/scss/index.scss';
 
 
class Model extends React.Component {
  constructor(props: any) {
    super(props);
  }
 
  componentDidMount() {
    const loginStateConfig = requestConfig.loginStateConfig;
    const config = {
      param: { t: new Date().getTime() },
      callback: (response: any) => {
        if (response.data.code == 0) {
          history.push('/home');
        } else {
          history.push('/');
        }
      }
    }
    const finalConfig = { ...loginStateConfig, ...config };
    utils.axiosMethod(finalConfig);
  }
 
  render() {
    return (
      <div style={{ height: '100%' }}>
        <Route exact={true} path="/" component={Login} />
        <Route exact={true} path="/home" component={App} />
      </div>
    )
  }
}
 
ReactDOM.render(
  <Router history={history}>
    <Model />
  </Router>
  ,document.getElementById('root') as HTMLElement
);
registerServiceWorker();

4、给 App.tsx 增加退出登录按钮,修改后代码如下:

import './assets/scss/App.scss';
 
import * as React from 'react';
import history from './utils/history';
import requestConfig from './utils/axiosRequestConfig'
import utils from './utils/utils'
import { Button, Icon } from 'antd';
import logo from './assets/images/logo.svg';
 
class App extends React.Component {
  constructor(props: any) {
    super(props);
    this.loginOutMethod = this.loginOutMethod.bind(this);
  }
 
  loginOutMethod() {
    const loginOutConfig = requestConfig.loginOutConfig;
    const config = {
      param: { t: new Date().getTime() },
      callback: (response: any) => {
        if (response.data.code == 0) {
          history.push('/');
        }
      }
    }
    const finalConfig = { ...loginOutConfig, ...config };
    utils.axiosMethod(finalConfig);
  }
 
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
        <Button type="primary">AntDesgin按钮</Button>
        <Icon style={{ color: 'green', fontSize: '24px', marginLeft: '50px' }} type="check-circle-o" />
        <br/>
        <Button style={{marginTop:'30px'}} onClick={this.loginOutMethod}>退出登录</Button>
      </div>
    );
  }
}
 
export default App;

以上我们的代码部分就暂时撸完了现在我们启动spring-boot项目及npm start运行react项目,效果如下(不会做gif理解一下。。。):

image.png

image.png

输入正确的用户名密码后点击登录会跳转到localhost:3000/home页面(主页),点击退出登录会返回登录页测试效果与我们希望的结果一致。

结束语

看到这里就结束了!!?这时候就会有小伙伴说了,Redux被你吃了么?全程没看到啊。。说来惭愧,Redux整合本菜倒是弄得差不多了,然而由于刚接触,后续有空再更新吧。。

以上就是本菜最近几天的摸索成果,实现起来可能不够优雅,希望各位React大佬勿喷,毕竟我只是一个React的萌新。。。最后希望本文对各位同学入坑React能有所帮助,如果能帮到您,是我的荣幸~~

\color{red}{最后附上github地址:}https://github.com/xueyecheng/react-antd-ts-demo

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

推荐阅读更多精彩内容