前端工程化常用工具总结

一、代码规范

1.1 vscode集成editorconfig

安装editorconfig插件后,在项目根目录下生成如下配置

.editorconfig

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

1.2 使用prettier格式化工具

Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。

  1. 安装prettier

    npm install prettier -D
    

    -D 就是npm install --save-dev 表示改依赖只在开发环境中

  2. prettier配置文件.prettierrc

    • useTabs:使用tab缩进还是空格缩进,选择false;
    • tabWidth:tab是空格的情况下,是几个空格,选择2个;
    • printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
    • singleQuote:使用单引号还是双引号,选择true,使用单引号;
    • trailingComma:在多行输入的尾逗号是否添加,设置为 none
    • semi:语句末尾是否要加分号,默认值true,选择false表示不加;

    .prettierrc

    {
      "useTabs": false,
      "tabWidth": 2,
      "printWidth": 80,
      "singleQuote": true,
      "trailingComma": "none",
      "semi": false
    }
    
  3. prettier忽略文件.prettierignore

    /dist/*
    .local
    .output.js
    /node_modules/**
    
    **/*.svg
    **/*.sh
    
    /public/*
    
  4. package.json中添加格式化所有文件的脚本

    "prettier": "prettier --write ."
    

1.3 使用ESLint检测

  1. 如果使用vue cli创建项目的时候选择了ESLint,则vue会默认配置好ESLint所需的环境

  2. vscode安装ESLint插件

  3. 解决ESLintprettier之间的冲突

    vue cli创建的项目中,ESLint的规范是vue团队的,如果我们想要用自己的ESLint配置,则会和他们的规范冲突,这样一来prettier格式化后就会和ESLint的不一致,为了解决这个问题,需要安装如下两个插件

    npm i eslint-plugin-prettier eslint-config-prettier -D
    

    这两个插件如果在vue cli创建项目时选择了ESLint + prettier,则会默认帮我们装上的

    将插件添加到.eslintrc.js

    extends: [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/typescript/recommended',
        '@vue/prettier',
        '@vue/prettier/@typescript-eslint',
        'plugin:prettier/recommended'
    ],
    

    即在最后一行加上'plugin:prettier/recommended'即可

1.4 git Husky保证提交代码的规范

虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:

  • 也就是我们希望保证代码仓库中的代码都是符合eslint规范的;
  • 那么我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;

husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push

这里我们可以使用自动配置命令:

npx husky-init && npm install

该命令会做三件事:

  1. 添加husky项目依赖到package.json中的devDependencies

  2. 在项目目录下创建 .husky 文件夹,该文件夹中存放hook配置,也可以手动执行下面的命令进行创建

    npx huksy install
    
  3. package.json中添加一个脚本

    "prepare": "husky install"
    

接下来,我们需要去完成一个操作:在进行commit时,执行package.json中的lint脚本,这时候就需要修改hook配置了

打开.hucky中的pre-commit配置文件,将原本的npm test改成npm run lint即可

1.5 git commit规范

  • Commitizen用于编写规范的commit message
  • commitlint用于检查提交的信息是否符合规范,用于避免提交的时候是直接git commit -m "xxx",而不是通过Commitizen时的情况

1.5.1 Commitizen

  1. 安装Commitizen

    npm install commitizen -D
    
  2. 安装cz-conventional-changelog,并且初始化cz-conventional-changelog

    npx commitizen init cz-conventional-changelog --save-dev --save-exact
    

    该命令会安装cz-conventional-changelog并在package.json中进行配置

  3. 现在提交代码就可以使用npx cz提交,提交的message就是规范的了

    • type

      Type 作用
      feat 新增特性 (feature)
      fix 修复 Bug(bug fix)
      docs 修改文档 (documentation)
      style 代码格式修改(white-space, formatting, missing semi colons, etc)
      refactor 代码重构(refactor)
      perf 改善性能(A code change that improves performance)
      test 测试(when adding missing tests)
      build 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
      ci 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
      chore 变更构建流程或辅助工具(比如更改测试环境)
      revert 代码回退

1.5.2 commitlint

  1. 安装 @commitlint/config-conventional 和 @commitlint/cli

    npm i @commitlint/config-conventional @commitlint/cli -D
    
  2. 在根目录创建commitlint.config.js文件,配置commitlint

    module.exports = {
      extends: ['@commitlint/config-conventional']
    }
    
  3. 使用husky生成commit-msg文件,验证提交信息:

    npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
    

二、第三方集成

2.1 vue.config.js配置

vue.config.js有三种配置方式:

  • 方式一:直接通过CLI提供给我们的选项来配置:
    • 比如publicPath:配置应用程序部署的子目录(默认是 /,相当于部署在 https://www.my-app.com/);
    • 比如outputDir:修改输出的文件夹;
  • 方式二:通过configureWebpack修改webpack的配置:
    • 可以是一个对象,直接会被合并;
    • 可以是一个函数,会接收一个config,可以通过config来修改配置;
  • 方式三:通过chainWebpack修改webpack的配置:
    • 是一个函数,会接收一个基于 webpack-chain 的config对象,可以对配置进行修改;

示例

const path = require('path')

module.exports = {
  // 配置方式一
  outputDir: './build',
  
  // 配置方式二:对象形式
  configureWebpack: {
    resolve: {
      alias: {
        views: '@/views'
      }
    }
  }
  
  // 配置方式三:函数形式
  configureWebpack: (config) => {
    config.resolve.alias = {
      '@': path.resolve(__dirname, 'src'),
      views: '@/views'
    }
  },
      
  // 配置方式四:链式调用形式
  chainWebpack: (config) => {
    config.resolve.alias.set('@', path.resolve(__dirname, 'src')).set('views', '@/views')
  }
}

遇到明确没问题的ESLint提示

比如这里的const path = require('path'),ESLint会提示使用ES风格的import替代,但是由于是给node用的配置文件,必须是commonJS风格的,这个时候我们需要把这一条ESLint提示禁用掉

vscode中将鼠标悬停在提示的代码处,会弹出对应的ESLint提示项,比如这里的提示就是@typescript-eslint/no-var-requires

将它复制下来,打开.eslintrc.js,在rules中添加该配置项,并且值设为off即可关闭

rules: {
  '@typescript-eslint/no-var-requires': 'off'
}

2.2 vue-router集成

  1. 安装vue-router

    npm install vue-router@4
    
  2. 创建文件 -- src/router/index.ts

    index.ts

    import { createRouter, createWebHashHistory } from 'vue-router'
    import { RouteRecordRaw } from 'vue-router'
    
    const routes: RouteRecordRaw[] = [
      {
        path: '/',
        redirect: '/main'
      },
      {
        path: '/main',
        component: () => import('@/views/main/main.vue')
      },
      {
        path: '/login',
        component: () => import('@/views/login/login.vue')
      }
    ]
    
    const router = createRouter({
      routes,
      history: createWebHashHistory()
    })
    
    export default router
    
  3. main.ts中注册

    main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    
    import router from '@/router'
    
    const app = createApp(App)
    app.use(router)
    app.mount('#app')
    
  4. App.vue中配置路由跳转

    <template>
      <div class="app">
        <router-link to="/login">登录</router-link>
        <router-link to="/main">首页</router-link>
        <router-view></router-view>
      </div>
    </template>
    

2.3 vuex集成

  1. 安装vuex

    npm install vuex@next --save
    
  2. 创建文件 -- src/store/index.ts

    index.ts

    import { createStore } from 'vuex'
    
    const store = createStore({
      state() {
        return {
          name: 'plasticine'
        }
      }
    })
    
    export default store
    
  3. main.ts中注册

    app.use(store)
    
  4. App.vue中使用

    <h1>{{ $store.state.name }}</h1>
    

2.4 element-plus集成

  1. 安装element-plus

    npm install element-plus --save
    
  2. 引入element-plus

2.4.1 完整引入

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

2.4.2按需引入

  1. 安装两个插件

    npm install -D unplugin-vue-components unplugin-auto-import
    
  2. 修改vue.config.js中的Webpack配置

    const AutoImport = require('unplugin-auto-import/webpack')
    const Components = require('unplugin-vue-components/webpack')
    const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
    
    module.exports = {
      configureWebpack: {
        plugins: [
          AutoImport({
            resolvers: [ElementPlusResolver()]
          }),
          Components({
            resolvers: [ElementPlusResolver()]
          })
        ]
      }
    }
    
  3. main.ts中注册全局组件

    main.ts

    import {
      ElButton,
      ElTable,
      ElAlert,
      ElAside,
      ElAutocomplete,
      ElAvatar,
      ElBacktop,
      ElBadge
    } from 'element-plus'
    
    const app = createApp(App)
    
    const components = [
      ElButton,
      ElTable,
      ElAlert,
      ElAside,
      ElAutocomplete,
      ElAvatar,
      ElBacktop,
      ElBadge
    ]
    
    for (const cpn of components) {
      app.component(cpn.name, cpn)
    }
    

    这样的话虽然能用,但是随着开发进度的进行,引用的组件越来越多,会导致main.ts文件过于臃肿,作为入口文件,应当尽量只包括主要逻辑,不应该包含过多的逻辑代码,因此现在我们对其进行抽离

  4. 创建文件 -- src/global/index.ts

    index.ts

    import {
      ElButton,
      ElTable,
      ElAlert,
      ElAside,
      ElAutocomplete,
      ElAvatar,
      ElBacktop,
      ElBadge
    } from 'element-plus'
    import { App } from 'vue'
    
    const components = [
      ElButton,
      ElTable,
      ElAlert,
      ElAside,
      ElAutocomplete,
      ElAvatar,
      ElBacktop,
      ElBadge
    ]
    
    export function registerApp(app: App): void {
      for (const cpn of components) {
        app.component(cpn.name, cpn)
      }
    }
    

    导出一个函数registerApp,在main.ts中只用调用该函数即可

    main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    import { registerApp } from '@/global'
    
    const app = createApp(App)
    
    registerApp(app)
    app.mount('#app')
    

    其实还可以进一步抽离,因为以后可能还要注册别的组件,这时候如果全部注册逻辑写在单个registerApp函数里就又会变得臃肿了,因此将每个组件库的组件注册再次抽离成一个文件,如现在要注册element-plus,那么我们就在global目录下创建一个register-element.ts,然后把注册逻辑写在里面,registerApp去调用即可

  `register-element.ts`

  ```typescript
  import { App } from 'vue'
  import {
    ElButton,
    ElTable,
    ElAlert,
    ElAside,
    ElAutocomplete,
    ElAvatar,
    ElBacktop,
    ElBadge
  } from 'element-plus'
  
  const components = [
    ElButton,
    ElTable,
    ElAlert,
    ElAside,
    ElAutocomplete,
    ElAvatar,
    ElBacktop,
    ElBadge
  ]
  
  export default function (app: App): void {
    for (const cpn of components) {
      app.component(cpn.name, cpn)
    }
  }
  ```

  `src/global/index.ts`

  ```typescript
  import { App } from 'vue'
  import registerElement from './register-element'
  
  export function registerApp(app: App): void {
    registerElement(app)
  }
  ```
  1. App.vue中直接使用

    <el-button>element-plus 按钮</el-button>
    
  2. 更优雅地注册

    Vue的app.use()会默认传入app,因此可以进行如下调整,让代码风格更加统一

  `main.ts`

  ```typescript
  import { createApp } from 'vue'
  import App from './App.vue'
  
  import { globalRegister } from '@/global'
  
  const app = createApp(App)
  
  app.use(globalRegister)
  app.mount('#app')
  ```

  `src/global/index.ts`

  ```typescript
  import { App } from 'vue'
  import registerElement from './register-element'
  
  export function globalRegister(app: App): void {
    app.use(registerElement)
  }
  ```

2.5 axios集成

2.5.1 安装axios

npm install axios

2.5.2 创建文件 -- src/service/request/config.ts

该文件用于存放一些axios用到的配置项,如BASE_URL

/**
 * 生产环境 -- production
 * 开发环境 -- development
 * 测试环境 -- test
 */

let BASE_URL = ''
const TIME_OUT = 10000

switch (process.env.NODE_ENV) {
  case 'development':
    BASE_URL = 'http://123.207.32.32:8000'
    break
  case 'production':
    BASE_URL = 'https://www.baidu.com/'
    break
  case 'test':
    BASE_URL = 'https://www.baidu.com/'
    break
}

export { BASE_URL, TIME_OUT }

2.5.3 封装AxiosInstance

封装AxiosInstance实例对象,主要是添加对各种拦截器的支持,拦截器的粒度细致到以下三个阶段:

  1. 全局请求响应拦截,对所有的请求都生效
  2. 实例请求响应拦截,针对不同的实例可以设置不同的请求响应拦截
  3. 单独请求响应拦截,针对具体接口设置相应的请求响应拦截

要实现上述拦截器,需要自己封装一个拦截器类型接口,分别对应请求成功处理、请求失败处理、响应成功处理、响应失败处理

因此再创建一个文件,用于存放用到的接口类型 -- src/service/request/type.ts

import { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface WFRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface WFRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: WFRequestInterceptors<T>
  showLoading?: boolean
}

创建一个类用于封装AxiosInstance实例,这个类存放在 src/service/request/index.ts中并默认导出

src/service/request/index.ts

import axios from 'axios'

import { ElLoading } from 'element-plus'
import type { AxiosInstance } from 'axios'

import { WFRequestConfig, WFRequestInterceptors } from './type'
import { DEAFAULT_LOADING } from './config'
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'

class WFRequest {
  instance: AxiosInstance
  interceptors?: WFRequestInterceptors
  showLoading: boolean // 处理请求时是否要显示加载动画
  loading?: LoadingInstance

  constructor(config: WFRequestConfig) {
    // 创建 axios 实例
    this.instance = axios.create(config)
    // 保存基本信息
    this.showLoading = config.showLoading ?? DEAFAULT_LOADING
    this.interceptors = config.interceptors

    // ================== 属于实例的拦截器 ==================
    // 将请求拦截器注册到 axios 实例中
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    // 将请求拦截器注册到 axios 实例中
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )

    // ================== 所有实例的拦截器 ==================
    // 所有实例的请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        console.log('所有的实例都有的拦截器: 请求成功拦截')

        // 处理加载动画
        if (this.showLoading) {
          this.loading = ElLoading.service({
            lock: true,
            text: '正在请求数据......',
            background: 'rgba(0, 0, 0, 0.5)'
          })
        }

        return config
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 请求成功拦截')
        return err
      }
    )
    // 所有实例的响应拦截器
    this.instance.interceptors.response.use(
      (res) => {
        console.log('所有的实例都有的拦截器: 响应成功拦截')
        // 如果有加载动画则将加载动画移除
        this.loading?.close()

        // 从 res 中提出 data 返回 因为 data 才是前端真正需要的,其他的东西是 axios 自己封装的 基本用不到
        const data = res.data

        return data
      },
      (err) => {
        console.log('所有的实例都有的拦截器: 响应失败拦截')
        this.loading?.close()

        // HTTP 的状态码要在失败响应拦截器中拦截
        if (err.response.status === 404) {
          console.log('404 not found...')
        }

        return err
      }
    )
  }

  request<T>(config: WFRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 单个请求如果配置了请求拦截器 则先执行其配置的请求拦截器 再执行全局的请求拦截器
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }

      // 判断是否要显示 loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }

      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 如果单次请求配置了响应拦截器 则执行实例的响应拦截器
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          // 将 showLoading 设置为 true -- 这样就不会影响下一个请求了
          this.showLoading = DEAFAULT_LOADING

          resolve(res)
        })
        .catch((err) => {
          // 将 showLoading 设置为 true -- 这样就不会影响下一个请求了
          this.showLoading = DEAFAULT_LOADING
          reject(err)

          return err
        })
    })
  }

  get<T>(config: WFRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T>(config: WFRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  put<T>(config: WFRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' })
  }

  delete<T>(config: WFRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  patch<T>(config: WFRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}

export default WFRequest

2.5.3.1使用泛型T的原因

这里使用到的泛型T,意思是在调用request方法后返回的对象类型是由axios的AxiosResponse封装好的T,即调用返回对象的data属性拿到的就是T类型的对象,这点可以通过源码验证:

request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;

而这里调用request时,传入的泛型为request<any, T>,目的是不让axios帮我们封装成AxiosResponse实例,而是直接返回我们需要的泛型对象T(会由Promise封装)

因此thenres类型就是T,然后再在调用单个请求的拦截器的时候:

export interface WFRequestInterceptors<T = AxiosResponse> {
  ...
  responseInterceptor?: (res: T) => T
}

此时响应拦截器接收到的参数类型就是T,T默认就是AxiosResponse,而一旦我们更改为自己想要的T类型,则不会再传入和返回AxiosResponse类型的对象

之所以要折腾这么一大长串代码,是因为要实现一个功能:让调用接口的时候得到的返回值是预先定义好的后端接口会返回的数据格式的接口对象,这样在调用者看来,就能有如下体验:

// 可以指定接口返回的对象类型
interface DataType {
  data: any
  returnCode: string
  success: boolean
}

wfRequest
  .get<DataType>({
    url: '/home/multidata'
  })
  .then((res) => {
    console.log(res)
    console.log(res.data)
    console.log(res.returnCode)
    console.log(res.success)
  })

调用者在then中拿到的不再是AxiosResponse对象,而是调用get方法时传入的DataType泛型对象

2.5.3.2 service中实例化封装好的类

src/service/index.ts中实例化一个WFRequest的对象,并将其导出以供使用

import WFRequest from './request'
import { BASE_URL, TIME_OUT } from './request/config'

const wfRequest = new WFRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {
    requestInterceptor: (config) => {
      // 给该实例发起的所有请求携带上 token
      const token = 'temp_token'
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }
      console.log('单个实例请求成功的拦截')

      return config
    },
    requestInterceptorCatch: (err) => {
      console.log('单个实例请求失败的拦截')
      return err
    },
    responseInterceptor: (res) => {
      console.log('响应成功的拦截')
      return res
    },
    responseInterceptorCatch: (err) => {
      console.log('响应失败的拦截')
      return err
    }
  }
})

export default wfRequest

2.5.3.3 体验

在项目的main.ts中使用体验一下

import wfRequest from './service'

// 可以指定接口返回的对象类型
interface DataType {
  data: any
  returnCode: string
  success: boolean
}

wfRequest.get<DataType>({
  url: '/home/multidata',
  interceptors: {
    requestInterceptor: (config) => {
      console.log('单独请求的拦截器')
      return config
    },
    responseInterceptor: (res) => {
      console.log('单独响应的拦截器')
      console.log(res.returnCode)
      console.log(res.success)
      console.log(res.data)
      return res
    }
  }
})
体验封装好的axios
请求中会自动携带上token
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容