OpenHarmony ArkUI ETS- 装饰器解读

前言

最近利用空闲时间在学习华为方舟开发框架(简称:ArkUI)的ets开发,发现在ets语言中装饰器的有着非常重要的作用,在写每一个自定义组件时都需要用到它,看到装饰器这个字眼,想起之前学过的设计模式里的装饰器模式(允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装),ets中的装饰器的作用就是类似这样的。华为官网中对装饰器的介绍也比较详细,参照官网的讲解,自己对其进行了汇总,方便后续理解与查阅。

装饰器

1、@Component

装饰内容为struct,@Component装饰的struct表示该结构体具有组件化能力,能够成为一个独立的组件,这种组件就是自定义组件

  • 所有的UI都是由组件构成,组件的数据结构为struct
  • 自定义组件可以调用其他自定义组件和内置组件
  • 自定义组件内部必须实现build方法来描述UI结构
  • 禁止自定义构造函数

@Component

struct MyComponent {

    build() {

        Column() {

            Text('my component')

                .fontColor(Color.Red)

        }.alignItems(HorizontalAlign.Center)

    }

}



@Component

struct ParentComponent {

    build() {

        Column() {

            MyComponent()

            Text('we use component')

                .fontSize(20)

        }

    }

}

2、@Entry

装饰内容为struct,用@Entry装饰的自定义组件用作页面的默认入口组件,加载页面时,将首先创建并呈现@Entry装饰的自定义组件。

  • 在单个源文件中,最多可以使用@Entry装饰一个自定义组件。即一个页面有且仅有一个@Entry
// 只会渲染加载出 MyComponent 组件,显示hello world

@Entry

@Component

struct MyComponent {

    build() {

        Column() {

            Text('hello world')

                .fontColor(Color.Red)

        }

    }

}



@Component

struct HideComponent {

    build() {

        Column() {

            Text('goodbye')

                .fontColor(Color.Blue)

        }

    }

}

3、@Preview

装饰内容为struct,用@Preview装饰的自定义组件可以在DevEco Studio的预览器上进行预览。

  • 单组件预览,仅预览被@Preview装饰的自定义组件

  • 在单个源文件中,最多可以使用10个**@Preview**装饰一个自定义组件

多个组件预览

4、@Builder

装饰内容为方法, @Builder装饰的方法用于定义组件的声明式UI描述,在一个自定义组件内快速生成多个布局内容。


@Entry

@Component

struct CompA {

  @State message: string = 'Builder演示'

  @State isA: boolean = false

  @State isB: boolean = false

  @Builder myBuilder(str: string, state: boolean, click: () => void) {

    Row() {

      Text(str + ":" + state)

        .fontSize(24).margin({ right: 20 })



      Button('开关').fontSize(24).onClick(() => {

        click()

      })

    }.margin({bottom: 5})

  }

  @Builder myCheckBox() {

    Row() {

      Checkbox({name: 'checkbox1',  group: 'checkboxGroup'})

        .select(true)

        .selectedColor(0xed6f21)

        .onChange((value: boolean) => {

          console.info('Checkbox1 change is'+ value)

        })

      Checkbox({name: 'checkbox2',  group: 'checkboxGroup'})

        .select(false)

        .selectedColor(0x39a2db)

        .onChange((value: boolean) => {

          console.info('Checkbox2 change is'+ value)

        })

    }

  }



  build() {

    Row() {

      Column() {

        Text(this.message)

          .fontSize(24)

          .fontWeight(FontWeight.Bold).margin({ bottom: 20 })



        this.myBuilder('A', this.isA, () => {

          this.isA = !this.isA

        })



        this.myBuilder('B', this.isB, () => {

          this.isB = !this.isB

        })

        this.myCheckBox()

      }

      .width('100%')

    }

    .height('100%')

  }

}

5、@Extend

装饰内容为方法,@Extend装饰器将新的属性函数添加到内置组件上,如Text、Column、Button等。

  • 通过@Extend装饰器可以快速定义并复用组件的自定义样式,即对内置组件进行封装,增强复用性

  • @Extend装饰器不能用在自定义组件struct定义框内。


  //给内置组件Text添加属性函数font 用于同时设置 字号,字体颜色,粗细

  @Extend(Text) function font (size: number, color: ResourceColor, weight: FontWeight) {

    .fontSize(size)

    .fontColor(color)

    .fontWeight(weight)

  }

  @Component

  @Entry

  struct Index {

    @State message: string = 'Hello World'

    build() {

      Row() {

        Column() {

          // 后面设置的样式会覆盖前面的,这里的最终字号为50

          Text(this.message)

            // 调用新增的属性函数 font

            .font(30, Color.Blue, FontWeight.Bold)

            .fontSize(50)

        }

        .width('100%')

      }

      .height('100%')

    }

  }

6、@CustomDialog

@CustomDialog装饰器用于装饰自定义弹窗

@CustomDialog

struct CustomDialogExample {

  controller: CustomDialogController

  cancel: () => void

  confirm: () => void



  build() {

    Column() {

      Text('自定义弹框标题').fontSize(20).margin({ top: 10, bottom: 10 })

      Text('这是自定义弹框内容,确定选择吗?').fontSize(16).margin({ bottom: 10 })

      Flex({ justifyContent: FlexAlign.SpaceAround }) {

        Button('取消')

          .onClick(() => {

            this.controller.close()

            this.cancel()

          }).backgroundColor(0xffffff).fontColor(Color.Black)

        Button('确定')

          .onClick(() => {

            this.controller.close()

            this.confirm()

          }).backgroundColor(0xffffff).fontColor(Color.Red)

      }.margin({ bottom: 10 })

    }

  }

}



@Entry

@Component

struct CustomDialogUser {

  @State textValue: string = ''

  dialogController: CustomDialogController = new CustomDialogController({

    builder: CustomDialogExample({ cancel: this.onCancel.bind(this), confirm: this.onAccept.bind(this)}),

    cancel: this.existApp,

    autoCancel: true

  })



  onCancel() {

    this.textValue = '点击了取消'

  }

  onAccept() {

    this.textValue = '点击了确定'

  }

  existApp() {

    this.textValue = '点击了空白区域'

  }



  build() {

    Column() {

      Text(this.textValue)

        .fontSize(30).margin({bottom: 20})

      Button('打开自定义弹框')

        .onClick(() => {

          this.dialogController.open()

        }).fontSize(30)

    }.width('100%').margin({ top: 20 })

  }

}

7、@BuilderParam

这是API8 提供的一个新的装饰器,@BuilderParam装饰器用于修饰自定义组件内函数类型的属性(例如:@BuilderParam content: () => any;)

  • @BuilderParam 是一个属性装饰器

  • 初始化自定义组件时被@BuilderParam修饰的属性必须赋值。

  • 可以在局部样式相同,局部样式不同时复用

  • 类似于slot插槽一样,能在自定义组件中插入对应的UI结构

  • 如果既有参数初始化组件和尾随闭包初始化组件,尾随闭包初始化组件优先级更高

参数初始化组件

通过参数初始化组件时,将@Builder装饰的方法赋值给@BuilderParam修饰的属性,并在自定义组件内调用content属性值。对@BuilderParam修饰的属性进行赋值时不带参数(如:content: this.specificParam),则此属性的类型需定义成无返回值的函数(如:@BuilderParam content: () => void)。若带参数(如:callContent: this.specificParam1("111")),则此属性的类型需定义成any(如:@BuilderParam callContent: any;)。

@Component

struct CustomContainer {

  header: string = "";

  @BuilderParam noParam: () => void;

  @BuilderParam withParam: any;

  footer: string = "";

  build() {

    Column() {

      Text(this.header)

        .fontSize(50)

      this.noParam()

      this.withParam()

      Text(this.footer)

        .fontSize(50)

    }

  }

}

@Entry

@Component

struct CustomContainerUser {

  @Builder specificNoParam() {

    Column() {

      Text("noParam").fontSize(50)

    }

  }

  @Builder SpecificWithParam(label: string) {

    Column() {

      Text(label).fontSize(50)

    }

  }



  build() {

    Column() {

      CustomContainer({

        header: "Header",

        noParam: this.specificNoParam,

        withParam: this.SpecificWithParam("WithParam"),

        footer: "Footer",

      })

    }

  }

}

参数初始化组件

在自定义组件中使用@BuilderParam修饰的属性接收尾随闭包(在初始化自定义组件时,组件名称紧跟一个大括号“{}”形成尾随闭包场景(CustomComponent(){})。开发者可把尾随闭包看做一个容器,向其填充内容,如在闭包内增加组件({Column(){Text("content")}),闭包内语法规范与build一致。此场景下自定义组件内有且仅有一个使用@BuilderParam修饰的属性。注意:一个组件内只允许有一个@BuilderParam装饰器

@Component

struct CustomContainer {

  header: string = "";

  @BuilderParam closer: () => void;

  build() {

    Column() {

      Text(this.header)

        .fontSize(50)

      this.closer()

    }

  }

}

@Builder function specificParam(label1: string, label2: string) {

  Column() {

    Text(label1)

      .fontSize(50)

    Text(label2)

      .fontSize(50)

  }

}

@Builder function specificNoParam() {

  Column() {

    Text('88888')

      .fontSize(50)

    Text('999999')

      .fontSize(50)

  }

}

@Entry

@Component

struct CustomContainerUser {

  @State text: string = "header"

  build() {

    Column() {

      CustomContainer({

        header: this.text,

        closer: specificNoParam

      })

      {

        Column(){

          specificParam("111", "22")

        }.onClick(()=>{

          this.text = "changeHeader"

        })

      }

    }

  }

}

8、@Styles

这是API8 提供的一个新的装饰器,我们知道@Extend装饰器不能写在组件内,但@Styles却能够定义在组件内。

@Styles和@Extend一样,也是提供方法将新的属性函数添加到内置组件。

有几个注意的点:

  • @Styles装饰的方法不能带参数

  • @Styles装饰的方法可以有多个

  • 后面执行的属性函数优先级最高

  • 当前@Styles仅支持通用属性

  • @Styles可以定义在组件内或组件外,在组件外定义时需带上function关键字,组件内定义时不需要

// 定义在组件外的 需要带function

@Styles function globalFancy() {

  .backgroundColor(Color.Red)

}



@Entry

@Component

struct FancyUse {

   // 定义在组件内的 不需要带function

  @Styles componentFancy() {

    .backgroundColor(Color.Blue)

  }

  build() {

    Column({ space: 10 }) {

      Text("Fancy")

        // 调用属性新增的函数

        .globalFancy()

        .width(100)

        .height(100)

        .fontSize(30)

      Text("Fancy")

        // 调用属性新增的函数

        .componentFancy()

        .width(100)

        .height(100)

        .fontSize(30)

    }

  }

}

9、@State

@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新

  • 支持多种类型:允许class、number、boolean、string强类型的按值和按引用类型。允许这些强类型构成的数组,即Array、Array、Array、Array。不允许object和any。

  • 支持多实例:组件不同实例的内部状态数据独立。

  • 内部私有:标记为@State的属性是私有变量,只能在组件内访问。

  • 需要本地初始化:必须为所有@State变量分配初始值,将变量保持未初始化可能导致框架行为未定义。

  • 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@State状态属性的初始值。

简单类型的状态属性示例


@Entry

@Component

struct StateDemo {

  @State count: number =0

  private toggleClick() {

    this.count += 1

  }

  build() {

    Column() {

      Button() {

        Text(`click times: ${this.count}`)

          .fontSize(20).padding(10)

      }.onClick(this.toggleClick.bind(this))

    }.margin(30)

  }

}

复杂类型的状态变量示例


// 自定义Model类

class Model {

  value: string

  constructor(value: string) {

    this.value = value

  }

}



@Entry

@Component

struct EntryComponent {

  build() {

    Column() {

      // 创建MyComponent实例时通过变量名给组件内的变量进行初始化 2个组件实例的内部状态互不影响

      MyComponent({count: 1, increaseBy: 2})

      MyComponent({title: {value: 'Hello, World 2'}, count: 7})

    }.margin(20)

  }

}



@Component

struct MyComponent {

  @State title: Model = {value: 'Hello World'}

  @State count: number = 0

  private toggle: string = 'Hello World'

  private increaseBy: number = 1



  build() {

    Column() {

      Text(`${this.title.value}`).fontSize(30)

      Button() {

        Text(`点击修改title`).fontSize(20).fontColor(Color.White).padding({left: 10, right: 10})

      }.onClick(() => {

        this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello UI'

      }).margin({top: 10, bottom: 10})



      Button() {

        Text(`添加增加 count=${this.count}`).fontSize(20).fontColor(Color.White).padding({left: 10, right: 10})

      }.onClick(() => {

        this.count += this.increaseBy

      })

    }.margin(16)

  }

}

10、@Prop

@Prop用于修饰基本数据类型,修改后的状态数据用于在父组件和子组件之间建立单向数据依赖关系。修改父组件关联数据时,更新当前组件的UI。

@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但更改不会通知给父组件,即@Prop属于单向数据绑定。类似于vue中的父子组件传值prop, 只是vue中的prop传递的属性值在子组件中不允许修改。

@Prop状态数据具有以下特征:

  • 支持简单类型:仅支持number、string、boolean简单类型;

  • 私有:仅在组件内访问;

  • 支持多个实例:一个组件中可以定义多个标有@Prop的属性;

  • 创建自定义组件时将值传递给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。


@Entry

@Component

struct ParentComponent {

  @State countDownStartValue: number = 10 // 10 Nuggets default start value in a Game

  build() {

    Column() {

      Text(`这是子组件中显示的信息 count: ${this.countDownStartValue}`)

      Button() {

        Text('数字+1')

      }.onClick(() => {

        this.countDownStartValue += 1

      })

      Button() {

        Text('数字-1')

      }.onClick(() => {

        this.countDownStartValue -= 1

      })

      // 创建子组件时,必须在构造函数参数中提供其@Prop变量的初始值,同时初始化常规变量CostOfOneAttump(非Prop)

      CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2})

    }

  }

}



@Component

struct CountDownComponent {

  @Prop count: number

  private costOfOneAttempt: number



  build() {

    Column() {

      if (this.count > 0) {

        Text(`这是子组件中显示的信息 count: ${this.count}`)

      } else {

        Text('Game over!')

      }



      Button() {

        Text('Try again')

      }.onClick(() => {

        this.count -= this.costOfOneAttempt

      })

    }

  }

}

11、@Link

@Link用于修饰基本数据类型,类,数组,父子组件之间的双向数据绑定,父组件的内部状态数据作为数据源,任何一方所做的修改都会反映给另一方。

@Link装饰的变量可以和父组件的@State变量建立双向数据绑定:

  • 支持多种类型:@Link变量的值与@State变量的类型相同,即class、number、string、boolean或这些类型的数组;

  • 私有:仅在组件内访问;

  • 单个数据源:初始化@Link变量的父组件的变量必须是@State变量;

  • 双向通信:子组件对@Link变量的更改将同步修改父组件的@State变量;

  • 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过'$'操作符创建引用。

  • @Link变量不能在组件内部进行初始化。

  @Entry

  @Component

  struct Player {

    @State isPlaying: boolean = false

    build() {

      Column() {

        PlayButton({buttonPlaying: $isPlaying})

        Text(`Player is ${this.isPlaying? '':'not'} playing`)

        Button() {

          Text(this.isPlaying? 'parent play' : 'parent pause')

        }.onClick(() => {

          this.isPlaying = !this.isPlaying

        })

      }

    }

  }

  @Component

  struct PlayButton {

    @Link buttonPlaying: boolean

    build() {

      Column() {

        Button() {

          Text(this.buttonPlaying? 'child play' : 'child pause')

        }.onClick(() => {

          this.buttonPlaying = !this.buttonPlaying

        })

      }

    }

  }

12、@Observed 和 @ObjectLink

由于 @State无法监听到类嵌套类的属性,@ObjectLink可以监听到类嵌套@Observed修饰的类的属性

@Observed

用于修饰类,表示该类中的数据变更被UI页面管理。

@ObjectLink

  • 用于修饰变量,并且不可以初始化

  • 不能用在@entry修饰的组件中

  • 装饰的变量类型必须为类(class type),并且该类要被@Observed装饰器所装饰。

  • @ObjectLink装饰的变量是不可变的(immutable)。

  • 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知去重新渲染。
  • @ObjectLink装饰的变量不可设置默认值。
  • 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume所装饰变量参与的TS表达式进行初始化。
  • @ObjectLink装饰的变量是私有变量,只能在组件内访问。

@Observed class ClassA {

  public numA: number

  public classB: ClassB



  constructor(cValue: ClassB, aValue: number = 0) {

    this.classB = cValue;

    this.numA = aValue;

  }

}



@Observed

class ClassB {

  public numB: number



  constructor(aValue: number = 0) {

    this.numB = aValue;

  }

}



@Entry

@Component

struct Observer_page {

  @State classA: ClassA = new ClassA(new ClassB());



  build() {

    Column() {



      Text('父组件').fontSize(24).margin({ top: 20 })

      Text(`ClassA的numA值:${this.classA.numA}`)

        .fontSize(20)

        .fontWeight(FontWeight.Bold)

        .onClick(() => {

          this.classA.numA += 1

        })



      Text('父组件不会刷新').fontSize(16).margin({ top: 20 })  .fontColor('#FF0000')

      Text(`ClassB的numB值:${this.classA.classB.numB}`)

        .fontSize(20)

        .fontWeight(FontWeight.Bold)

        .onClick(() => {

          // 修改值,只会导致 @ObjectLink 装饰的变量所在的组件被刷新,当前组件不会刷新

          this.classA.classB.numB += 1

        })



      Text('子组件').fontSize(24).margin({ top: 20 })



      Observer_page2({ classB: this.classA.classB })

    }

    .width('100%')

    .height('100%')

  }

}



@Component

struct Observer_page2 {

  @ObjectLink  classB: ClassB



  build() {

    Column() {

      Text('子组件被刷新').fontSize(16).margin({ top: 20 })  .fontColor('#FF0000')



      Text(`ClassB的numB值:${this.classB.numB}`)

        .fontSize(20)

        .fontWeight(FontWeight.Bold)

        .onClick(() => {

          // 修改值,只会导致 @ObjectLink 装饰的变量所在的组件被刷新

          this.classB.numB += 1

        })



    }.width('100%')

    .height('100%')

  }

}

13、@Consume和@Provide

都用于修饰基本数据类型,类,数组,Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。Consume在感知到Provide数据的更新后,会触发当前view的重新渲染。

表1 @Provide

装饰器参数: 是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide("alias")这种形式。
同步机制|:@Provide的变量类似@state,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。
初始值:必须制定初始值。
页面重渲染场景: 触发页面渲染的修改:- 基础类型(boolean,string,number)变量的改变;- @Observed class类型变量及其属性的修改;- 添加,删除,更新数组中的元素。

表2 @Consume

初始值 : 不可设置默认值。

其他属性说明与Provide一致。


@Entry

@Component

struct CompA {

  @Provide("reviewVote") reviewVotes : number = 0;



  build() {

    Column() {

      CompB()

      Button() {

        Text(`${this.reviewVotes}`)

          .fontSize(30)

      }.width(100)

      .onClick(() => {

        this.reviewVotes += 1;

      })

    }

  }

}



@Component

struct CompB {

  build() {

    Column() {

      CompC()

    }

  }

}



@Component

struct CompC {

  @Consume("reviewVote") reviewVotes : number;

  build() {

    Column() {

      Button() {

        Text(`${this.reviewVotes}`)

          .fontSize(30)

      }.width(100).margin(20)

      .onClick(() => {

        this.reviewVotes += 1;

      })

    }

  }

}

14、@StorageProp和@StorageLink

如何将组件变量与AppStorage同步,主要提供@StorageLink和@StorageProp装饰器

@StorageLink装饰器

组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中。类似于@Link装饰器。

@StorageProp装饰器

组件通过使用@StorageProp(key)装饰的状态变量,将与AppStorage建立单向数据绑定,key标识AppStorage中的属性键值。当创建包含@StoageProp的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。AppStorage中的属性值的更改会导致绑定的UI组件进行状态更新。类似于@Prop装饰器。


let varA = AppStorage.Link('varA')

let envLang = AppStorage.Prop('languageCode')



@Entry

@Component

struct ComponentA {

  @StorageLink('varA') varA: number = 2

  @StorageProp('languageCode') lang: string = 'en'

  private label: string = 'count'



  aboutToAppear() {

    this.label = (this.lang === 'zh') ? '数' : 'Count'

  }



  build() {

    Row({ space: 20 }) {



      Button(`${this.label}: ${this.varA}`)

        .onClick(() => {

          AppStorage.Set<number>('varA', AppStorage.Get<number>('varA') + 1)

        })

      Button(`lang: ${this.lang}`)

        .onClick(() => {

          if (this.lang === 'zh') {

            AppStorage.Set<string>('languageCode', 'en')

          } else {

            AppStorage.Set<string>('languageCode', 'zh')

          }

          this.label = (this.lang === 'zh') ? '数' : 'Count'

        })

    }

  }

}

16、@Watch

用于修饰 被@State, @Prop, @Link, @ObjectLink,@Provide, @Consume, @StorageProp, @StorageLink中任意一个装饰的变量,@Watch用于监听状态变量的变化,应用可以注册回调方法。

@Watch用于监听状态变量的变化,语法结构为:

@State @Watch("onChanged") count : number = 0

如上给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。


@Entry

@Component

struct CompWatch {

  @State @Watch("onBasketUpdated") shopBasket : Array<number> = [ 7, 12, 47, 3 ];

  @State totalPurchase : number = 0;



  updateTotal() : number {

    let sum = 0;

    this.shopBasket.forEach((i) => { sum += i; });

    // 计算新的购物篮总价值,如果超过100RMB,则适用折扣

    this.totalPurchase = (sum < 100) ? sum : 0.9 * sum;

    return this.totalPurchase;

  }



  // @Watch cb

  onBasketUpdated(propName: string) : void {

    this.updateTotal();

  }



  build() {

    Column() {

      Button("add to basket").onClick(() => { this.shopBasket.push(Math.round(100 * Math.random())) })

      Text(`${this.totalPurchase}`)

        .fontSize(30)

    }

  }

}

总结

ets中装饰器的作用非常重要,需要在项目中多实践才能更好的熟练应用,理解其中的原理和作用。

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

推荐阅读更多精彩内容