Webpack 多环境代码打包(测试、预发、正式环境)

在 package.json 文件的 scripts 中,会提供开发环境与生产环境两个命令。但是实际使用中会遇见 测试版、预发布版以及正式版代码相继发布的情况,这样反复更改服务器地址,偶尔忘记更改 url 会给工作带来很多不必要的麻烦。这样就需要在生产环境中配置 测试版本打包命令、预发布版本打包命令与正式版本打包命令。

具体步骤如下:

1. Package.json 文件中 增加命令行命令,并指定路径。

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",  //开发环境打包命令
    "start": "npm run dev",
    "test": "node build/build-test.js", //测试环境打包命令
    "pre": "node build/build-pre.js",  //预发布环境打包命令
    "build": "node build/build.js", //正式环境打包命令
  },

2. 在 build 文件中添加相应文件

image.png

build-test.js

/**
 * 测试版
 */

'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production-test'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))

    console.log(chalk.yellow(
        '  Tip: built files are meant to be served over an HTTP server.\n' +
        ' '+ process.env.NODE_ENV
      ))
  })
})

build-pre.js

/**
 * 预发布版
 */

'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production-pre'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))

    console.log(chalk.yellow(
        '  Tip: built files are meant to be served over an HTTP server.\n' +
        ' '+ process.env.NODE_ENV
      ))
  })
})

build.js

/**
 * 正式版
 */
require('./check-versions')()

process.env.NODE_ENV = 'production'

var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')

var spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, function (err, stats) {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      ' '+ process.env.NODE_ENV
    ))

     //gulp 正式站 生成 zip文件,
     var fs = require('fs');
     function getPackageJsonVersion () {
       // 这里我们直接解析 json 文件而不是使用 require,这是因为 require 会缓存多次调用,这会导致版本号不会被更新掉
       return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
     };
     var gulp = require('gulp');
     var zip = require('gulp-zip');
     var  filename=(new Date()).toLocaleString().replace(/:/g,'');
     console.log(chalk.yellow(
       '  Tip: zipName.\n' +
       ' '+ filename +getPackageJsonVersion()
     ))
     //创建一个文件到V.txt 到 dist 目录

     fs.writeFile('./dist/ver.txt','版本号:'+getPackageJsonVersion(),function(err){  
      if(err)  
          return console.error(err);  
      console.log('写入文件成功');  
     });  


     gulp.task('zip', function() {
              gulp.src(['dist/**','README.md'])
             .pipe(zip(`Philips_production_${filename}_v${getPackageJsonVersion()}.zip`))
             .pipe(gulp.dest('dist1'));
     });
    //  gulp.run('zip')
  })
})

3、在 config 文件中增加环境变量配置

image.png

prod-test.env.js 增加环境变量

/**
 * 测试环境配置
 */

module.exports = {
    NODE_ENV: '"production-test"'
}

prod-pre.env.js 增加环境变量

/**
 * 预发环境配置
 */

module.exports = {
    NODE_ENV: '"production-pre"'
}

prod.env.js

/**
 * 生产环境配置(正式)
 */

module.exports = {
    NODE_ENV: '"production"'
}

index.js

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {  // dev开发 环境
    evn:require('./dev.env'),
    // 静态资源文件夹
    assetsSubDirectory: 'static',
    // 发布路径
    assetsPublicPath: '/',
    //配置代理(可跨域)
    proxyTable: {
      '/api': {
        target: "http://sfe.crmclick.com",// 接口的域名
        // secure: false,  // 如果是https接口,需要配置这个参数
        changeOrigin: true,// 如果接口跨域,需要进行这个参数配置
        pathRewrite: {
          '^/api': '/'
        }
      },
      // 注意: '/api' 为匹配项,target 为被请求的地址,因为在 ajax 的 url 中加了前缀 '/api',而原本的接口是没有这个前缀的,
      //所以需要通过 pathRewrite 来重写地址,将前缀 '/api' 转为 '/'。如果本身的接口地址就有 '/api' 这种通用前缀,就可以把 pathRewrite 删掉。
    },

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-


    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: { // 测试 预发布 正式生产 环境
    evn:process.env.NODE_ENV == "production"
    ? require('./prod.env')
    : process.env.NODE_ENV == "production-pre"
    ? require('./prod-pre.env')
    : process.env.NODE_ENV == "production-test"
    ? require('./prod-test.env')
    : require('./prod-test.env'), // 使用 config/prod.env.js 中定义的编译环境
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'), // 编译输入的 index.html 文件

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'), // 编译输出的静态资源路径
    assetsSubDirectory: 'static', // 编译输出的二级目录
    assetsPublicPath: '/', // 编译发布的根目录,可配置为资源服务器域名或 CDN 域名

    /**
     * Source Maps
     */

    productionSourceMap: false, // 是否开启 cssSourceMap
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false, // 是否开启 gzip
    productionGzipExtensions: ['js', 'css'], // 需要使用 gzip 压缩的文件扩展名

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

4. 修改 build 文件夹下的 webpack.prod.conf.js

image.png

webpack.prod.conf.js

"use strict";
const path = require("path");
const utils = require("./utils");
const webpack = require("webpack");
const config = require("../config");
const merge = require("webpack-merge");
const baseWebpackConfig = require("./webpack.base.conf");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");

// const env = require('../config/prod.env')

console.log('------------------+'+process.env.NODE_ENV)

const env =
  process.env.NODE_ENV === "production"
    ? require("../config/prod.env")
    : process.env.NODE_ENV === "production-test"
    ? require("../config/prod-test.env")
    : process.env.NODE_ENV === "production-pre"
    ? require("../config/prod-pre.env")
    : require("../config/dev.env");

const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath("js/[name].[chunkhash].js"),
    chunkFilename: utils.assetsPath("js/[id].[chunkhash].js")
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      "process.env": env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath("css/[name].[contenthash].css"),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: "index.html",
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: "dependency"
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks(module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(path.join(__dirname, "../node_modules")) === 0
        );
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: "manifest",
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: "app",
      async: "vendor-async",
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, "../static"),
        to: config.build.assetsSubDirectory,
        ignore: [".*"]
      }
    ])
  ]
});

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require("compression-webpack-plugin");

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: "[path].gz[query]",
      algorithm: "gzip",
      test: new RegExp(
        "\\.(" + config.build.productionGzipExtensions.join("|") + ")$"
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  );
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
    .BundleAnalyzerPlugin;
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}

module.exports = webpackConfig;

5. 修改 build 文件夹下的 webpack.base.conf.js

webpack.base.conf.js

"use strict";
const path = require("path");
const utils = require("./utils");
const config = require("../config");
const vueLoaderConfig = require("./vue-loader.conf");

function resolve(dir) {
  return path.join(__dirname, "..", dir);
}

module.exports = {
  context: path.resolve(__dirname, "../"),
  entry: {
    app: "./src/main.js"
  },
  output: {
    path: config.build.assetsRoot,
    filename: "[name].js",
  publicPath:
     process.env.NODE_ENV === "development"
     ? config.dev.assetsPublicPath
     : config.build.assetsPublicPath
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue$: "vue/dist/vue.esm.js",
      "@": resolve("src")
    }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,
        loader: "babel-loader",
        include: [
          resolve("src"),
          resolve("test"),
          resolve("node_modules/webpack-dev-server/client")
        ]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: "url-loader",
        options: {
          limit: 10000,
          name: utils.assetsPath("img/[name].[hash:7].[ext]")
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: "url-loader",
        options: {
          limit: 10000,
          name: utils.assetsPath("media/[name].[hash:7].[ext]")
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: "url-loader",
        options: {
          limit: 10000,
          name: utils.assetsPath("fonts/[name].[hash:7].[ext]")
        }
      },
      {
        test: /\.less$/,
        loader: "style-loader!css-loader!less-loader"
      }
    ]
  },
  node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: "empty",
    fs: "empty",
    net: "empty",
    tls: "empty",
    child_process: "empty"
  }
};

6. 创建一个公用的页面,用来增加环境变量判断 以及 对 axios 进行配置 & 拦截

untils/config.js

import axios from 'axios';
import qs from 'qs';
import VueRouter from 'vue-router';
import routers from '../router';

const router = new VueRouter({
    routers
});

// 设置接口地址
let baseUrl = '';
// 上传图片文件路径
let uploadBaseUrl = '';
// alert(process.env.NODE_ENV)
if (process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'production-test') {
    // 开发/测试 环境
    baseUrl = 'https://sfe.crmclick.com';
    uploadBaseUrl = 'http://sfe.crmclick.com/uploadNoZip.aspx';
}else if(process.env.NODE_ENV == 'production-pre'){
    // 预发 环境
    baseUrl = 'xxxx';
    uploadBaseUrl = 'xxxx';
}else if(process.env.NODE_ENV == 'production'){
    // 正式环境
    baseUrl = 'xxxx';
    uploadBaseUrl = 'xxxx';
}

// axios 配置 & 拦截
// 响应时间
axios.defaults.timeout = 20000;
// 是否允许携带cookie
// withCredentials为true的情况下,后端要设置Access-Control-Allow-Origin为你的源地址,例如http://localhost:8080,不能是*,
// 而且还要设置header('Access-Control-Allow-Credentials: true');              
axios.defaults.withCredentials = false;
// 配置请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => {
    //在发送请求之前强制降低复杂请求
    if(config.method  === 'post'){
        config.data = qs.stringify(config.data);
    }
    return config;
},(error) =>{
    return Promise.reject(error);
});
// 返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) =>{
    //对响应数据做些事
    if(!res.data){
        return Promise.reject(res);
    }

    if(res.data.respCode == '111'){
        // 登陆 过期
        sessionStorage.removeItem('User');
        alert("登陆已失效,请重新登陆");
        router.go('/');
        return
    }
    return res;
}, (error) => {
    return Promise.reject(error);
});

export {
    baseUrl,
    uploadBaseUrl,
}

7、在页面 import baseUrl axios 等 即可针对不同环境进行接口调用

<script>
import axios from 'axios';
import { baseUrl } from '../untils/config';
import HeaderMenu from './common/HeaderMenu';//引用子组件

export default {
    name: 'Gift',
    data() {
        return {
            title:"礼品展示",
            PageIndex:1,
            PageSize:10,
            GiftList:[],
            loading:false,
            LoadMore:true,
        }
    },
    components: {
        "V-Menu": HeaderMenu,
    },
    created() {
        this.GetGiftList();
    },
    mounted() {

    },
    methods: {
        GetImg(url){
            return baseUrl + url;
        },
        GetGiftList() {
            axios.post(baseUrl+"/suzhou/Handler/Points.ashx",{
                op: "GiftList", 
                PageIndex: this.PageIndex, 
                PageSize:this.PageSize
            }).then(res => {
                if (res.data.state == 1) {
                    if(this.PageIndex==1){
                        this.GiftList = res.data.rows;
                    }else{
                        var newGiftArr = this.GiftList.slice(0);
                        if(res.data.rows.length>0){
                            for(var i=0;i<res.data.rows.length;i++){
                                newGiftArr.push(res.data.rows[i]);
                            }
                        }
                        this.GiftList = newGiftArr;
                    }
                    if(res.data.rows.length < this.PageSize){
                        this.LoadMore = false;
                    }
                }else if(res.data.rows == -1){
                    this.$toast.warning(res.data.msg);
                }
            })
            .catch(error => {
                debugger
                this.$toast.error(error);
            });
        },
        load:function(){
            if(this.LoadMore){
                this.loading = true;
                setTimeout(function() {
                    this.loading = false;
                    this.PageIndex ++;
                    this.GetGiftList();
                }, 2000)
            }
        },
    }
}
</script>

8、打包命令如下

npm run dev      开发环境
npm run test     测试环境
npm run pre      预发环境
npm run build    正式环境

点赞加关注,永远不迷路

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

推荐阅读更多精彩内容