TypeScript 使用指南

TypeScript 是 JS 的超集,强调变量类型。让JS更加接近强类型语言,下图说明了接入 TS 的好处:


TS作用

1、接入流程

  • 安装 tslint 和 typescript
npm i typescript -D
npm install typescript-eslint-parser --save-dev
npm install eslint-plugin-typescript --save-dev // typescript-eslint-parser 对一部分 ESLint 规则支持性不好,故我们需要安装 eslint-plugin-typescript,弥补一些支持性不好的规则
  • 项目根目录添加tsconfig.json
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5", // 编译后的代码转为 es5 以兼容低版本
    "module": "esnext", // 读取的文件采用的都是 ES6 的模块系统
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入
    "experimentalDecorators": true, // 允许使用 装饰器
    "moduleResolution": "node", // 决定如何处理模块
    "strict": true, // 启用所有严格检查选项。 包含--noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes和--strictPropertyInitialization
    "allowJs": true, // 接受JavaScript做为输入
    "skipLibCheck": true, // 忽略所有的声明文件(*.d.ts)的类型检查
    "sourceMap": true,
    "importHelpers": true,// 从tslib导入辅助工具函数
    "downlevelIteration": true,
    "lib": ["es2015", "dom"], // 编译过程中需要引入的库文件的列表
    "jsx": "react" // 在.tsx文件里支持JSX
  },
  "include": [ //读取所有可识别的src目录下的文件
    "./src/**/*"
  ],
  "exclude": ["*.js", "dist", "node_modules", "ios", "android"]  // 不读取这些文件
}

  • 调整 eslintrc 文件
    parser: 'typescript-eslint-parser',
    plugins: [
        'typescript'
    ],
  • 调整 vscode 设置
"eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact" // jsx 支持
  ],
  • 遇到的一些问题:
    1、webpack 中定义了alias后,在文件中使用时报"模块找不到的错误",这时需要在 tsconfig.json 中定义 baseUrl,如
// alias
containers: resolve('src/containers'),
// 某tsx文件中
import Com from 'containers/Com' // 这时会报模块找不到错误
// 配置tsconfig.json 的 baseUrl 可以解决
baseUrl: './src'

2、在 tsx 文件中引入的 scss、css、png等非 js 文件报模块找不到错误,可以在 src 下新建一个 global.d.ts,内容如下:

declare module '*.(scss|png|jpg)' {
  const content: any;
  export default content;
}

3、webpack 的 module.hot 报错,需安装 @types/webpack-env @types/node

npm i -D @types/webpack-env  @types/node

2、基本语法

基础类型

相比于原始JS,多了 any,void,never,元组,枚举

let a:any = 'tom'; // 为编程阶段还不清楚类型的变量指定一个类型
let b:function():void = function(){} // 当一个函数没有返回值时
let c:function():never = function(){ throw Error('error')}  // 返回never的函数必须存在无法达到的终点
let d:[string,number] =['tom',123]  // 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
enum Color = {Green,Red,Blue}  // 枚举类型可以为一组数值赋予友好的名字

// 其他:类型断言(即类型转换)
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// 或者
let strLength: number = (<string>someValue).length;

高级类型

  • 交叉类型 &
    交叉类型是将多个类型合并为一个类型
function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}
  • 联合类型 |
    联合类型表示一个值可以是几种类型之一,如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
function padLeft(value: string, padding: string | number) {
    // ...
}
  • typeof 和 instanceof 类型保护
    TypeScript可以将二者识别为一个类型保护
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

// 去除联合类型中的null,语法是添加!后缀:identifier!从identifier的类型里去除了null和undefined
function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}
  • 类型别名 type
    类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。通常用类型别名定义联合类型,与接口的区别在于没有产生新的类型,且不能被继承和实现
type a = 'string' | 'number' | null

接口 interface

TypeScript的核心原则之一是对值所具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。接口的2个作用:1、类型检查;2、被类继承,强制类实现某种契约

// 基本语法(前 readonly 只读,后 ? 可选属性)
interface 类型名称 {
   ...
}
  • 定义对象接口
interface SquareConfig {
    color?: string;
    readonly width?: number;
    [index: string]: number;
    [propName: string]: any;
}
  • 定义函数接口
// 函数类型检查器
interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}
  • 定义数组接口
// 数组类型 索引检查器
interface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0]
  • 定义类接口
// 类类型检查器(当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。)
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}
// 等价于
declare class ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}
  • 接口继承接口
interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
  • 接口继承类(会继承到类的private和protected成员)
class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
    select() { }
}
  • 泛型接口
// 函数泛型接口,比普通函数接口更为严格
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
  • 混合接口
    一个对象可以同时做为函数和对象使用,并带有额外的属性
// 这个接口既可以当类接口也可当对象接口
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

  • 成员修饰符 public , protected, private
    所以成员默认都是public.
    private成员不能在类外使用,即不能在实例上以及子类上使用
    protected成员可以在子类中使用,不能在自身及其子类的实例上使用。不过可以通过子类的实例方法访问
    若把构造函数声明为protected,则该类只能被子类实例化,本身不能实例化
  • 只读属性 readonly
    只读属性必须在声明时或构造函数里被初始化
  • 参数属性
    参数属性可以方便地让我们在一个地方定义并初始化一个成员,通过用在构造函数上。参数属性通过给构造函数参数添加一个访问限定符来声明
class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
  • 抽象类
    abstract 类中的抽象方法必须被子类所实现
abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

函数

传递给一个函数的参数个数必须与函数期望的参数个数一致

  • 可选参数和默认参数
    可选参数必须跟在必须参数后面
    带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入undefined值来获得默认值
function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}
  • 剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
  • 函数重载
    为同一个函数提供多个函数类型定义来进行函数重载
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

泛型

泛型即类型变量,它是一种特殊的变量,只用于表示类型而不是值

function identity<T>(arg: T): T {
    return arg;
}

我们定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'

第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'

如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

  • 泛型接口
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
  • 泛型类
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
  • 泛型约束
    在泛型中使用 extends 关键字
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

类型兼容性

  • 普通类型和对象
    要将y复制给x,必须保证x的每个属性都在y中,且类型匹配。对于y多余的属性则不管。
interface Named {
    name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y;
  • 函数兼容
    与上面刚好相反
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

模块

模块的导入导出和ES6一致,import export,但也有新增部分

  • 整体导出
    使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;

// 导入
import zip = require("./ZipCodeValidator");
  • 使用外部模块
    node里大部分模块都不是TS写的,如果要用就需要为这些模块做个TS声明,一般放在.d.ts文件里:
// node.d.ts
declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

现在我们可以/// <reference> node.d.ts并且使用import url = require("url");或import * as URL from "url"加载模块。

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。

declare module "hot-new-module";
import x, {y} from "hot-new-module"; // 该模块的导出所有类型都将会是any
x(y);
  • 导入其他类型文件,如txt,json等
declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

// 
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
  • 扩展模块
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}

// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());


// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

declare global {  // 全局扩展
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

一些操作符和关键字

  • keyof T 索引类型查询操作符
    keyof T的结果为T上已知的公共属性名的联合
  • T[K] 索引访问操作符
    T[K] 返回属性的类型
  • extend 继承
    接口继承接口,接口继承类,类继承类
  • implement 实现
    类实现接口
  • extends 泛型约束关键字
  • new
function createInstance<A>(c: new () => A): A {
    return new c();
}

function createInstance<A>(c: {new(): T; }): A {
    return new c();
}

常见问题

  • 枚举类型key-value互转
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // 2

let colorName: string = Color[Color.Green];  // 'Green'
  • 给回调函数声明类型
type Cb = () => void;
on(type: string, cb: Cb);
  • 自己写 类型声明文件
// 首先声明一下模块:
declare module 'progressbar.js' {
  // 模块中暴露了 Circle 类
  export class Circle {
    constructor(container: HTMLElement, options: Options);
  }
  // 构造函数的 Options 需要单独声明 
  interface Options {
    easing?: string;
    strokeWidth?: number;
    trailColor?: string;
    trailWidth?: number;
  }
}
  • 接口属性定义顺序
// 只读、必选、可选、未知属性
interface iProps {
  readonly x: number;
  readonly y: number;
  name: string;
  age: number;
  height?: number;
  [propName: string]: any;
}
  • 小技巧
// 定义一个javascript的对象,key是字符串,value是任意类型
const people:Record<string,any> = {
    name: 'chengfeng',
    age: 10
}

// 将传入的属性变为可选项
interface IPeople {
    title: string;
    name: string;
}

const people: Partial<IPeople> = {
    title: 'Delete inactive users',
};

// 传入的属性变为变成只读
const people: Readonly<IPeople> = {
    title: 'todo list',
    name: chenfeng;
};

const people1: Partial<IPeople>  = { title: 'ts' }; // OK

const people22: Required<IPeople> = { title: 'ts' }; // Error: property 'name' missing

type T = keyof IPeople // -> "name" | "age"

// 结合 react-router-dom 使用
import { withRouter, RouteComponentProps } from 'react-router-dom';

class App extends React.Component<IProps & RouteComponentProps<{}>, AppStates> {}
export default withRouter(App);
  • any 与 object 类型的区别
    Object类型的变量只是允许你给它赋任意值,但是却不能够在它上面调用任意的方法,即便它真的有这些方法;any 可以
  • readonly 与 const
    做为变量使用的话用const,若做为属性则使用readonly
  • 对象字面量的TS检测
    对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。可以用索引签名解决这个问题:
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;  // 可包含任何属性
}
  • 类作为接口
    类可以产生接口,类分为静态部分和实例部分
class Clock implements ClockInterface {
  currentTime = new Date()
  setTime(d: Date) {
    this.currentTime = d
  }
  reset() {}
  a = 1
  constructor(h: number, m: number) {}
}

let clock: typeof Clock = Clock   // typeof Clock 取得是 构造函数 的类型

3、React 中使用的最佳实践

  • vscode 安装 Typescript React code snippets 插件
  • 所有用到 jsx 语法的文件都需要以 tsx 后缀命名
  • 使用组件声明时的Component<P, S>泛型参数声明,来代替PropTypes!
  • 全局变量或者自定义的 window 对象属性,统一在项目根下的 global.d.ts 中进行声明定义
  • 对于项目中常用到的接口数据对象,在schemas/目录下定义好其结构化类型声明
  • 类组件的声明
class App extends Component<IProps, IState> {
    static defaultProps = {
        // ...
    }
    
    readonly state = { // 使用 class properties 语法对state做初始化时,会覆盖掉Component<P, S>中对state的readonly标识,所以需要显示写上 readonly
        // ...
    }; 
    // 小技巧:如果state很复杂不想一个个都初始化,可以结合类型断言初始化state为空对象或者只包含少数必须的值的对象:  readonly state = {} as IState;
}
  • 函数式组件的声明
// SFC: stateless function components
// v16.7起,由于hooks的加入,函数式组件也可以使用state,所以这个命名不准确。新的react声明文件里,也定义了React.FC类型^_^
const List: React.SFC<IProps> = props => null
  • 正确的声明高阶组件
import { RouteComponentProps } from 'react-router-dom';
 
// 方法一
@withRouter
class App extends Component<Partial<RouteComponentProps>> {
    public componentDidMount() {
        // 这里就需要使用非空类型断言了
        this.props.history!.push('/');
    }
    // ...
});
 
// 方法二
@withRouter
class App extends Component<{}> {
    get injected() {
        return this.props as RouteComponentProps
    }
 
    public componentDidMount() {
        this.injected.history.push('/');
    }
    // ...

interface IVisible {
    visible: boolean;
}
 
 //排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType<Self & IVisible>): React.ComponentType<Omit<Self, 'visible'>> {
    return class extends Component<Self> {
        render() {
            return <WrappedComponent {...this.props}  visible={true} />
        }
    }
}
  • react-router-dom 中常用 RouteComponentProps,react中常用的 ReactNode
// IProps 常用写法
interface IProps extends RouteComponentProps {
  title: string | ReactNode
  style: any
}
// 或者
type IProps = {
  style: any
  config: {}[]
}

// IState 状态声明常用写法
const defaultState = {$1}
type IState = Readonly<typeof defaultState>
  • 在组件定义的地方定义接口,而不是使用的时候,否则需要定义两遍
// 定义处
type IProps = {
  style?: any
  config: {}[]
}

export default class Paragraph extends PureComponent<IProps, {}> {
  render() {
    const { style, config } = this.props
    return (
      <div className={styles.layout} style={style}>
        {config.map((item, index) => (
          <p className={styles.paragraph} key={index} style={style}>
            {item}
          </p>
        ))}
      </div>
    )
  }
}

// 使用处
<Paragraph config={this.config.subTitle} />
  • 泛型IProps
    上述IProps中,如果config是多类型的对象数组,则可以这样
type IProps<T> = {
  style?: any
  config: T[]
}

export default class Paragraph<T> extends PureComponent<IProps<T>, {}> {
  render() {
    const { style, config } = this.props
    return (
      <div className={styles.layout} style={style}>
        {(config as T[]).map((item, index) => (
          <p className={styles.paragraph} key={index} style={style}>
            {item}
          </p>
        ))}
      </div>
    )
  }
}

参考

https://github.com/plantain-00/blogs/issues/28
https://tasaid.com/blog/20171102225101.html
https://juejin.im/entry/5a156adaf265da43231aa032
typescript 高级技巧
https://mp.weixin.qq.com/s/_lO3cd0FcF0Dg3TRnHPdwg)
TypeScript 模块解析

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,068评论 1 32
  • 周末的早晨10:00左右,饿到肚子扁,终于等到懒懒的小主起床了…… 跟着小主pp后走来走去,心里想:“快给我准备早...
    小布瓜啊哈阅读 198评论 0 0
  • 2018年6月12 星期二 晴 午饭时。娘俩边吃边聊,说她们组今中午升格了。学了数学广角---推理,老师讲了...
    朱嘉怡妈妈阅读 243评论 0 0
  • 谢娜越来越漂亮了,基于这点我开始回头去找谢娜变漂亮的原因。 事业顺遂,湖南广电一姐,有为自己量身定做的节目。感情如...
    晚来Yvonne阅读 4,196评论 53 81
  • 乘黄阅读 145评论 0 0