享惯了vue-cli2.0的自由那么撸一个react-cli吧

你是否对如何构建一个react应用得心应手呢

create-react-app MyReactApp

npm run start

噜噜啦 多简单;

but 如果我要改一些配置呢?too young, too simple 能难倒我?

npm run eject  反编译create-react-app的config到项目中

npm install react-app-rewired --save 了解下?

好好好,但是您能不能自己写一个呢?

。。。ののの

其实代码我2018年3月份就写好了,之后每天忙于写业务代码(-。-;)

package.json

{

  "name": "react-cli",

  "version": "0.0.1",

  "description": "React 16.0 boilerplate with react-router-dom, redux & webpack 4.x",

  "scripts": {

    "start": "webpack-dev-server --config build/webpack.dev.conf.js --progress --watch --colors --profile ",

    "build": "webpack -p --mode production --progress --config ./build/webpack.prod.conf.js"

  },

  "devDependencies": {

    "@babel/core": "^7.0.0",

    "@babel/plugin-syntax-dynamic-import": "^7.0.0",

    "@babel/plugin-syntax-object-rest-spread": "^7.0.0",

    "@babel/plugin-transform-runtime": "^7.0.0",

    "@babel/preset-env": "^7.0.0",

    "@babel/preset-react": "^7.0.0",

    "@babel/runtime": "^7.0.0",

    "@commitlint/cli": "^7.0.0",

    "@commitlint/config-conventional": "^7.0.1",

    "autoprefixer": "^9.0.0",

    "babel-core": "^6.26.3",

    "babel-eslint": "^8.2.6",

    "babel-loader": "^8.0.2",

    "babel-plugin-dynamic-import-node": "^2.2.0",

    "babel-plugin-import": "^1.8.0",

    "babel-plugin-module-resolver": "^3.1.1",

    "clean-webpack-plugin": "^0.1.19",

    "compression-webpack-plugin": "^1.1.11",

    "copy-webpack-plugin": "^4.5.2",

    "cross-env": "^5.2.0",

    "css-loader": "^1.0.0",

    "file-loader": "^2.0.0",

    "friendly-errors-webpack-plugin": "^1.7.0",

    "highcharts": "^6.1.2",

    "highcharts-react-official": "highcharts/highcharts-react",

    "html-webpack-plugin": "^3.2.0",

    "husky": "^0.14.3",

    "jest": "^23.4.1",

    "less": "^3.8.1",

    "less-loader": "^4.1.0",

    "lint-staged": "^7.2.2",

    "mini-css-extract-plugin": "^0.4.1",

    "node-notifier": "^5.2.1",

    "node-ssh": "^5.1.2",

    "portfinder": "^1.0.20",

    "postcss-import": "^11.1.0",

    "postcss-loader": "^2.1.6",

    "postcss-safe-parser": "^4.0.1",

    "prettier": "^1.14.2",

    "source-map-loader": "^0.2.3",

    "style-loader": "^0.21.0",

    "styled-components": "^3.4.5",

    "url-loader": "^1.0.1",

    "webpack": "^4.16.1",

    "webpack-bundle-analyzer": "^2.13.1",

    "webpack-cli": "^3.1.0",

    "webpack-dev-server": "^3.1.4",

    "webpack-merge": "^4.2.1"

  },

  "dependencies": {

    "@babel/plugin-proposal-class-properties": "^7.4.0",

    "@babel/plugin-proposal-decorators": "^7.4.0",

    "@babel/plugin-proposal-do-expressions": "^7.2.0",

    "@babel/plugin-proposal-export-default-from": "^7.2.0",

    "@babel/plugin-proposal-export-namespace-from": "^7.2.0",

    "@babel/plugin-proposal-function-bind": "^7.2.0",

    "@babel/plugin-proposal-function-sent": "^7.2.0",

    "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",

    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0",

    "@babel/plugin-proposal-numeric-separator": "^7.2.0",

    "@babel/plugin-proposal-optional-chaining": "^7.2.0",

    "@babel/plugin-proposal-pipeline-operator": "^7.3.2",

    "@babel/plugin-proposal-throw-expressions": "^7.2.0",

    "@babel/plugin-syntax-import-meta": "^7.2.0",

    "antd": "^3.7.0",

    "axios": "^0.18.0",

    "babel-polyfill": "^6.26.0",

    "bignumber": "^1.1.0",

    "debug": "^4.1.1",

    "import-local": "^2.0.0",

    "js-base64": "^2.5.1",

    "js-cookie": "^2.2.0",

    "jsencrypt": "^3.0.0-rc.1",

    "memoize-one": "^5.0.1",

    "node-sass": "^4.11.0",

    "nzh": "^1.0.4",

    "pnp-webpack-plugin": "^1.4.1",

    "progress-bar-webpack-plugin": "^1.12.1",

    "react": "^16.4.1",

    "react-dom": "^16.4.1",

    "react-loadable": "^5.5.0",

    "react-redux": "^5.0.7",

    "react-router-config": "^5.1.1",

    "react-router-dom": "^4.3.1",

    "react-table": "^6.9.2",

    "react-transition-group": "^4.4.1",

    "redux": "^4.0.0",

    "redux-devtools-extension": "^2.13.8",

    "redux-logger": "^3.0.6",

    "redux-thunk": "^2.3.0",

    "sass-loader": "^7.1.0",

    "yargs": "^13.2.2"

  }

}

.babelrc

{

  "presets": [

    "@babel/preset-env",

    "@babel/preset-react"

  ],

  "plugins": [

    "@babel/plugin-syntax-dynamic-import",

    "@babel/plugin-transform-runtime",

    "dynamic-import-node",

    [

      "@babel/plugin-proposal-decorators",

      {

        "legacy": true

      }

    ],

    "@babel/plugin-proposal-class-properties",

    "@babel/plugin-syntax-import-meta",

    "@babel/plugin-proposal-json-strings",

    "@babel/plugin-proposal-function-sent",

    "@babel/plugin-proposal-export-namespace-from",

    "@babel/plugin-proposal-numeric-separator",

    "@babel/plugin-proposal-throw-expressions",

    "@babel/plugin-proposal-export-default-from",

    "@babel/plugin-proposal-logical-assignment-operators",

    "@babel/plugin-proposal-optional-chaining",

    [

      "@babel/plugin-proposal-pipeline-operator",

      {

        "proposal": "minimal"

      }

    ],

    "@babel/plugin-proposal-nullish-coalescing-operator",

    "@babel/plugin-proposal-do-expressions",

    "@babel/plugin-proposal-function-bind",

  ]

}

webpack.base.conf.js

'use strict'

const path = require('path');

const PnpWebpackPlugin = require('pnp-webpack-plugin');

function resolve (dir) {

  return path.join(__dirname, '..', dir)

}



module.exports = {

  context: path.resolve(__dirname, '../'),

  entry: {

    index: ['babel-polyfill','./src/index.js']

  },

  output: {

    path: path.resolve(__dirname, '../dist'),

    filename: 'app.[hash:8].js',

    publicPath: '/',

    libraryTarget: 'umd',

  },

  

  resolve: {

    // 自动解析文件扩展名(补全文件后缀)(从左->右)

    extensions: ['.js', '.jsx', '.json'],

    alias: {

      '@': resolve('src')

    },

    plugins: [

      // 正确解析程序所需的依赖的插件

      PnpWebpackPlugin,

    ],

  },

  resolveLoader: {

    plugins: [

      // 也与插件‘n’Play有关,但这一次它告诉Webpack从当前的包中加载其加载程序

      PnpWebpackPlugin.moduleLoader(module),

    ],

  },

  module: {

    rules: [

      {

        test: /\.(js|jsx)$/,

        exclude: /node_modules/,

        enforce: 'pre',

        use: [{

          loader: 'babel-loader',

        }]

      },

      {

        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,

        loader: 'url-loader',

        options: {

          // 不写fallback,file size 大于limit时,会自动调用file-loader,但是使用的是默认的[hash].[ext]无法指定路径名和文件名

          // 显示声明 fallback之后,就可以将name传至file-loader。

          // 因为使用了CopyWebpackPlugin,防止诸如[name][hash].[ext]和[name].[ext]文件同时存在

          // 特将文件名指定为[name].[ext]

          fallback: 'file-loader',

          name: 'assets/img/[name].[ext]',

          limit: 10000,

        }

      },

      {

        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,

        loader: 'url-loader',

        options: {

          fallback: 'file-loader',

          limit: 10000,

          name: 'assets/media/[name].[ext]',

        }

      },

      {

        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,

        loader: 'url-loader',

        options: {

          fallback: 'file-loader',

          limit: 10000,

          name: 'assets/fonts/[name].[ext]',

        }

      }  

    ]

  },

  node: {

    module: 'empty',

    dgram: 'empty',

    dns: 'mock',

    fs: 'empty',

    net: 'empty',

    tls: 'empty',

    child_process: 'empty',

  },

  performance: false,

}

webpack.dev.conf.js

const utils = require('./utils')

const webpack = require('webpack')

const merge = require('webpack-merge')

const path = require('path')

const baseWebpackConfig = require('./webpack.base.conf')

const CopyWebpackPlugin = require('copy-webpack-plugin')

const HtmlWebpackPlugin = require('html-webpack-plugin')

const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

const portfinder = require('portfinder')

const HOST = process.env.HOST

const PORT = (process.env.PORT && Number(process.env.PORT)) || '8080'



const devWebpackConfig = merge(baseWebpackConfig, {

  mode: 'development',

  optimization: {

    minimize: false,

    minimizer: [],

    splitChunks: {

      chunks: 'all',

      name:'vendor'

    },

  },

  module: {

    //合并styleloader

    rules: utils.styleLoaders({ sourceMap: true, usePostCSS: false, extract: false, })

  },

  // 源错误检查

  devtool: 'eval-source-map',

  devServer: {

    clientLogLevel: 'warning',

    //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

    historyApiFallback: true, 

    contentBase: path.resolve(__dirname, '../src'),

    compress: true,

    // 热加载

    hot: true, 

    //自动刷新

    inline: true, 

    //自动打开浏览器

    open: false, 

    host: HOST||'localhost',

    port: PORT,

    // 在浏览器上全屏显示编译的errors或warnings。

    overlay: { warnings: false, errors: true }, 

    publicPath: '/',

    // 终端输出的只有初始启动信息。webpack 的警告和错误是不输出到终端的

    proxy: {},

    quiet: true, 

    // 通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。默认为false

    watchOptions: {

      poll: false

    }

  },

  plugins: [

    new webpack.DefinePlugin({

      ...process.env

    }),

    //开启HMR(热替换功能,替换更新部分,不重载页面!)

    new webpack.HotModuleReplacementPlugin(),

    //显示模块相对路径

    // new webpack.NamedModulesPlugin(),

    //不显示错误信息

    new webpack.NoEmitOnErrorsPlugin(),

    // https://github.com/ampedandwired/html-webpack-plugin

    //配置html入口信息

    new HtmlWebpackPlugin({

      filename: 'index.html',

      template: path.resolve(__dirname, '../index.html'),

      chunksSortMode: 'none',

      inject: true

    }),

    new CopyWebpackPlugin([{

      from: path.resolve(__dirname, '../static'),

      to: 'static',

      ignore: ['.*']

    }]),

  ]

})

module.exports = new Promise((resolve, reject) => {

    // 获取当前设定的端口

  portfinder.basePort = PORT

  portfinder.getPort((err, port) => {

    if (err) {

      reject(err)

    } else {

      // 发布新的端口,对于e2e测试

      process.env.PORT = port

      devWebpackConfig.devServer.port = port

      // 友好的报错插件

      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({

        compilationSuccessInfo: {

          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],

        },

        onErrors: utils.createNotifierCallback(),

      }))

      resolve(devWebpackConfig)

    }

  })
})

webpack.prod.conf.js

'use strict'

const path = require('path')

const utils = require('./utils')

const webpack = require('webpack')

const merge = require('webpack-merge')

const baseWebpackConfig = require('./webpack.base.conf')

const HtmlWebpackPlugin = require('html-webpack-plugin')

const CleanWebpackPlugin = require('clean-webpack-plugin');

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const BundleAnalyzerPlugin = process.env.NODE_ENV=== 'analysis' ? require('webpack-bundle-analyzer').BundleAnalyzerPlugin:null

const CopyWebpackPlugin = require('copy-webpack-plugin')



const env = process.env.NODE_ENV === 'testing'

  ? {NODE_ENV: '"testing"'}

  : {NODE_ENV: '"production"'}



const webpackConfig = merge(baseWebpackConfig, {

  mode: 'production',

  module: {

    //并入style-loader

    rules: utils.styleLoaders({

      sourceMap: false,

      extract: true,

      usePostCSS: true

    })

  },

  output: {

    path: path.resolve(__dirname, '../dist'),

    filename: ('js/[name].[hash:8].js'),

    chunkFilename: ('js/[name]-[id].[hash:8].js')

  },

  //4.0配置  重点

  optimization: {

    //它的作用是将包含chunks 映射关系的 list单独从 app.js里提取出来,

    //因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,

    //所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。

    runtimeChunk: {

      name: "manifest"

    },

    splitChunks: {

      chunks: 'all'

    }

    // splitChunks: {

    //   //默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk._modules为gaichunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css

    //   chunks: "async”,

    //   //默认值是30kb

    //   minSize: 30000,

    //   //被多少模块共享  

    //   minChunks: 1,

    //   //所有异步请求不得超过5个  

    //   maxAsyncRequests: 5,

    //   //初始化并行请求不得超过3个  

    //   maxInitialRequests: 3,

    //   //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~  

    //   name: true,

    //   //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例  

    //   cacheGroups: { 

    //     common: {

    //       //抽取的chunk的名字

    //       name: 'common',

    //       //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取  

    //       chunks(chunk) { 

    //       },

    //       //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。

    //       test(module, chunks) {  

    //       },

    //     //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中

    //     priority: 10,  

    //     //最少被几个chunk引用

    //     minChunks: 2,  

    //     //   如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码

    //     reuseExistingChunk: true,

    //     // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize

    //     enforce: true  

    //     }

    //   }

    // }

  },

  plugins: [

    //清除目录

    new CleanWebpackPlugin(path.resolve(__dirname, '../dist/*'), {

      root: path.resolve(__dirname, '../'),

      verbose: true,

      dry: false

    }),

    //允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用

    new webpack.DefinePlugin({

      'process.env': env

    }),

    //压缩css

    new MiniCssExtractPlugin({

      filename: 'css/[name].[hash:8].css',

      chunkFilename: 'css/[name]-[id].[hash:8].css',

    }),

    new HtmlWebpackPlugin({

      filename: 'index.html',

      template: path.resolve(__dirname, '../index.html'),

      title: 'React Demo',

      inject: true, // true->'head' || false->'body'

      minify: {

        //删除Html注释

        removeComments: true,

        //去除空格

        collapseWhitespace: true,

        //去除属性引号

        removeAttributeQuotes: true

      },

      chunksSortMode: 'dependency'

    }),

    // 该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境。

    new webpack.HashedModuleIdsPlugin(),

    // 这个插件会在 webpack 中实现以上的预编译功能 提升你的代码在浏览器中的执行速度。

    new webpack.optimize.ModuleConcatenationPlugin(),

    //把静态资源copy

    new CopyWebpackPlugin([{

      from: path.resolve(__dirname, '../static'),

      to: '/static',

      ignore: ['.*']

    }]),

    process.env.NODE_ENV=== 'analysis' ? new BundleAnalyzerPlugin() : ()=>{}

  ]

})



// 需要服务端做相关的gzip配置

/*

gzip on;

gzip_disable "msie6";

gzip_buffers 32 4k;

gzip_static on;

 */

// 页面请求后的Response Headers中的Content-Encoding的值为“gzip”,Request Headers中Accept-Encoding的值存在“gzip”值

if (true) {

  const CompressionWebpackPlugin = require('compression-webpack-plugin')



  webpackConfig.plugins.push(

    new CompressionWebpackPlugin({

      asset: '[path].gz[query]',

      algorithm: 'gzip',

      test: /\.(js|css)$/,

      threshold: 10240,

      minRatio: 0.8

    })
  )
}

module.exports = webpackConfig

utils.js

'use strict'

const path = require('path')

const autoprefixer = require('autoprefixer');

const packageConfig = require('../package.json')

const MiniCssExtractPlugin = require('mini-css-extract-plugin');





exports.cssLoaders = function (options) {

  options = options || {}

  // options是用来传递参数给loader的

  // minimize表示压缩,如果是生产环境就压缩css代码

  const cssLoader = {

    loader: 'css-loader',

    options: {

      sourceMap: options.sourceMap,

      importLoaders: 2,

      minimize: options.extract ? true : false,

    }

  }

  //postcss-loader

  const postcssLoader = {

    loader: 'postcss-loader',

    options: {

      sourceMap: options.sourceMap,

      plugins: () => [autoprefixer({

        browsers: 'last 5 versions'

      })],

    }

  }





  function generateLoaders (loader, loaderOptions) {

    // 将上面的基础cssLoader配置放在一个数组里面

    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    // 如果该函数传递了单独的loader就加到这个loaders数组里面,这个loader可能是less,sass之类的

    if (loader) {

      loaders.push({

        // 加载对应的loader

        loader: loader + '-loader',

        // Object.assign是es6的方法,主要用来合并对象的,浅拷贝

        options: Object.assign({}, loaderOptions, {

          sourceMap: options.sourceMap

        })

      })

    }

    // 注意这个extract是自定义的属性,

    //可以定义在options里面,主要作用就是当配置为true就把文件单独提取,false表示不单独提取,

    if (options.extract) {

      return [ MiniCssExtractPlugin.loader ].concat(loaders)

    } else {

      return ['style-loader'].concat(loaders)

    }

  }



  return {

    css: generateLoaders(),

    postcss: generateLoaders(),

    less: generateLoaders('less', {

        modifyVars: {

            "@primary-color": "#075DA5",

            "@font-size-base": "12px",

            '@menu-dark-bg':'#283142'

        },

        javascriptEnabled: true,

    }),

    sass: generateLoaders('sass', { indentedSyntax: true }),

    scss: generateLoaders('sass'),

    stylus: generateLoaders('stylus'),

    styl: generateLoaders('stylus')

  }

}



// 下面这个主要处理import这种方式导入的文件类型的打包,上面的exports.cssLoaders是为这一步服务的

exports.styleLoaders = function (options) {

  const output = []

  const loaders = exports.cssLoaders(options)

  // 下面就是生成的各种css文件的loader对象

  for (const extension in loaders) {

    // 把每一种文件的laoder都提取出来

    const loader = loaders[extension]

    // 把最终的结果都push到output数组中,大事搞定

    output.push({

      test: new RegExp('\\.' + extension + '$'),

      // exclude: /node_modules/,

      use: loader

    });

    // 处理node_modules中的样式问题

    // if (options.extract === ) {

    //   output.push({

    //     test: new RegExp('\\.' + extension + '$'),

    //     include: /node_modules/,

    //     use: loader

    //   })

    // }

  }

  // console.log(output)

  return output

}



exports.createNotifierCallback = () => {

  const notifier = require('node-notifier')



  return (severity, errors) => {

    if (severity !== 'error') return

    const error = errors[0]

    const filename = error.file && error.file.split('!').pop()

    notifier.notify({

      title: packageConfig.name,

      message: severity + ': ' + error.name,

      subtitle: filename || '',

      icon: path.join(__dirname, 'logo.png')

    })
  }
}

至此一个基于webpack4+bable7的react脚手架就搭建完成了。

这样的一个脚手架是不是维护起来特别方便和简单呢?

附上GitHub地址

现在是webpack5了, 后面有时间给改成webpack5

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

推荐阅读更多精彩内容