基于 Webpack4 + Vue 的多页应用解决方案(一)

本项目 GitHub 仓库地址:https://github.com/charleylla/charley-vue-multi

去年九月份,我写了一个使用 Webpack3 配置多页的系列文章,下面是文章地址:
使用 webpack3 配置多页应用(一)
使用 webpack3 配置多页应用(二)
使用 webpack3 配置多页应用(三)
使用 webpack3 配置多页应用(四)

在这之前,我还写过一个使用 Webpack2 搭建 React 应用脚手架的文章:
搭建基于 webpack2 的 react 脚手架

现在,我去了新的公司,公司需要我搭建一个多页应用脚手架,作为公司多页应用的通用解决方案。由于公司的主要技术栈是 Vue,我就琢磨着使用 Webpack + Vue 搭建这样一个脚手架。
对于 Vue 的多页应用方案,网上有很多文章,其中有一些是基于 vue-cli 生成的单页应用进行修改,我看了这些文章觉得有点麻烦,正好之前也搭过类似的脚手架,就决定再重复造个轮子。
一年间,很多朋友阅读了我的 《使用 webpack3 配置多页应用》系列文章,并提供了一些宝贵的建议,我将这些建议应用到了新的多页应用脚手架中。新的脚手架,使用了 Webpack4。如果不打算使用 Zero Config(事实上使用自定义配置的灵活性更强,可以完成更多个性化的需求),Webpack4 和 Webpack3 没有太大的区别,但还是有部分配置发生了变化,我在写文章的时候会指出来。
Webpack 的配置确实麻烦,从 Webpack2 到 Webpack4,每次配置都要重新学习一些知识,幸运的是每次学习的时间都比上一次要少。搭建 Webpack2 的脚手架,花了大概两个星期,搭建 Webpack3 的脚手架,花了不到一周,而搭建 Webpack4 的脚手架,花了大约两天时间。这说明 Webpack2 到 Webpack4 在配置层面变动并不大(内核层面变动很大)多配置几次,就慢慢熟悉了。
建议大家先看 《使用 webpack3 配置多页应用》这系列的文章,可以对使用 Webpack 构建多页应用和常用 Loader 及 Babel 等工具的用法有个了解,本系列文章是在前面文章的基础上进行整合改进的结果,再包含了一些前端架构上的内容。

项目目录一览

下面是本项目的一个整体目录及说明:

│  .babelrc
│  .editorconfig
│  .eslintrc.js
│  .gitignore
│  package-lock.json
│  package.json
│  postcss.config.js
│  README.md
│  webpack.config.js
│
├─build                         —— Webpack 配置文件
│      alias.js
│      bundle.js
│      externals.js
│      loaders.js
│      webpack.config.base.js
│      webpack.config.dev.js
│      webpack.config.prod.js
│
├─doc                           —— 项目文档
│      README.md
│
├─src                           —— 项目源代码
│  ├─assets                     —— 通用的库/图片/样式文件 
│  │  ├─image
│  │  ├─lib
│  │  └─style
│  │          atom.scss
│  │          constant.scss
│  │          func.scss
│  │          main.scss
│  │
│  ├─component                  —— 组件
│  │  └─test
│  │          index.js
│  │          style.scss
│  │          template.vue
│  │
│  ├─model                      —— 数据,接口请求
│  │      index.js
│  │
│  ├─page                       —— 页面
│  │  ├─home                    —— 每个页面一个文件夹,文件夹名字为页面名字
│  │  │      index.js
│  │  │      style.scss
│  │  │      template.vue
│  │  │
│  │  └─index
│  │          index.js
│  │          style.scss
│  │          template.vue
│  │
│  ├─shared                     —— 公用配置,如 API,常量等
│  │      api.js
│  │      constant.js
│  │
│  └─util                       —— 帮助,封装通用的方法
│          index.js
│          request.js
│
└─template                      —— 项目模板文件
        config.js
        favicon.ico
        index.html

基础配置

下面介绍几个基础配置,主要是 Babel,ESLint 和 PostCSS 的配置内容。

  • .babelrc文件
{
  "presets": [
    "env"
  ],
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false,
        "regenerator": true
      }
    ]
  ]
}
  • postcss.config.js 文件
module.exports = {
  plugins: {
    "autoprefixer": {
      browsers: ["last 5 version","Android >= 4.0"],
      //是否美化属性值 默认:true
      cascade: true,
      //是否去掉不必要的前缀 默认:true
      remove: true
    }
  }
}
  • .eslintrc.js 文件
module.exports = {
  "env": {
    "browser": true,
    "commonjs": true,
    "es6": true,
    "node": true,
  },
  "extends": [
    "eslint:recommended",
    "plugin:vue/essential"
  ],
  "parserOptions": {
    "ecmaVersion": 8,
    "sourceType": "module"
  },
  "rules": {
    "comma-dangle": ["warn", "always-multiline"],
    "indent": ["warn", 2],
    "linebreak-style": ["warn", "unix"],
    "quotes": ["warn", "double"],
    "semi": ["warn", "always"],
    "no-unused-vars": ["warn"],
    "no-console": "warn",
  },
};
  • .editorconfig 文件
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

Webpack 配置文件

下面介绍脚手架 Webpack 的配置,也是本文的重点。

入口文件

使用根目录下的 webpack.config.js 作为 Webpack 配置的入口文件:

const env = process.env.ENVIROMENT.trim();
const option = process.env.OPTION ? process.env.OPTION.trim() : "";
const webpackConfigFn = require(`./build/webpack.config.${env}`);
module.exports = webpackConfigFn(env,{ option })

首先,获取两个环境变量:ENVIROMENTOPTION,这两个环境变量在 package.json 中,通过 cross-env 传入:

...
"scripts": {
  "dev": "cross-env ENVIROMENT=dev webpack-dev-server --open",
  "build": "cross-env ENVIROMENT=prod webpack",
  "build:report": "cross-env OPTION=report npm run build",
  "serve": "http-server ./dist -o"
},
...

使用 cross-env 是为了解决 Windows 和 Linux 下设置环境变量的方式不一致的问题,通过统一的方式设置环境变量。
ENVIROMENT 环境变量用来标识生产或者开发环境,OPTION 环境变量用来进行一些选项的设置,后文进行介绍。
ENVIROMENT 环境变量有两个:devprod,通过这个环境变量决定导入 webpack.config.dev.js 或者 webpack.config.prod.js。
在 webpack.config.dev.js 和 webpack.config.prod.js 文件中,导出的是一个函数而不是一个配置对象,通过函数的方式具有更大的灵活性,可以接收参数,然后根据参数生成不同的配置文件。

基础配置文件

使用 webpack.config.base.js 作为 Webpack 的基础配置文件,包含一些公用的配置,该文件导出的也是一个函数,可以接收环境变量参数。
下面是 webpack.config.base.js 文件的内容:

const CleanWebpackPlugin = require("clean-webpack-plugin")
const VueLoaderPlugin = require("vue-loader/lib/plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin");

const { initConfig,resolve } = require("./bundle")
const { initLoader } = require("./loaders")
const config = {
  devtool: "cheap-module-source-map",
  // 加载器
  module: {
    rules: [],
  },
  resolve:{
    mainFields: ["jsnext:main", "browser", "main"]
  },
  plugins: [
    new CleanWebpackPlugin(["dist"], {
      root: resolve(""),
      verbose: true, //开启在控制台输出信息
      dry: false,
    }),
    new VueLoaderPlugin(),
    new CopyWebpackPlugin([{
            from: resolve("template"),
      to: resolve("dist"),
      ignore:["*.html"]
    }]),
  ],
}

module.exports = function(env){
  const {
    entry,
    output,
    alias,
    htmlPlugins
  } = initConfig(env)
  const loaders = initLoader(env);

  config.entry = entry;
  config.output = output;
  config.resolve.alias = alias;
  config.module.rules.push(...loaders);
  config.plugins.push(...htmlPlugins)

  return config;
}

可见,在 webpack.base.js 中,并没有直接对入口出口以及各种 Loader 以及 Alias 等进行配置,而是从 bundle.js 和 loaders.js 中导入了几个函数,通过为函数传参产生相应的配置,并添加到配置文件上。

拆分配置文件

通过 webpack.base.js 文件可以看到,我把一些配置写到 bundle.js 中了。该文件用来产生 Entry,Output,Alias 和 Plugins 的配置项,这样做的好处,首先是减少了 webpack.base.js 文件的大小,然后通过对配置文件进行拆分,使得后期修改配置文件很方便,不用直接和 webpack.base.js 打交道,只需要调整 bundle.js 中的函数即可,扩展性更好一点。
下面就来看 bundle.js 文件的内容:

const fs = require("fs")
const path = require("path")
const HTMLWebpackPlugin = require("html-webpack-plugin")
const alias = require("./alias")
const resolve = (p) => path.resolve(__dirname,"..",p)

const entryDir = resolve("src/page")
const outputDir = resolve("dist")
const templatePath = resolve("template/index.html")
const entryFiles = fs.readdirSync(entryDir)
const
  entry = {},
  output = {}
  htmlPlugins = [];


// Map alias
function resolveAlias(){
  Object.keys(alias).forEach(attr => {
    const val = alias[attr]
    alias[attr] = resolve(val)
  })
}

// Handle Entry and Output of Webpack
function resolveEntryAndOutput(env){
  entryFiles.forEach(dir => {
    entry[dir] = resolve(`${entryDir}/${dir}`)
    if(env === "dev"){
      output.filename = "js/[name].bundle.js";
    }else{
      output.filename = "js/[name].bundle.[hash].js";
    }
    output.path = outputDir;
  })
}

// Handle HTML Templates
function combineHTMLWithTemplate(){
  entryFiles.forEach(dir => {
    const htmlPlugin = new HTMLWebpackPlugin({
      filename:`${dir}.html`,
      template:templatePath,
      chunks:[dir,"vendor"]
    })
    htmlPlugins.push(htmlPlugin)
  })
}

function initConfig(env){
  resolveAlias();
  resolveEntryAndOutput(env);
  combineHTMLWithTemplate();
  return{
    entry,
    output,
    alias,
    htmlPlugins
  }
}

exports.initConfig = initConfig;
exports.resolve = resolve;

resolve 方法用来解析路径,对 path.resolve 进行了一层包装。
alias 是 Webpack 配置的别名,从 alias.js 中导入,用来提供快捷的文件导入,避免 ../../../../xxx 这样的情况。下面是 alias.js 文件的内容:

const alias = {
  "@component":"src/component",
  "@util":"src/util",
  "@shared":"src/shared",
  "@model":"src/model",
  "@assets":"src/assets",
}

module.exports = alias;

对于入口和出口文件的设置,首先通过 fs.readdirSync 方法读取 src/page 目录下的子目录,每个子目录都是一个页面,然后根据读取到的目录以及环境变量来设置 Webpack 的 Entry 和 Output。设置 Entry 和 Output 需要用到 resolveEntryAndOutput 方法。
对于 html-webpack-plugin 插件的模板,也根据 src/page 下的子目录自动生成。
最后导出一个 initConfig 函数,包含了基础的 Entry,Output,Alias 和 Plugins 配置项。
然后导出了前面定义的 resolve 方法,供其他文件使用。

拆分 Loaders

loaders.js 中包含了各种 Loader,下面是 loaders.js 文件的内容:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { resolve } = require("./bundle")
const vueLoader = {
  test: /\.vue$/,
  use: "vue-loader"
}

const cssLoader = {
  test: /\.css$/,
  exclude: /node_modules/,
  use: [
    "vue-style-loader",
    "css-loader",
    "postcss-loader"
  ]
}

const sassLoader = {
  test: /\.scss$/,
  exclude: /node_modules/,
  use: [
    "vue-style-loader",
    "css-loader",
    "sass-loader",
    "postcss-loader",
    {
      loader: "sass-resources-loader",
      options: {
        resources: resolve("src/assets/style/main.scss"),
      },
    }
  ]
}

const jsLoader = {
  test: /\.js$/,
  exclude: /node_modules/,
  use: ["babel-loader"]
}

const imgLoader = {
  test: /\.(png|svg|jpg|gif)$/,
  use: {
    loader: "file-loader",
    options: {
      name: "[name].[ext]",
      outputPath:"img"
    }
  }
}

const fontLoader = {
  test: /\.(woff|woff2|eot|ttf|otf)$/,
  use: {
    loader: "file-loader",
    options: {
      outputPath:"font"
    }
  }
}

const eslintLoader = {
  test: /\.(js|vue)$/,
  enforce: "pre",
  exclude: /node_modules/,
  loader: "eslint-loader",
  options: {
    fix:true,
    emitWarning:true,
  }
}

exports.initLoader = function(env){
  const loaders = [];
  if(env !== "dev"){
    cssLoader.use = [
      MiniCssExtractPlugin.loader,
      "css-loader",
      "postcss-loader"
    ];

    sassLoader.use = [
      MiniCssExtractPlugin.loader,
      "css-loader",
      "sass-loader",
      "postcss-loader",
      {
        loader: "sass-resources-loader",
        options: {
          resources: resolve("src/assets/style/main.scss"),
        },
      }
    ]
  }else{
    loaders.push(eslintLoader)
  }

  loaders.push(
    vueLoader,
    cssLoader,
    sassLoader,
    jsLoader,
    imgLoader,
    fontLoader
  );

  return loaders;
}

在 Webpack4 中,推荐使用 mini-css-extract-plugin 插件来提取 CSS 文件(老版本使用 extract-text-webpack-plugin 进行提取),在暴露的 initLoader 函数中,对环境变量进行了判断,如果是生产环境,就使用 mini-css-extract-plugin 对 CSS 进行提取。
还有一个 sass-resources-loader,这个 Loader 用来为 SASS 提供全局的变量和函数,Mixins等功能。
对于图片和字体文件,使用 file-loader 将他们提取到 img 和 font 目录。这里我选择 file-loader 而不是 url-loader 的原因是使用 url-loader 会将图片打包成 Base64,插入到 JS 和 CSS 文件中,导致打包后的 JS 和 CSS 文件过大。

Externals 配置

使用 Externals,可以让 Webpack 打包时忽略某些库,直接使用 CDN 上的资源,有效的减轻了打包文件的体积。
Externals 配置放在 externals.js 文件中:

exports.externals = {
  "vue": "Vue",
  "axios": "axios"
}

开发环境配置文件

说完基础的配置文件,再看开发环境的配置文件就简单多了。开发环境的配置文件,主要是在基础配置文件的基础上,对 Webpack Dev Server 和热更新进行配置:

const webpackMerge = require("webpack-merge");
const webpack = require("webpack");
const { resolve } = require("./bundle")
const webpackBaseFn = require("./webpack.config.base");

module.exports = function(env){
  const baseConfig = webpackBaseFn(env)
  return webpackMerge(baseConfig,{
    mode:"development",
    devServer:{
      contentBase:resolve("dist"),
      host:"0.0.0.0",
      useLocalIp: true,
      overlay:{
        errors:true,
        warnings:true
      },
      open:true,
      hot:true,
      historyApiFallback: true,
      inline: true,
      disableHostCheck: true,
      stats:{
        assets: false,
        chunks: false,
        chunkGroups: false,
        chunkModules: false,
        chunkOrigins: false,
        modules: false,
        moduleTrace: false,
        source: false,
        builtAt: false,
        children: false,
        hash:false,
      },
    },
    plugins:[
      //热更新
      new webpack.HotModuleReplacementPlugin(),
    ],
  });
}

下面对 devServer 配置进行一些说明:

  1. host
    host 指定为 "0.0.0.0",就可以通过 IP 地址来访问 Webpack Dev Server 提供的服务了,处于安全问题的考虑,Webpack Dev Server 默认禁止了通过 IP 地址访问服务。
  2. useLocalIp
    该配置项和 open 配置结合在一起使用,将useLocalIp 设置为 true 后,自动打开浏览器时将会通过 IP 地址访问服务,如果不设置 useLocalIp,自动打开浏览器将会打开 0.0.0.0:8080
  3. stat
    该配置项主要对控制台的输出进行一些清理工作,默认的控制台打印的日志很乱,通过对 stat 进行配置后,控制台输出的日志清爽多了。

生产环境配置文件

下面是生产环境的配置文件 webpack.config.prod.js:

const webpackMerge = require("webpack-merge");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

const { resolve } = require("./bundle")
const { externals } = require("./externals")
const webpackBaseFn = require("./webpack.config.base");

module.exports = function(env,{ option }){
  const baseConfig = webpackBaseFn(env)
  const reportOn = option === "report"
  const plugins = [
    new MiniCssExtractPlugin({
      filename: "css/[name].[hash].css",
    }),
  ];
  if(reportOn){
    plugins.push(new BundleAnalyzerPlugin())
  }

  return webpackMerge(baseConfig,{
    mode:"production",
    optimization:{
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendor",
            chunks: "all"
          }
        }
      },
      minimizer: [
        new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              warnings: false,
              drop_debugger: false,
              drop_console: true
            }
          }
        }),
        new OptimizeCSSAssetsPlugin({
          cssProcessorOptions: {
            safe: true
          }
        })
      ]
    },
    stats:{
      chunkGroups: false,
      chunkModules: false,
      chunkOrigins: false,
      modules: false,
      moduleTrace: false,
      source: false,
      children: false,
    },
    externals,
    plugins
  });
}

生产环境的配置文件中,主要对公用的 JS 和 CSS 进行了压缩提取,例外提供了报表功能:当将 OPTION 环境变量设置为 report 时,将会在构建完成后自动打开打包报表分析,可以分析打包中出现的问题,以及各个 Thunk 的大小。
在 Webpack4 中,代码分割和压缩 JS 以及 CSS 的配置放到了 optimization 选项中,splitChunks 用来提供代码分割功能,minimizer 用来提供压缩功能。

package.json

下面是 package.json 文件的内容:

{
  "name": "CHARLEY_MUTLI_KIT",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "cross-env ENVIROMENT=dev webpack-dev-server",
    "build": "cross-env ENVIROMENT=prod webpack",
    "build:report": "cross-env OPTION=report npm run build",
    "serve": "http-server ./dist -o"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^8.6.4",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.4",
    "babel-plugin-transform-async-to-generator": "^6.24.1",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.7.0",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.5.2",
    "cross-env": "^5.2.0",
    "css-loader": "^0.28.11",
    "eslint": "^5.0.1",
    "eslint-loader": "^2.0.0",
    "eslint-plugin-vue": "^4.5.0",
    "file-loader": "^1.1.11",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "html-webpack-plugin": "^3.2.0",
    "http-server": "^0.11.1",
    "mini-css-extract-plugin": "^0.4.1",
    "node-sass": "^4.9.0",
    "optimize-css-assets-webpack-plugin": "^4.0.3",
    "postcss-loader": "^2.1.5",
    "sass-loader": "^7.0.3",
    "sass-resources-loader": "^1.3.3",
    "style-loader": "^0.21.0",
    "uglifyjs-webpack-plugin": "^1.2.7",
    "url-loader": "^1.0.1",
    "vue-loader": "^15.2.4",
    "vue-style-loader": "^4.1.0",
    "vue-template-compiler": "^2.5.16",
    "webpack": "^4.14.0",
    "webpack-bundle-analyzer": "^2.13.1",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4",
    "webpack-merge": "^4.1.3"
  },
  "dependencies": {
    "axios": "^0.18.0",
    "vue": "^2.5.16"
  }
}

配置总结

本次在 Webpack 的配置中,我对配置文件中常用的功能进行了提取,将改动频率大的配置项单独出来,方便了扩展和维护,功能也更加清晰。此外,将 Node 和 Webpack 的配置结合的更加紧密,有了很大的灵活性,如果以后想做一个 React 的脚手架,只需要修改下 loaders.js 即可,不用动 Webpack 配置文件的主体。
Webpack 的配置部分就到此结束,下篇文章我给大家介绍下项目中的模块划分。

本项目 GitHub 仓库地址为:https://github.com/charleylla/charley-vue-multi,如果您觉得我的文章对您有帮助,欢迎帮我点个 Star,如果您有问题,欢迎提 Issue~

完。

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

推荐阅读更多精彩内容