11-脚手架create react app源码分析(1)

最近抽时间阅读了下 create-react-app 源码,里面所使用到的有用的插件,会不断分析扩展到项目中。当然源码内容过多,可能会摘选出基于自身考虑需要优化的内容加以介绍。

准备

我们可以通过yarn create react-app helloreact创建基于 create-react-app 的脚手架工程,然后执行yarn eject命令将原有的已经封装好的配置暴露出来,之后我们就可以便利的阅读及扩展了,不过需要注意的是这个命令是不可逆的,一旦执行后就不能恢复原状态了。大致的项目结构如下所示:

paths.js分析

而对于所有项目中的关键路径,create-react-app 都全部抽象到 paths.js 中统一定义,例如包括srcpackage.json等文件的路径等。并且,通过以下代码实现了正确解析所有的相对路径:

// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

通过fs.realpathSync(process.cwd())获取到当前nodejs执行的工作目录后,就能够在任意层级的配置中解析项目对应的相对路径了。如果我们需要实现类似 create-react-app 这种动态配置,可以采用这种方式来实现。

env.js分析

我们在实际开发中,会使用到类似远程数据库访问用户名,密码或者部署容器的用户名,密码等敏感信息,这些信息暴露出去以后是相当危险的,并且对于这些类似的信息我们可能不同环境需要指定不同的值,例如开发环境和产品环境的数据库配置肯定是不同的。如此我们需要能够根据环境不同,能够自定义需要的变量,该文件即实现了此功能。

以下列表展示了实现加载环境变量的主要插件:

  • dotenv 加载指定的 env*类的环境定义到 nodejsprocess.env 环境变量中。
  • dotenv-expand 使dotenv可以定义变量。使用方法如下所示,最终获取到的 BASE_URL 变量值为 BASE_URL: 'http://127.0.0.1:8080/'
PORT=8080
IP=127.0.0.1
BASE_URL = http://${IP}:${PORT}/

如下代码指定了对应环境变量所加载的定义文件及顺序。NODE_ENV 变量需要我们根据不同环境指定,例如开发development产品production测试test[].filter(Boolean)是移除所有的 false 类型元素 (false, null, undefined, 0, NaN or an empty string) 的一个简写方式。

var dotenvFiles = [
  `${paths.dotenv}.${NODE_ENV}.local`,
  `${paths.dotenv}.${NODE_ENV}`,
  // Don't include `.env.local` for `test` environment
  // since normally you expect tests to produce the same
  // results for everyone
  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
  paths.dotenv,
].filter(Boolean);

根据不同环境,循环按序加载环境变量定义文件。

dotenvFiles.forEach(dotenvFile => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv-expand')(
      require('dotenv').config({
        path: dotenvFile,
      })
    );
  }
});

支持基于 NODE_PATH 来解析程序模块,将 NODE_PATH 里定义的相对路径,转换为基于应用程序的绝对路径。如何项目想使用非标准布局,可以考虑使用 NODE_PATH 来解析。

const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
  .split(path.delimiter)
  .filter(folder => folder && !path.isAbsolute(folder))
  .map(folder => path.resolve(appDirectory, folder))
  .join(path.delimiter);

过滤出 REACT_APP_ 开头的环境变量后,与 NODE_ENVPUBLIC_URL 一起提供给 WebpackDefinePlugin。应用程序中可以随意通过process.env.REACT_APP_*的方式使用定义的变量。

const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter(key => REACT_APP.test(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        // Useful for determining whether we’re running in production mode.
        // Most importantly, it switches React into the correct mode.
        NODE_ENV: process.env.NODE_ENV || 'development',
        // Useful for resolving the correct path to static assets in `public`.
        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
        // This should only be used as an escape hatch. Normally you would put
        // images into the `src` and `import` them in code to get their paths.
        PUBLIC_URL: publicUrl,
      }
    );
  // Stringify all values so we can feed into Webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
  };

  return { raw, stringified };
}

webpack.config.js分析

看文件名就可以知道,肯定和 Webpack 打包有关,create-react-app中是通过定义一个 Webpack 工厂函数来实现开发和产品环境区分的,通过之间传入对应的环境参数不同,生成不同环境的打包配置。

module.exports = function(webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';
  ...
}

getStyleLoaders 函数定义了处理 Css 所需要的 loaders。

  • style-loader 通过注入<style>标签将CSS添加到DOM,建议将 style-loadercss-loader 结合使用。
  • css-loader 解释 @importurl() ,会 import/require() 后再解析它们,主要用于将 CSS 转换为JS模块。
  • postcss-loader 启用 postcss 来处理 Css,需要另外配置。通过配置不同插件,可以完成非常强大的功能。postcss
  • sass-loader 加载 SASS / SCSS 文件并将其编译为 CSS。

产品环境中,使用的 MiniCssExtractPlugin 插件将每个JS中包含的CSS提取为独立文件。

以下我们重点关注一些新增的配置或插件。

bail

编译遇到错误立即终止打包过程

output.pathinfo

告诉 webpack 在 bundle 中引入「所包含模块信息」的相关注释

optimization.minimize

Webpack4 启动的优化配置,一般只在产品环境设置。

optimization.splitChunks

根据注释理解嗯,如下配置会自动开启 vendorcommons 的分割。

splitChunks: {
   chunks: 'all',
   name: false,
},

但实际执行效果和手动配置有差异。

// 代码块分割配置
    splitChunks: {
      cacheGroups: {
        vendor: {
          // 抽取出来文件的名字,默认为 true,表示自动生成文件名
          name: "vendor",
          // 表示从所有chunks里面抽取代码, 可选值为initial、async、all,也可以自定义函数
          chunks: "all",
          // 表示要过滤 modules, 这里限制为 node_modules
          test: /node_modules/,
          // 表示抽取权重,数字越大表示优先级越高。
          priority: 20,
          // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的
          reuseExistingChunk: true
        },
        commons: {
          // 抽取出来文件的名字,默认为 true,表示自动生成文件名
          name: "commons",
          // 从初始chunks里面抽取代码
          chunks: "initial",
          // 表示被引用次数,默认为1
          minChunks: 2,
          // 表示抽取出来的文件在压缩前的最小大小
          minSize: 0,
          // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的
          reuseExistingChunk: true
        }
      }
    },

terser-webpack-plugin插件(仅限产品环境)

在之前的配置中使用的 uglifyjs-webpack-plugin 插件不支持 es6 语法的解析,需要配合 Babel 一起使用,现在通过 terser-webpack-plugin 插件可以直接完成。

optimize-css-assets-webpack-plugin插件(仅限产品环境)

优化及压缩CSS的插件。并且配置使用 postcss-safe-parser 这个能修复语法错误的 PostCSS 的容错CSS解析器。

pnp-webpack-plugin插件

添加支持由Yarn 团队开发的 PnP 特性。解决现有的依赖管理方式效率太低,引用依赖时慢,安装依赖时也慢的痛点。该特性还比较新,实际尝试了下,安装体验确实很大改善,不过相对来讲如果想要查看对应的源码就相当麻烦了,同时想添加区别于全局的特定版本也需要额外操作。详情可以参考此博文 Yarn 的 Plug'n'Play 特性

react-dev-utils/ModuleScopePlugin插件

react-dev-utils 工具集提供的插件,禁止导入 srcnode_modules 文件夹以外的模块。

module.strictExportPresence

使缺少的导出出现错误而不是警告

module.rules里的oneOf

后接 loader 数组,会遍历所有 loader 直到有一个符合要求,最终缺少 loader 的情况下,会由最后的 file-loader 完成解析。

node

nodejs 标准模块的mock。使 nodejs 编写的程序能够在类似浏览器等环境运行。

performance

关闭 bundle 文件大小提示,create react app 使用了自带的 react-dev-utils/FileSizeReporter 插件。

html-webpack-plugin插件

相对之前的配置,针对产品环境,增加了压缩处理。

react-dev-utils/InterpolateHtmlPlugin

react-dev-utils 工具集提供的插件,与 HtmlWebpackPlugin 一起使用,以在index.html中嵌入值。

react-dev-utils/ModuleNotFoundPlugin

react-dev-utils 工具集提供的插件,创建模块未找到而错误的上下文环境。

case-sensitive-paths-webpack-plugin插件

强制所有需要的模块的整个路径匹配磁盘上实际路径的具体情况。适用于 window 环境和 osx 环境共同开发的情况。

react-dev-utils/WatchMissingNodeModulesPlugin

react-dev-utils 工具集提供的插件,Webpack 在缺少相关包时会抛出错误。
如果执行 yarn install 后,除非重启 devServer,否则通常无法识别该包。该插件会在安装新包时,自动识别它而无需重新启动 devServer

webpack-manifest-plugin插件

生成项目的清单文件,包含所有资产的引用。要开启 PWA 功能时,会使用到该清单文件。

fork-ts-checker-webpack-plugin插件

使用专门线程来进行 ts 类型检查,目的就是运用多核资源来提升编译的速度。

IgnorePlugin

指定不加载某些第三方包的资源。例如忽略moment 2.18的本地化内容。

优化配置

基于以上的分析我们可以对原有程序做做优化。

新增 config/webpack/getModuleRules.js

// 将每个JS中包含的CSS提取为独立文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// 允许通过读取browserslist配置来部分加载 normalize.css或sanitize.css
const postcssNormalize = require("postcss-normalize");
const path = require("path");
const fs = require("fs");

// 获取nodejs执行的工作目录
const appDirectory = fs.realpathSync(process.cwd());
// 获取相对于工作目录的相对路径的真实路径
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

// 定义正则匹配
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

const getCssRules = webpackEnv => {
  // 是否为开发环境
  const isEnvDevelopment = webpackEnv === "development";
  // 是否为产品环境
  const isEnvProduction = webpackEnv === "production";
  // 启用/禁用 Sourcemap 开发环境启用/产品环境禁用
  const shouldUseSourceMap = isEnvDevelopment ? true : false;
  // 根据环境,获取style相关loader数组
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      // 开发环境使用style-loader
      isEnvDevelopment && require.resolve("style-loader"),
      // 生产环境使用MiniCssExtractPlugin.loader
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader
      },
      // 解释 @import 和 url() ,会 import/require() 后再解析它们,主要用于将 CSS 转换为JS模块
      {
        loader: require.resolve("css-loader"),
        options: cssOptions
      },
      {
        // 启用postcss
        loader: require.resolve("postcss-loader"),
        options: {
          // 解决引用外部css出现的异常
          // https://github.com/facebook/create-react-app/issues/2677
          ident: "postcss",
          plugins: () => [
            require("postcss-flexbugs-fixes"),
            // 允许你使用未来的 CSS 特性
            require("postcss-preset-env")({
              // 自动添加前缀
              autoprefixer: {
                flexbox: "no-2009"
              },
              // 填充语法允许使用标准stage3阶段
              stage: 3
            }),
            postcssNormalize()
          ],
          sourceMap: shouldUseSourceMap
        }
      }
    ].filter(Boolean);
    // 添加其他loader sass或less等
    if (preProcessor) {
      loaders.push({
        loader: require.resolve(preProcessor),
        options: {
          sourceMap: shouldUseSourceMap
        }
      });
    }
    return loaders;
  };

  return [
    {
      test: cssRegex,
      exclude: cssModuleRegex,
      use: getStyleLoaders({
        // 用于配置css-loader作用于 @import的资源之前有多少个loader
        importLoaders: 1,
        // 是否开启sourceMap
        sourceMap: shouldUseSourceMap
      }),
      sideEffects: true
    },
    {
      test: cssModuleRegex,
      use: getStyleLoaders({
        importLoaders: 1,
        sourceMap: shouldUseSourceMap,
        modules: true
      })
    },
    {
      test: sassRegex,
      exclude: sassModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 2,
          sourceMap: shouldUseSourceMap
        },
        require.resolve("sass-loader")
      ),
      sideEffects: true
    },
    {
      test: sassModuleRegex,
      use: getStyleLoaders(
        {
          importLoaders: 2,
          sourceMap: shouldUseSourceMap,
          modules: true
        },
        require.resolve("sass-loader")
      )
    }
  ];
};

// 获取完整的模块处理规则
const getModuleRules = webpackEnv => {
  return [
    // 解析图片资源
    {
      test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
      loader: require.resolve("url-loader"),
      options: {
        limit: 10000,
        name: "static/media/[name].[hash:8].[ext]"
      }
    },
    // babel-loader解析typescript
    {
      test: /\.(ts|tsx|js|jsx)$/,
      exclude: /node_modules/,
      include: resolveApp("src"),
      use: {
        loader: require.resolve("babel-loader")
      }
    },
    // css解析相关loaders
    ...getCssRules(webpackEnv),
    // 其他文件解析
    {
      loader: require.resolve("file-loader"),
      // Exclude `js` files to keep "css" loader working as it injects
      // its runtime that would otherwise be processed through "file" loader.
      // Also exclude `html` and `json` extensions so they get processed
      // by webpacks internal loaders.
      exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
      options: {
        name: "static/media/[name].[hash:8].[ext]"
      }
    }
  ];
};

module.exports = getModuleRules;

通过传递不同环境参数,组合 WebpackoneOf 所需要使用的模块解析规则。

新增 config/webpack/getEnvVariables.js

const fs = require("fs");

const getEnvVariables = webpackEnv => {
  // 默认仅加载 .env.production | .env.development | .env.test格式的变量定义文件
  const dotenvFiles = [`.env.${webpackEnv}`].filter(Boolean);
  // 将计算机自身和自定义变量加载到nodejs环境中
  dotenvFiles.forEach(dotenvFile => {
    if (fs.existsSync(dotenvFile)) {
      require("dotenv-expand")(
        require("dotenv").config({
          path: dotenvFile
        })
      );
    }
  });

  // 假定应用程序中所使用的环境变量都是以 APP_ 开头
  const VAR_PREFIX = /^APP_/i;
  // 生成需要注入的变量
  const raw = Object.keys(process.env)
    .filter(key => VAR_PREFIX.test(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key];
        return env;
      },
      {
        // 增加环境变量
        NODE_ENV: webpackEnv
      }
    );
  // 需要注入的变量字符串化
  const stringified = {
    "process.env": Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {})
  };
  return { raw, stringified };
};

module.exports = getEnvVariables;

项目可以在根目录添加类似 .env.development.env.production.env.test的区分不同环境的变量,推荐将 .env.development 加入 git 管理,以便协作的同学清楚工程中环境变量的定义。

比如我们新增 .env.development

APP_DB_URL=127.0.0.1
APP_DB_USERNAME=admin
APP_DB_PASSWORD=12345

具体的 webpack 配置文件调整分别如下所示:
webpack.common.js

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const path = require("path");
const webpack = require("webpack");

module.exports = {
  // 入口文件
  entry: "./src/index.tsx",
  // 需要解析的文件后缀名
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
    modules: ["node_modules", path.resolve(__dirname, "src")],
  },
  // 管理插件,通过插件实现增强功能
  plugins: [
    // 自动清理dist
    new CleanWebpackPlugin(),
    // 生成清单目录
    new ManifestPlugin({
      fileName: "asset-manifest.json",
      generate: (seed, files) => {
        const manifestFiles = files.reduce(function(manifest, file) {
          manifest[file.name] = file.path;
          return manifest;
        }, seed);

        return {
          files: manifestFiles
        };
      }
    }),
    // 忽略moment 2.18的本地化内容
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ],
  // 配置项目处理的不同文件及模块
  module: {
    // 使缺少的导出出现错误而不是警告
    strictExportPresence: true,
    rules: [
      // Disable require.ensure as it's not a standard language feature.
      { parser: { requireEnsure: false } },
      {
        enforce: "pre",
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        include: path.resolve(__dirname, "src"),
        loader: "eslint-loader"
      }
    ]
  },
  // 管理输出
  output: {
    // 定义输出文件名路径
    path: path.resolve(__dirname, "dist"),
    publicPath: "/"
  },
  optimization: {
    // 代码块分割配置
    splitChunks: {
      cacheGroups: {
        vendor: {
          // 抽取出来文件的名字,默认为 true,表示自动生成文件名
          name: "vendor",
          // 表示从所有chunks里面抽取代码, 可选值为initial、async、all,也可以自定义函数
          chunks: "all",
          // 表示要过滤 modules, 这里限制为 node_modules
          test: /node_modules/,
          // 表示抽取权重,数字越大表示优先级越高。
          priority: 20,
          // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的
          reuseExistingChunk: true
        },
        commons: {
          // 抽取出来文件的名字,默认为 true,表示自动生成文件名
          name: "commons",
          // 从初始chunks里面抽取代码
          chunks: "initial",
          // 表示被引用次数,默认为1
          minChunks: 2,
          // 表示抽取出来的文件在压缩前的最小大小
          minSize: 0,
          // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的
          reuseExistingChunk: true
        }
      }
    },
    // manifest分割配置
    runtimeChunk: true
  },
};

webpack.dev.js

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const getModuleRules = require("./config/webpack/getModuleRules");
const getEnvVariables = require("./config/webpack/getEnvVariables");
// 开发环境
const webpackDev = "development";
// 定义模块解析规则
const rules = getModuleRules(webpackDev);
// 获取环境变量定义
const env = getEnvVariables(webpackDev);
// 将环境定义注入到Nodejs中,后续Babel等配置会使用该变量
process.env.NODE_ENV = webpackDev;

module.exports = merge(common, {
  // 标识配置为开发用
  mode: webpackDev,
  // 控制是否生成,以及如何生成 source map
  devtool: "cheap-module-source-map",
  // 管理开发服务器
  devServer: {
    // 开启服务器路由支持,默认定位根目录index.html
    historyApiFallback: true,
    // 查找文件路径
    contentBase: "dist",
    // 启用 HMR
    hot: true
  },
  plugins: [
    // 当开启 HMR 的时候使用该插件会显示模块的相对路径,建议用于开发环境
    new webpack.NamedModulesPlugin(),
    // 启用 HMR 热更新,建议用于开发环境
    new webpack.HotModuleReplacementPlugin(),
    // 预设程序执行环境
    new webpack.DefinePlugin(env.stringified),
    // 根据模板生成html
    new HtmlWebpackPlugin({
      title: "My App",
      template: "./src/index.html"
    })
  ],
  // 管理输出
  output: {
    // 定义输出文件名规则
    filename: "static/js/bundle.js",
    // 定义非入口(non-entry) chunk 文件的名称
    chunkFilename: "static/js/[name].chunk.js",
    // 告诉 webpack 在 bundle 中引入「所包含模块信息」的相关注释
    pathinfo: true
  },
  // 配置项目处理的不同文件及模块
  module: {
    // 配置项目处理模块规则
    rules: [
      {
        oneOf: rules
      }
    ]
  }
});

webpack.prod.js,生成环境去掉了之前采用的 UglifyJSPlugin JS压缩插件。

const merge = require("webpack-merge");
const common = require("./webpack.common.js");
// const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const safePostCssParser = require("postcss-safe-parser");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const getModuleRules = require("./config/webpack/getModuleRules");
const getEnvVariables = require("./config/webpack/getEnvVariables");
// 生产环境
const webpackDev = "production";
// 定义模块解析规则
const rules = getModuleRules(webpackDev);
// 获取环境变量定义
const env = getEnvVariables(webpackDev);
// 将环境定义注入到Nodejs中,后续Babel等配置会使用该变量
process.env.NODE_ENV = webpackDev;

module.exports = merge(common, {
  // 标识配置为生产用
  mode: webpackDev,
  // 编译遇到错误立即终止打包过程
  bail: true,
  // 控制是否生成,以及如何生成 source map
  devtool: false,
  plugins: [
    // 预设程序执行环境
    new webpack.DefinePlugin(env.stringified),
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:8].css",
      chunkFilename: "static/css/[name].[contenthash:8].chunk.css"
    }),
    // 根据模板生成html
    new HtmlWebpackPlugin({
      title: "My App",
      template: "./src/index.html",
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    })
  ],
  // 管理输出
  output: {
    // 定义输出文件名规则
    filename: "static/js/[name].[contenthash:8].js",
    // 定义非入口(non-entry) chunk 文件的名称
    chunkFilename: "static/js/[name].[contenthash:8].chunk.js"
  },
  // 代码分割配置
  optimization: {
    // 启用js代码压缩,生产环境默认为true
    minimize: true,
    // 指定自定义压缩插件
    minimizer: [
      // Terser配置
      new TerserPlugin({
        terserOptions: {
          parse: {
            ecma: 8
          },
          compress: {
            ecma: 5,
            warnings: false,
            comparisons: false,
            inline: 2
          },
          mangle: {
            safari10: true
          },
          output: {
            ecma: 5,
            comments: false,
            ascii_only: true
          }
        },
        // 开启多线程,加快编译速度
        parallel: true,
        // 开启文件缓存
        cache: true,
        // 关闭sourceMap
        sourceMap: false
      }),
      // 优化及压缩CSS
      new OptimizeCSSAssetsPlugin({
        cssProcessorOptions: {
          parser: safePostCssParser,
          map: false
        }
      })
    ]
  },
  // 配置项目处理的不同文件及模块
  module: {
    // 配置项目处理模块规则
    rules: [
      {
        oneOf: rules
      }
    ]
  }
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342