IE8环境下Webpack+React全家桶配置总结

https://github.com/sitorhy/react-webpack-boilerplate

一、IE8基础兼容问题


1.1、CSS兼容

旧引擎不支持太多CSS3特性,在无法使用Flex布局的情况下尽量使用原始的表格排版代替,使用表格基本上不用考虑兼容性问题。

IE8不支持渐变和透明通道,做全局半透明遮罩的时候尤其蛋疼,DXImageTransform滤镜虽然能对容器进行着色但不能防止事件穿透,即便是一个全屏着色的遮罩层其背后的各种控件依然是可以点击的,建议放弃线性渐变,使用小尺寸PNG图片作为容器背景元素,设置background-repeat进行平铺。

1.2、JavaScript兼容

IE8非调试环境下不支持console,执行到console.log之类的代码会直接报错,只有调出控制台时才会生成window.console对象,因此首要引入console-polyfill,具体作用可参见其源代码。

1.2.1、保留字问题

ECMAScript定义了一套关键字和保留字,根据规定,关键字是保留的,不能用作变量名或函数名。在实现ECMAScript 3JavaScript引擎中使用关键字作标识符,会导致"Identifier Expected"(缺少标识符)错误。而使用保留字作标识符可能会也可能不会导致相同的错误,具体取决于特定的引擎。

break do instanceof typeof case else
new var catch finally return void
continue for switch while debugger function
this with default if throw delete
in try abstract enum int short
boolean export interface static byte extends
long super char final native synchronized
class float package throws implements protected
volatile double import public package let
yield

ECMAScript 5对使用关键字和保留字的规则进行了少许修改。关键字和保留字虽然仍然不能作为标识符使用,但可以用作对象的属性名,IE8自然是不支持新特性的,会将属性名当作保留字处理,定位“缺少标识符”错误时基本上都会指向某个保留字,使用插件es3ify-webpack-plugin可转换对象访问方式,将点运算符访问改为使用中括号运算符访问。

// In
var x = {class: 2,};
x.class = [3, 4,];
 
// Out:
var x = {"class": 2};
x["class"] = [3, 4];

混淆插件uglifyjs-webpack-plugin开启ie8支持能达到同样效果,因此可以只保留uglifyjs-webpack-plugin插件。

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

new UglifyJsPlugin({
      uglifyOptions: {
         ie8: true
   } 
})

1.2.2、ECMAScript 5

IE8不支持ECMAScript5,不能使用Array.mapFunction.bind等方法,引入es5-shim可扩展出大部分ES5 API

唯一重要且不能模拟的是Object.defineProperty,直接通过变量赋值的方式触发事件分发,已经超出旧引擎的能力范围了,像Vue.js这些使用到该特性的第三方库均不可能兼容IE8

<html>
 <head></head>
 <body> 
  <p id="time"></p>  
  <script>
    // run in internet explorer 9+
    var obj = {};
    var text = "";
    Object.defineProperty(obj, "a", {
      set: function(newValue) {
        text = newValue;
        document.getElementById("time").innerText = text;
      },
      get: function() {
        return text;
      }
    });

    setInterval(function() {
      obj.a = new Date().toString();
    },
    1000);
</script> 
 </body>
</html>

1.2.3、ECMAScript 6

Webpack 2.x / 3.x


基本上引入babel-polyfill足够了,主要是兼容Promise,引入现版本babel-polyfill已经不需要前置引入es5-shim

  • [必要] devtool设为source-map,不想生成js.map文件可以设成false
  • [非必要] 引入es5-shim,不适用复杂场景,babel-polyfill可以代劳。
  • [非必要] 引入es5-sham,不适用复杂场景。
  • [非必要] 引入es3ify-webpack-plugin,不使用React情况下,想观察转码结果可以单独引入。
  • [必要] 引入uglifyjs-webpack-plugin,并开启ie8支持。
  • [必要] 引入console-polyfill
  • [必要] 引入babel-polyfill

Webpack 4.x


改动大致如下:

  • [必要] @babel/polyfill,全盘引入,省时省力,不要多想。
  • [必要] anujs@1.5.2,额,用1.5.3以上版本可能会翻车,针对ie8这种老古董,没事还是不要折腾核心库。
  • [必要] 不要引入es5-shim,堆栈溢出。
  • [必要] 不要引入es5-sham,堆栈溢出。
  • [必要] 引入插件@babel/plugin-transform-modules-commonjs,能绕过'Accessors not supported!'异常。
{
  "presets": [
    [
      "@babel/env",
      {
        "loose": true
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-transform-modules-commonjs",
    "@babel/plugin-proposal-class-properties"
  ]
}
  • [必要] 不能使用splitChunk分包功能。
  • [必要] 引入uglifyjs-webpack-plugin,见下文。
  • [非必要] 引入@babel/plugin-proposal-class-properties,支持如下写法,不用到处bind回调方法。
class Test extends React.Component
{
      selfMethod = () => {
            /// do something
      };

      render()
      {
            return ( <button onClick={this.selfMethod}>Button</button> );
      }
}

二、Webpack配置


2.1、核心插件

Webpack 2.x / 3.x


某些插件在Webpack2.x3.x下具有不同适用性,安装的时候注意版本号。

插件 / 版本 webpack 2.7.0 webpack 3.12.0
react 0.14.9 0.14.9
react-dom 0.14.9 0.14.9
react-router 1.0.3 1.0.3
redux 3.5.2 3.5.2
webpack-dev-server 2.11.2 2.11.2
webpack-dev-middleware 2.0.6 2.0.6
webpack-hot-middleware >=2.22.2 >=2.22.2
url-loader 0.6.2 >=1.0.1
extract-text-webpack-plugin 2.1.2 3.0.2
history 1.7.0 1.7.0
es3ify-webpack-plugin >=0.0.1 >=0.1.0

测不出版本上限的插件全部用>=标注,可以尝试用最新版。
react能兼容ie8的最后版本是0.14.9
redux版本需要<3.6.0,我不清楚3.5.x3.6.0版本之间发生了什么。
react-router需要<2.0
react-redux@4.4.10是最后支持React 0.14的版本。
webpack-dev-middleware<3.0.0
history用于手动路由路转,配合react-router
anujs@1.5.2比较稳定。

Webpack 4.x


React全家桶保留Webpack 2.x / 3.x的兼容版本,其他插件基本上使用最新的即可。
因为针对IE8,需要使用uglifyjs-webpack-plugin代替默认的terser-webpack-plugin进行代码压缩。

optimization: {
        minimize: true,
        minimizer:  [
            new UglifyJsPlugin({
                uglifyOptions: {
                    warnings: false,
                    parse: {},
                    compress: false,
                    mangle: true,
                    output: null,
                    toplevel: false,
                    nameCache: null,
                    ie8: true,
                    keep_fnames: false
                }
            })
        ]
    }

因为IE8不支持WebSocket,IE8预览网页需要devServer使用生产环境配置实时watch编译,但代码被压缩不容易排错,因此还需要启动另外一个devServerChrome下进行开发。

2.2、入口文件顺序

entry项定义入口文件,其中包含必需的兼容库,视情况甚至还需要引入jQuery
main为自定义入口文件,在main.js完成ReactDOM.render等启动操作,视个人习惯而言。

entry: {
        "console-polyfill": "console-polyfill",
        "es5-shim": "es5-shim/es5-shim.js",
        "es5-sham": "es5-shim/es5-sham.js",
        "babel-polyfill": "babel-polyfill",
        main: [path.resolve("src", "index.js")]
    }

由于html-webpack-plugin默认注入顺序是不可预测的,可能会出现入口文件引入顺序不是兼容库填充顺序的情况出现。

<script type="text/javascript" src="./scripts/babel-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/main.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-shim.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-sham.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/console-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>

解决办法之一是不使用entry配置,手动整理出需要加载的静态资源,然后用<script>标签在模版文件中依次注入,这样的话就不需要配置entry了,只留一个入口文件main.js就够了。

另外一种方法是指定html-webpack-plugin插件的配置项chunksSortMode"manual",打包时入口文件会按配置项chunks指定的顺序注入。

plugins: [
        new HtmlWebpackPlugin(
            {
                template: path.join("src", "index.html"),
                favicon: path.join("src", "favicon.ico"),
                minify: false,
                hash: true,
                inject: true,
                chunks: ["console-polyfill", "es5-shim", "es5-sham", "babel-polyfill", "main"],
                chunksSortMode: "manual"
            })
    ]
<script type="text/javascript" src="./scripts/console-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-shim.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-sham.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/babel-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/main.js?fbe5d7c0c4f12cdfda8c"></script>

三、CSS模块化


Webpackloader预处理类似于管道,上一个loader的输出作为下一个loader的输入,用数组表示就是索引大的loader的输出作为索引小loader的输入。

{
    test: /\.less$/,
    use: [{
        loader: "style-loader" // creates style nodes from JS strings
    },
    {
        loader: "css-loader" // translates CSS into CommonJS
    },
    {
        loader: "less-loader" // compiles Less to CSS
    }]
}

3.1、预处理和分离

对于less文件的编译,less-loaderless文件编译输出为css代码,css代码中的url会在css-loader中被转换,样式代码随入口文件一并打包,style-loader在运行时生成<style>标签,并将样式代码注入到<style>标签中。

Webpack 2.x / 3.x


为了提高页面加载效率,需要使用插件extract-text-webpack-plugin将样式代码从入口文件中分离存储为css文件。

const ExtractTextPlugin = require('extract-text-webpack-plugin');
 
// Create multiple instances
const extractCSS = new ExtractTextPlugin('stylesheets/[name]-one.css');
const extractLESS = new ExtractTextPlugin('stylesheets/[name]-two.css');
 
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: extractCSS.extract([ 'css-loader', 'postcss-loader' ])
      },
      {
        test: /\.less$/i,
        use: extractLESS.extract([ 'css-loader', 'less-loader' ])
      },
    ]
  },
  plugins: [
    extractCSS,
    extractLESS
  ]
};

无论用什么预处理器,style-loadercss-loader总是固定存在的,配置
也是基本相同的,以至于可以编写一个模版函数来统一生成这些预处理配置。

Webpack 4.x


到了Webpack 4.xextract-text-webpack-plugin已被mini-css-extract-plugin代替。
2.x / 3.x不同,4.x提取css在预处理中间阶段进行,因此与style-loader互斥。
因此进行css分离时,不需要添加style-loader
如配置开发环境时,此时不需要分离css文件:

{
            test: /\.less$/i,
            use: [ "style-loader","css-loader","less-loader"]
}

到了生产环境,需要从js文件中分离样式:

{
        test: /\.less$/i,
        use: [ 
           { 
              loader :require("mini-css-extract-plugin").loader 
           },
           "css-loader",
           "less-loader"
         ]
}

///////// 生产环境需要在插件项配置 "mini-css-extract-plugin"

plugins:[  
        new MiniCssExtractPlugin({
            filename: outputPublicPath("[hash].css", "stylesheet")
        })
]

相当于生产环境下,mini-css-extract-plugin/loader代替了style-loader

样式压缩,4.x提供了统一的入口插件optimize-css-assets-webpack-plugincss-loader已经移除了minimize选项。

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

....

optimization: {
        minimize: true,
        minimizer: [
            new OptimizeCSSAssetsPlugin()
        ]
    }

3.2、模块化

Webpack 2.x / 3.x


css-loader自带模块化功能,其实就是混淆,需要指定配置项moduleslocalIdentName才会生效。

{
    loader: 'css-loader',
    options: {
        modules: true,
        localIdentName: '[path][name]__[local]--[hash:base64:5]'
    }
}

模块机制下的css编写与传统方式有些许不同,图片url是相对源代码目录的,经Webpack编译处理后才会转为网络资源路径,但当使用到less-loader这类预处理器时,这个环节会出错,css-loader不能处理上一级的输出,例下less文件:

.app
{
  background-image: url("./img/pic.png");

  .red
  {
    color: red;
  }
}

.redColor{
  color: red;
}

启动模块化功能时,css-loader会直接提示"Module not found",找不"pic.png",需要在中间加一层resolve-url-loader作转换,完整的配置如下:

{
    test:/\.less$/,
    use:extractLESS.extract({
        fallback:{
            loader:"style-loader",
            options:{
                sourceMap:true
            }
        },
        use:[{
            loader:"css-loader",
            options:{
                sourceMap:true,
                minimize:config.compress,
                localIdentName:"[name]-[local]-[hash:base64:5]",
                modules:true
            }
        },{
            loader:"resolve-url-loader"
        },{
            loader:"less-loader",
            options:{
                sourceMap:true
            }
        }],
        publicPath:"../"
    })
}

Webpack 4.x


4.x开启混淆只需要设置modulestrue

{
        test: /\.less$/i,
        use:[
              {
                  loader: 'css-loader',
                  options: {
                      modules:true
                  }
              },
              "less-loader"
        ]
}

3.3、全局冲突问题

模块化解决了全局污染问题,但是也可能导致全局样式失效,只要途径同一预处理管道的样式文件,选择器名称无例外都会被混淆,导致与容器的class匹配不上,因此需要把全局作用的样式文件从模块化管道中排除掉,最简单的方法是分开目录存放,然后修改配置项的正则表达式,通过目录名称来过滤。

/* In */
.app
{
  background-image: url("./img/pic.png");

  .red
  {
    color: red;
  }
}


/* Out */
.src-components-app-styles---app---1-oEM
{
   background-image: url("./images/pic.png");
}

.src-components-app-styles---app---1-oEM .src-components-app-styles---red---12lu-
{
  color: red;
}
 import React from 'react';
 import './app.less';
 
 export default () => {
   return (
      // app -> src-components-app-styles---app---1-oEM
     <div className="app">
       <p className="red">Hello World</p>
     </div>
   );
 };

3.3.1、方案一

CSS Modules允许使用:global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串。

.title {
   color: red;
 }
 
 :global(.title) {
   color: green;
 }

CSS Modules还提供一种显式的局部作用域语法:local(.className),等同于.className,所以上面的css也可以写成下面这样。

:local(.title) {
   color: red;
 }
 
 :global(.title) {
   color: green;
 }

3.3.2、方案二

通过文件后缀来区分是否是全局作用的样式文件,扩展名为“.less”则视为全局作用,后缀为“.scope.less”则视为局部作用,通过区分后缀是否包含scope字眼来分开编译,但正则表达式不擅长“不包含”的识别,不要企图通过/(scope){0}.less/来区分,你会发现路径冲突根本不会通过编译。
预处理配置项test除了可以接受正则表达式外,使用回调函数也是可以的,指定一个传入参数为文件物理路径的function,通过返回布尔值来代替正则表达式的test操作:

// custom function
const lang= "less"; // lang = "less" 、"sass" .....
const suffix = `.scope.${lang}`; 
const ext = `.${lang}`;

// if path include 'scope.less'
function test(path) {
    return path.lastIndexOf(ext) == path.length - ext.length && path.lastIndexOf(suffix) == path.length - suffix.length;
}

// webpack config
{
    test:test,
    use:extractLESS.extract({
        fallback:{
            loader:"style-loader",
            options:{
                sourceMap:true
            }
        },
        use:[{
            loader:"css-loader",
            options:{
                sourceMap:true,
                minimize:config.compress,
                localIdentName:"[name]-[local]-[hash:base64:5]",
                modules:true
            }
        },{
            loader:"resolve-url-loader"
        },{
            loader:"less-loader",
            options:{
                sourceMap:true
            }
        }],
        publicPath:"../"
    })
}

或者使用include/exclude参数,例如create-react-app脚手架默认文件名后缀为module.less为模块化样式文件,那么全局样式通道可设置exclude排除模块化样式文件,实现分流。

// 模块化
{
     test: /\.module\.(less)$/i,
     use: [
          {
                loader:"css-loader",
                options:{ modules:true }
          },
          "less-loader"  
    ]
}

// 全局作用
{
     test: /\.(less)$/i,
     exclude: /\.module\.(less)$/i,
     use: [
          {
                loader:"css-loader"
          },
          "less-loader"  
    ]
}

3.3、classnames插件

JSX书写className十分繁琐,可以使用插件classnames简化操作,具体使用方法参看官方文档。
https://github.com/JedWatson/classnames
https://www.npmjs.com/package/classnames

npm:
npm install classnames --save

Bower:
bower install classnames --save

Yarn (note that yarn add automatically saves the package to the dependencies in package.json):
yarn add classnames

模块机制下,样式文件导入后实际为一个map

{
    app: "src-components-app-styles---app---1-oEM",
    red: "src-components-app-styles---red---12lu-",
    big: "src-components-app-styles---big---Y9ObK"
}

JSXclassName只接受字符串,需自行拼接各个键值后赋值给className

import styles from "app.less"

<div className={styles.app+ " " +styles.red}>
  <p className={styles.red}></p>
</div>

// or

<div className={String.join(styles.app,styles.red," ")}>
  <p className={styles.red}></p>
</div>

使用classnames包装,实质仍然是输出字符串,但不用写String.join或加号了:

import styles from "app.less"
import classNames from "classnames";

<div className={classNames(styles.app,styles.red)}>
  <p className={styles.red}></p>
</div>

不想书写“styles.”,可以再进一步包装:

import styles from "app.less"
import classNames from "classnames/bind";

var cx = classNames.bind(styles);

<div className={cx('app','red')}>
  <p className={styles.red}></p>
</div>

理解其原理后,甚至可以:

function bindStyles(styles)
{
    return function(...argv){
        let _cx=classNames.bind(styles);
        let keys=argv.filter((i)=>typeof i =="string");
        console.log(keys)
        let maps=argv.filter((i)=>typeof i =="object");
        return [
            Object.entries(styles).filter((i)=>keys.indexOf(i[0])>=0).map((i)=>i[1]).join(" "),
            _cx(Object.assign({},...maps))
        ].join(" ");
    };
}

//////
import styles from "app.less"
import classNames from "classnames/bind";

let cx=bindStyles(styles);

<div className={cx("app",{"red":true})}>
  <p className={styles.red}></p>
</div>

四、React-Like


React 0.14过于老旧,部分特性已被警告使用。部分第三方React-Like框架可以兼容IE8,并且支持React15/16的特性,但redux/react-router还是要使用兼容版本。

anujs

https://github.com/RubyLouvre/anu

//webpack配置
resolve: {
   alias: {
      'react': 'anujs',
      'react-dom': 'anujs',
        // 若要兼容 IE 请使用以下配置
        // 'react': 'anujs/dist/ReactIE',
        // 'react-dom': 'anujs/dist/ReactIE',
        // 'redux': 'anujs/lib/ReduxIE',//这主要用于IE6-8,因为官方源码中的isPlainObject方法性能超差
        // 如果引用了 prop-types 或 create-react-class
        // 需要添加如下别名
        'prop-types': 'anujs/lib/ReactPropTypes',
        'create-react-class': 'anujs/lib/createClass'
        //如果你在移动端用到了onTouchTap事件
        'react-tap-event-plugin': 'anujs/lib/injectTapEventPlugin',  
   }
}

使用官方配置进行替换,即可顺利编译和运行,无须额外依赖。

nervjs

京东的轮子

https://github.com/NervJS/nerv
https://www.npmjs.com/package/nervjs
https://nervjs.github.io/docs/

使用时需要对引用作一些替换:

/* 
import React from "react"
import ReactDom from "react-dom"

replace with

import Nerv from "nervjs"
*/
import Nerv from "nervjs"

class App extends Nerv.Component {
  render() {
    return <TodoBox />;
  }
}

具体配置参考官方的IE8模版:
https://github.com/NervJS/nerv-webpack-boilerplate
IE8环境下nervjs自身可以正常运行,但不能整合react-router@1.0.3

五、其它坑


  • IE8不支持Websocket,只能使用现代浏览器进行日常开发,编译后再观察IE8运行效果。
  • React编译样式比较慢,尤其通过import方式引用样式时,dev-server经常因爆内存而崩溃。
  • 没有支持IE8的完善组件库,极有可能仍然离不开JQuery
    https://www.npmjs.com/package/jquery
    封装JQuery组件时尽量在componentWillUnmount中释放资源,但并不是所有第三方控件都有资源释放接口的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容