TypeScript学习

  TypeScript给JS添加了类型系统,使我们在写JS的时候,也可以为其指定类型,成静态语言了。但也正是因为增加了类型系统,原先能直接运行JS的浏览器和node.js并不能直接运行文件了,需要编译。TS给JS增加了类型,但也增加了复杂度,运行之前要先编译,增加了一个编译步骤,因此学习TS,要学习两部分,一部分是类型, 一部分是编译。类型,大家都很熟悉了,数字,字符串,布尔等 。编译,如果学过C, JAVA等静态语言,也很熟悉,就是使用编译器,把源文件转化成另外一种文件,在TS中,就是编译器把TS文件,转化成JS文件。那还要装个编译器吗? 是的, 不过,它是一个npm 包, 就叫typescript,   它提供了tsc命令,可以把TS文件编译成JS文件。mkdir ts-learning && cd ts-learning && npm init -y 新建ts-learning项目, npm install typescript -D安装tsc编译器。TS文件的后缀名是.ts, 所以touch type.ts 写TypeScript程序。写程序的第一个问题就是,TS是怎么给JS提供类型系统的?指定类型或类型推断,指定类型就是:(变量名:变量类型),如下图,TS提供了各种各样的类型

let str: string;
let a: number = 3;

  类型推断,就是不指定类型,让TS去推断类型,比如let a = 3; TS会自动推断出a的类型是数字类型。在VS Code中,把鼠标放到变量a上时,就可以看到,TS已经会推断出它的类型是数字类型。

  尽量让TS去推断类型,只有当TS不能正确地推断类型时,我们才指定类型,这也算是一种最佳实践吧。代码写完了,要运行,那就必须先编译, 运行编译后的代码。tsc有两种使用方式, 一种是命令行给参数,tsc type.ts -- --, 一种是使用tsconfig.json 配置文件,执行tsc 命令时,它会从项目根目录中寻找tsconfig.json, 然后根据配置文件进行编译。推荐使用tsconfig.json配置文件。配置文件可以手动输写

{
  "compilerOptions": {
    "rootDir": "./src",   /* .ts文件的根目录 */
    "outDir": "dist",     /* 编译后的文件放到哪里 */
    "target": "ES2015",   /* 编译到目标语法*/
    "module": "commonjs"   /* 编译后文件使用哪一种规范*/
  }
}

  也可以npx tsc --init 生成配置文件,自己再手动修改不符合项目需求的地方。配置文件就是告诉编译器,源文件在什么地方,要编译到什么地方,怎么编译等等。npx tsc 进行编译,项目就多出来了dist目录。node dist/type.js就可以运行程序了, 可以写一个命令。

 "scripts": {
    "start": "tsc && node dist/type.js"
  },

   为什么要为JS添加类型呢? 因为类型是对值和操作的限定,比如数组就不能相加,只有数字才能相加,一个对象如果没有属性,就不要去取属性的值。如果违反了规定,就会报错。

  在VS Code中可以看到标红了,报错了。鼠标移动上去,可以看到报错的原因。此时使用tsc 进行编译,也会报相同的错误。tsc在把TS编译成JS的过程中,会进行类型检查,如果不符合类型要求,就会报错。可以发现,有了类型后,可以尽早地发现问题,尽可能地把运行时发生的错误在编译时捕获,有利于写出安全运行的代码。VS Code 的实时提示,更利于写代码。它是怎么做到的?当安装typescript包时,也安装了tsserver(TypeScript Language Service), 编辑器或IDE利用tsserver来完成标红提示错误等功能。当你编写TS代码时,编辑器会时时地与tsserver 进行通信,实实编译代码(编译到内存中)。这时要注意TS版本,默认情况下,VS Code使用它自带的tsc编译器,当你打开任意一个.ts文件时,在VS Code的右下方,你都能看到这个版本号,

  如果你经常更新VS Code, 一般没有问题,如果不经常更新,可以使用npm 安装的版本。点击这个版本号或它旁边的{},在编辑区的上方会弹出一个选择框,点击选择想要的版本就可以了。

  TS类型带来了好处,也能编译和运行TS文件了,需要系统学习一下类型了。

  any: 任意类型

  当一个变量是any类型时,可以给它赋任何值。

let str: any;
str = 2;
str = 'str';

  同时,any类型的值也可以赋值给任意其它类型。

let a: any = 'string';
let b: number = a;

  当使用any类型时,TS不会进行类型检查,代码很容易出错,所以尽量不要用any 类型。

  unknown:不知道的类型

  为什么会有这种类型呢?在极少的情况下,你事先并不知道一个变量要保存什么类型,需要一个类型标注。你可能会说不是有any吗?确实,可以声明为any类型,但一旦声明为any类型,这个变量就不受控制了,就可以对这个变量进行任何操作,但你肯定不想对一个变量进行任意的操作,报错了怎么办?unknown类型和any类型不同之处就是在于控制操作上。当一个变量是unkonwn类型时,你做的事情并不多,

let a: any;
a = 3;
console.log(a.toUpperCase());

let b: unknown;
b = 3;
// Property 'toUpperCase' does not exist on type 'unknown'. console.log(b.toUpperCase());

  b是unknown类型,编译器直接报错了,toUpperCase属性不存在unknown类型上。如果要使用unknown类型,在使用之前,一定缩小它的类型范围(narrow type)。缩小类型范围的方法有很多,这里使用typeof, 只有字符串才调用toUpperCase()方法,那就判断b是不是字符串

let b: unknown;
b = 3;
if (typeof b === 'string'){
    console.log(b.toUpperCase());
}

  unknown和any一样,可以表示任意的值 ,但后续的操作却完全不同。any可以进行任意的操作,但unknown却要进行类型缩小,也因此更为安全。如果实在是不知道变量的类型,尽量使用unknown,不要使用any。

  null和undefined 类型

  在TS中,null和undefined比较特殊,默认情况下,它们可以赋值给其它任意类型,而不仅仅是null和undefined类型。

const x: number = null; 

  如果此时x调用number的方法,肯定报错,这样不太好,最好是null只能赋值给null类型,undefined只能赋给undefined类型。这要配置strictNullChecks来控制null或undefined是否能赋值给其它类型。strictNullChecks: true 表示禁止赋值给其它类型。当在tsconfig.json中写入strictNullChecks: true

// Type 'null' is not assignable to type 'number'
const x: number = null; 

  如果变量值就是null,那要明显地标注变量是null类型

const x: null = null; 

  字面量类型

  TypeScript 还定义了字面量类型,3也是一个类型。变量类型的声明方式是 变量名:类型,如果类型是一个字面量时,就是字面量类型。

let a: 3;
let b: 'name';
let c: false;

  当变量类型是字面量类型时,它的取值也就固定了。a只能赋值为3,b只能赋值为'name',c只能赋值为false。

  数组类型

  在使用数组时,数组中的元素通常是一个类型,所以数组的类型声明是元素的类型后面跟上[]

let arr: number[]; // arr数组中每一个元素都是number类型

  数组还有一个子类叫元组类型,声明如下

let turple: [number, string];

  [number, string] 就是一个元组类型,和数组类型的区别,就是元素的类型声明不是在[ ] 的外面,而是在里面。这表明,元组中元素都是固定的,也就是元组的长度是固定的,并且元组中的每一个元素的类型也是固定的,所以元组就是一个元素类型固定且长度固定的数组。turple 就是2个元素的元组,元组的第一个元素的类型是number,第二个元素的类型是string。

let arr: number[] = [2, 3, 4, 5];
let turple: [number, string] = [2, 'name'];

  对象类型

  在TS中有4种方式来标示一个变量(或值)是对象类型

let obj1: object = {a: 2}; // 小写object 类型
let a: {b: number} = { b: 12 }  //对象字面量语法,{b: number}类型
let danger: {} // {} 类型
let obj: Object; // 大写的object类型。

  先看第一种,小写的object类型。

let obj1: object = {a: 2};
// Property 'a' does not exist on type 'object'
obj1.a;

  获取obj1对象上a属性,发现报错了,object类型上不存在属性a。当声明一个变量是object类型时,只表示它是一个object,而不是null,再也没有其它过多信息了。你对object类型(obj1)什么也做不了。

  再看{} 类型和Object类型。

let danger: {};
danger = {};
danger = {x: 1}
danger = [];
danger = 2;

let Obj: Object;
Obj =1;
Obj = {x: 1};

  除了null和undefined以外的类型都能赋值{} 类型和Object类型,这两种声明方式没有什么用。

  以上三种声明对象类型的方式,几乎没有什么用,就只剩下对象字面量的声明方式。对象字面量语法声明对象类型,声明的是对象的形状,对象包含哪些属性。这样声明对象类型的方式,叫作structurally typed(duck typing )。structurally typed(duck typing )是一种编程风格,它只关心对象有哪些属性,而不关心它名义上的类型。

  当把一个对象字面量赋值给一个对象类型变量时,TS会执行非常严格的类型检查。对象字面量中的属性,既不能比对象类型中的定义的属性多,也不能比类型中的定义的属性少,必须一一对应,否则报错,这叫excess property check

let obj: {b: number} = {
    b: 123,
    /* 
    Type '{ b: number; a: number; }' is not assignable to type '{ b: number; }'.
    Object literal may only specify known properties, and 'a' does not exist in type '{ b: number; }'
    */
    a: 456
}

  但这会带来问题,有的对象有某个属性,有的对象却没有这个属性,有的对象有很多属性,有的对象却没有这些属性,但它们都有共有的属性a,这样怎么声明,可使用?: 标示某个属性可有可无,也可以使用索引表达式 [key: number]: boolean。 key(可以取任意名字)表示对象的属性,是number类型,值是布尔类型。

let obj: {
    b: number
    c?: string
    [key: number]: boolean
}

   obj 必须有b属性,且是number类型。c属性可能有也可能没有,如果有c属性,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。

obj = {b: 3};
obj = {b: 2, c:undefined};
obj = {b: 2, 10: false}
obj = {10: false} // 报错,没有b属性。

  函数类型

  无论是函数声明,还是函数表达式,在写函数的过程中可以直接定义其参数类型和返回值。

function sum(a: number, b: number): number {
    return a + b;
}
const substract = (a:number, b:number):number => a - b;

  参数a,b都是number类型,参数列表() 后面跟 :number,表示函数的返回值类型,是number类型。如果TS能推断出函数的返回值类型,返回值类型可以省略。

function sum(a: number, b: number) {
    return a + b;
}

  当你把鼠标放到sum上时,可以看到TS已经推断出函数的返回值是number类型。如果参数类型也不写呢?

function sum(a, b) {
    return a + b;
}

  鼠标放到a上,a是any类型。当TypeScript不能推断一个变量的类型时,它就默认变量的类型为any。这叫隐式any,因为你没有写any这个关键字,变是却是any类型。由于any类型,容易引起问题,所以最好不要让TS做这种类型推断。如果参数确实是any类型,要显示地标示出来。配置项noImplicitAny 就是阻止TS做这种类型推断的。当在tsconfig.json文件中配置noImplicitAny: true时,如果你没有为变量标注类型,而是让TS推断出了它的类型是any时,TS就会报错。

function sum(a, b) {// Parameter 'a' implicitly has an 'any' type.
    return a + b;
}

  如果函数没有返回值呢?返回值类型是void

function print(a: string): void {
    console.log(a);
}

  当函数抛出错误呢,返回值是never类型,永远不会发生。never类型是所有类型的子类型,你可以把never类型赋值给任意其它类型。

function erorr(): never {
    throw new Error('Error');
}

  有时函数参数是默认参数或可能传参,也可能不传参的参数,参数类型用?: 表示。

function request(url: string, method?: string){}

  函数中还有一个特殊的存在this,函数调用方式不同,this指向就不同,这也是问题的所在,所以函数中有this时,最好声明this的期望类型。在TypeScript中,可以在参数列表中声明this,不过,this要作为第一个参数存在,其它参数放到它后面

function fancyDate(this: Date, name?: string) {
    return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`
}

fancyDate.call(new Date)

  当函数作为参数传递,或者作为函数返回值时,就不能用这种方式声明类型了,那就需要自定义类型了。

  使用type自定义类型

  使用type自定义类型,格式为:  type 类型名称 = 类型。 以sum函数为例,它的类型是什么呢?两个number类型参数,返回一个number类型,类型就可以这么写

(a:number, b:number ) => number

  很像箭头函数的语法,参数列表 => 返回值类型。再取个名字Sum,自定义类型就是

type Sum = (a:number, b:number ) => number;

   Sum就是一个自定义的函数类型,只要一个函数接受两个number类型的参数,并返回number类型,这个函数就可以用Sum 标示。

let sub: Sum = (a, b) => a - b;

  同时可标注参数或返回值

function runSum(fn: Sum, a:number, b: number) {
    return fn(a, b);
}
function returnSum(): Sum {
    return  (a, b) => a + b ;
}

returnSum()(7, 8);

  type Sum = (a:number, b:number ) => number; 是Sum类型的简写形式,更完整的形式如下

type Sum = {
    (a: number, b: number): number
}

  因为在JS中函数是一个对象,对象就会有属性,声明一个函数类型,也就是声明一个对象类型,就可以用对象字面量的方式声明它。

  使用Type 来定义类型的时候,有两个重要概念,type union(联合类型),type insertection(交叉类型)。

  type union(联合类型)

  联合类型:两个或多个类型使用 | 联合在一起,形成一个新的类型。

type StrOrNum =  string | number;

   StrOrNum 或是string类型,或是number类型。

 

 

 

使用这个联合类型时,只能使用string和number类型的共用的属性,两者很少共有属性。可以把两个对象类型联合起来

type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean, wags: boolean}
type CatOrDogOrBoth = Cat | Dog

  当你看到一个类型是CatOrDogOrBoth的时候, 你想到了什么?保险起见,我只使用它们共有的属性name,这个类型肯定有一个name属性,其它属性有没有,不敢保证。反过来想,既然这个类型是CatOrDogOrBoth,我可以给它赋一个cat值,也可以给它赋一个Dog值,共有的值也可以赋给它。

let c:CatOrDogOrBoth = {name: 'cat', purrs: false } // Cat类型
let d:CatOrDogOrBoth = {name: 'dog', barks: false, wags: false } // dog类型
let cdBoth: CatOrDogOrBoth = { name: 'both', barks: true, purrs: true, wags: true}

  类型联合时要注意,如果两个类型中有相同的属性,但属性却是不同的类型时,属性的两个类型也进行联合(union)

type Product = {
    id: number,
    name: string,
    price?: number
};
type Person = {
    id: string,
    name: string,
    city: string
};

// Product 和 Person 联合之后的结果
type UnionType = {
    id: number | string,
    name: string
};

  UnionType就是Product和Person类型进行联合后生成的新类型。在UnionType中,id属性的类型是联合类型 number | string ,就是因为id 在Product中是number 类型,但是在Person类型中是string类型.  name属性在两个类型中都是string, 所以在新的联合类型中,name属性也是string

  type insertection(交叉类型)

  type insertection(交叉类型):两个或多个类型使用 & 联合在一起,形成一个新的类型。

type strAndnum =  string & number;

   strAndnum,既是string,又是number,新的类型集合了string类型 和number类型的所有属性。正是由于把所有的属性都集合到了一起,你会发现,没有一个值既是string,又是number,所以这个类型没有意义。通常,&用于对象类型。

type Person = {
    id: string;
    name: string;
    city: string;
}

type Employee = {
    company: string;
    dept: string
}

type PersonAndEmpoyee = {
    id: string;
    name: string;
    city: string;
    company: string;
    dept: string
}

  PersonAndEmpoyee就是PersonEmpoyee进行交叉生成的新的类型。你可以看到,type insertection(交叉类型) 就是把所有对象类型的属性全部组合到一起,允许你使用所有的属性,而不是共有类型。

   当给一个交叉类型进行赋值时,值必须包含每一个类型中都定义的属性

let bob: Person & Employee = {
    id: "bsmith", name: "Bob", city: "London",
    company: "Acme Co", dept: "Sales"
};

  如果两个要交叉类型有相同的属性时,这时要注意。如果相同的属性,类型也相同,这没有问题。给Empoyee再声明一个id:string属性,Person & Employee 的结果还是一样  

 

 

   但是如果类型不同,这个属性的类型,是两个不同类型进行交叉,分别给Person和Employee 定义一个contact属性,类型不同

type Person = {
    id: string;
    contact: number;
}

type Employee = {
    id: string;
    contact: string;
}

  Person & Employee

type PersonAndEmpoyee = {
    id: string;
    contact: number & string;
}

   这时就出现问题了,没有一个值,它既是number 又是string. 解决办法,就是相同的属性不要使用原始类型,要使用对象。  

type Person = {
    id: string;
    contact: {phone: number};
}

type Employee = {
    id: string;
    contact: {name: string}
}

type PersonAndEmpoyee = {
    id: string;
    contact: {phone: number} & {name: string};
}

   把类型看作是值的集合。比如number类型的是所有数值的集合,42和57是number类型,‘3’就不是number类型。最小的集合不包含任何值,是空集,对应的是never类型,也就是说,不能给never类型的变量赋值。再小的集合是只包含一个值,对应的就是字符串字面量。type A = 'A';  如果想包含几个值,可以使用union 操作符,type AB = 'A' | 'B'。联合类型就是值的集合的并集。从类型是值的集合的角度考虑,能不能给一个变量赋值,就是看值是不是在变量的类型所暗示的值的集合之中,或是不是值的子集。

  TypeScript中的类型是结构化类型,或是鸭式类型,只要值包含类型中定义的属性,就是这个类型,如果值中包含了更多的属性,它也是这个类型,这也是& 和 | 操作符的基础。

type worker = {id: string}; type student = {name: string};    worker1 = {id: 1} 是worker类型,worker2 ={id: 1,  name: 'sam'} 也是worker类型。同理 student1 = {name: 'jason', id: '2'} 也是student类型,集合相交,就是取既是它,又是它的值。worker2既是worker,又是student类型,所以 type ws = worker & student的类型是 {id: string; name: string}

 

lib:如果不设置,ts会有默认的配置,比如dom,它是根据target进行配置的。tsconfig还有一个extends选项,去继承另外一个配置文件,比如npm install  @tsconfig/node16,  配置文件就可以写么写

{
    “extends”: "@tsconfig/node16/tsconfig.json",
    "include": ["src/**/*"]   
}

  此时仍然可以写compilierOptions, 它会和extends里面的配置项合并,

{
    “extends”: "@tsconfig/node16/tsconfig.json",
    “compilerOptions”:  {"outDir": "dist"}
    "include": ["src/**/*"]   
}

编译的依据是tsconfig.json. 如果是在生产环境,只需要编译一次,生成运行的JS。那就是执行tsc, 然后node ./dist/index.js, 也可以直接运行ts-node来运行ts文件。ts-node不会打包到dist, 而是直接在编译在内存,然后执行。但是在开发环境下,如果只改变一点内容,就要切换到命令行中执行一下tsc和node 命令,就有点麻烦了. 刚开始的时候,是开启tsc -w , 然后用nodemon 监听dist, 或 直接临听 ts文件,使用nodemon 执行ts-node。比如nodemon.json

{
    "watch": ["src"],
    "ext": ".ts"   
    "ignore": [],
    "exec": "ts-node --files ./src/server.ts"   
}

  然后在package.json中 “dev”: "nodemon" 。 现在可以使用ts-node-dev, 监听,编译,运行,一起做了。如果是前端项目,需要webpack, rollup.

  debug使用vsCode debug。  如果是node项目上,一种是官网提供了,在上面的tsconfig.json中,添加sourcemap: true.  配置下面的launch.json,

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/src/server.ts",
            "preLaunchTask": "tsc: build - tsconfig.json",
            "outFiles": [
                "${workspaceFolder}/out/**/*.js",
                "!**/node_modules/**"
            ]
        }
    ]
}

  ${workspaceFolder}是VS Code 定义好的变量,指的是当前项目的根路径。除了它,还有${file},当前编辑器的活动文件。${env: Name}: 获取环境变量Name的值。上面的配置信息的意思, VS code 决定执行根目录下的src/ server.ts, 不过它在之前,要先执行tsc:build, 然后告诉调试器,到哪里去寻生成好的js文件,outFiles. 当在原文件中打断点时,VS Code 调试器,就会到outFiles中指定的文件中去找生成的文件。

  也可以直接使用ts-node 进行debug,launch.json的配置文件如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "debug ts node",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "cwd": "${workspaceFolder}",
            "runtimeArgs": ["-r", "ts-node/register"],
            "args": ["${workspaceFolder}/src/server.ts"],
        }
    ]
}

 cwd: 当前工作目录,用来 runtimeArgs: 运行node 时传递提供的选项,args 是运行node时,提供的参数,注册ts-node cli 来处理ts文件,运行 src/server.ts. 如果喜欢使用chrome 来debug node程序,可以执行 node -r ts-node/register --inspect src/server.ts, 然后打开chrome,输入chrome://inspect

 

当一个React 组件接受一个children 时, children 是什么类型?它可能是JSX.Element, JSX.Element[], React.ReactNode, React.ReactChildren, React.ReactChild[]. JSX.Element 只适合children返回的root组件,只包含一个子元素。React.Reactchildren和JSX.Element差不多,React.ReactNode 就是html差不多,它可以string, 可以是React 元素

如果一个组件还接受style 自定义样式, 那style属性怎么定义? 使用React.CSSproperties

const BoxProps = {children: React.ReactNode, style?: {React.CSSProperties}} .  当使用的时候 const Box = ({children, style={}) = <div>ddddd</div>

useReducer: 当提供useReduce初始值的时候,仍然不能推断出它返回的类型。

  真正对state起作用的是reduer函数,reducer 函数中,第一个参数是state是什么类型,返回的state就是什么类型。如果intialState的类型和reducer中state的类型,不一致,还会报错。

reducer中的action 就是一个对象,有type, payload,  就是要dispach的对象,type一般都是类型的交集, 比如 "ADD" | "SUB", playload就是携带的数据类型 type Action = {type: "ADD" | "SUB", playload: number}. useReducer中返回的dispatch的类型就是 Dispatch<Action> 它的类型是Action的类型

  React Context: 是在组件外定义的,但是React Context的value 的值却是在组件内获取的。所以在给React Context 定义类型时候, 它肯定会带上组件内有属性,比如setState, 比如 type ContextType= {count: number; setState: (value: number) => void} . 当创建React.Context的时候, 需要默认值, const context = React.CreateContext<ContextType>(), 参数是无法提供setState的,有一个简单的办法是使用as。const context = React.createContext<ContextType>({} as ContextType).

  高阶组件:一个组件可以接受一个组件作了为参数,参数应该怎么写呢? React.componentType<> <>里面 就是组件可以接受的参数。高阶组件,

export interface AdjustmentInputProps {
  id: string;
  label: string;
  value: number;
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

export interface ColorAdjustmentProps {
  Adjustment: React.ComponentType<AdjustmentInputProps>;
}

export const ColorAdjustment = ({ Adjustment }: ColorAdjustmentProps) => {

  return (
    <section className="color-sliders">
      <Adjustment
        id="red-slider"
        label="Red"
        value={red}
        onChange={adjustRed}
      />
    </section>
  );
};

  高阶组件的使用

import * as React from 'react';

import { ColorAdjustment } from './ColorAdjustment';
import { ColorInput } from './ColorInput';

const Application = () => {
  return (
    <main >
      <ColorAdjustment Adjustment={ColorInput} />
    </main>
  );
};

export default Application;

  那么ColorInput就应该有AdjustmentInputProps的属性。

import { AdjustmentInputProps } from './ColorAdjustment';

export const ColorInput = ({
  id,
  label,
  value,
  onChange
}: AdjustmentInputProps) => {
  return (
    <div className="color-input">
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type="number"
        min="0"
        max="255"
        value={value}
        onChange={onChange}
      />
    </div>
  );
};

 

  React.HtmlProps:

 

 

 

  模版字面量类型: type VerticalAlignment = 'top' | 'center' | 'bottom'; type HorizonalAlignment = 'left' | 'center' | 'right';

     type Alignment = `${VerticalAlignment}-${HorizonalAlignment}` 

 

 

  

 

posted @ 2021-09-30 18:09  SamWeb  阅读(358)  评论(0编辑  收藏  举报