文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

会写 TypeScript 但你真的会 TS 编译配置吗?

2024-12-02 14:24

关注

最近遇到了挺多涉及到前端“编译”方面的工作,其中关于 TypeScript 的编译会涉及到关于 tsconfig.json 文件的配置,由于配置项繁杂,遂逐一解析并验证,减少大家的一些疑惑,并提升工作效率!

随着 TypeScript 的流行,越来越多的项目通过使用 TypeScript 来实现编写代码时候的类型提示和约束,从开发过程中减少 BUG 出现的概率,以此提升程序的健壮性和团队的研发效率。

为什么会单独写一篇文章来讲述 tsconfig.json 文件的配置呐?原因是笔者在做 TS 项目的时候,由于对其中的配置项不熟悉,搞来搞去,搞好久,烦死了!所以决定好好梳理下。

越来越多的项目用上了 TypeScript,因此如何按需配置 tsconfig 也应该是前端工程师需要掌握的技能之一。

本文内容结构如下,朋友们按需食用:

 

一、前置知识 

在熟悉掌握 tsconfig.json 文件配置前,先给首次接触 TS 的同学预备一下“前置知识”。

1.1 TypeScript 是什么?

TypeScript 官网:https://www.typescriptlang.org

TypeScript 是一种基于 JavaScript 的强类型编程语言,它使得在前端项目开发过程中更加严谨且流畅,一定程度上保证了大型前端项目程序的健壮性。

但是 TypeScript 并不可以直接运行,而是需要转换成 JavaScript 代码才可以在 Node.js 或浏览器环境下执行,因此我们需要通过“编译器”将 TS 代码转换为 JS 代码。

1.2 什么是 tsc ?

tsc 的全称是 TypeScript Compiler,也就是将 TypeScript 转码为 JavaScript 代码的编译器。

tsc 的全局安装方式:

  1. npm install typescript -g 

当我们编译一份 index.ts 文件时,会使用下面的命令:

  1. tsc ./index.ts 

这样就可以得到一份编译成为 JavaScript 代码的 ./index.js 文件。

tsc 实际就是将 TS 转为 JS 的编译(器)脚手架工具,如果是一个 TS 的前端工程项目,那么就可以通过项目中的 tsconfig.json 文件来自定义配置 TS 编译相关规则。

项目中的 tsconfig.json 文件,我们一般会通过如下快捷命令生成:

  1. tsc --init 

执行完后,会在项目根目录生成一个简单的初始化 tsconfig.json 配置描述文件,如果没有特别的要求,该初始化配置就足以支持你愉快地使用 TS 开发啦!

更多相关 TS 编译配置和使用说明可以通过 tsc -h 查看。

1.3 tsconfig.json 文件

tsconfig.json[1] 文件是用于描述将 TypeScript 转为 JavaScript 代码的配置文件。

IDE(代码编辑器)将会根据 tsconfig.json 文件来对当前项目中支持不同程度的类型约束,同时也是对 TSC 编译 TypeScript 代码过程做一些预定义、约束入口和编译输出目录等配置。

因此对于一个支持 TypeScript 编程语言的工程来说,tsconfig.json 文件就是编码的基础。

二、tsconfig.json 配置详解 

有了上面的前置知识作为基石,相信大家会对 tsconfig.json 文件的配置项也会更加容易理解。

tsconfig 协议

 

笔者将从常见的配置项单独解释,然后在最后会将一些不常用的配置统一解释,朋友们可以将这篇文章收藏一下,可当作一份 tsconfig 配置的中文查询对照表 👀。

2.1 files

files 字段用于指明需要 tsc 编译的一个或多个 ts 文件,例如:

  1.   "files": ["index.ts""global.d.ts"], 

当指定的文件或文件夹不存在时,会提示 ❌ 错误!

2.2 include

include 字段用于指明需要被 tsc 编译的文件或文件夹列表,例如:

  1.   "include": [ 
  2.     "src"
  3.     "global.d.ts" 
  4.   ], 

2.3 exclude

exclude 字段用于排除不需要 tsc 编译的文件或文件夹列表,例如:

  1.   "exclude": ["test.ts""src/test.ts"], 

注意: exclude 字段中的声明只对 include 字段有排除效果,对 files 字段无影响,即与 include 字段中的值互斥。

如果 tsconfig.json 文件中 files 和 include 字段都不存在,则默认包含 tsconfig.json 文件所在目录及子目录的所有文件,且排除在 exclude 字段中声明的文件或文件夹。

2.4 compileOnSave

compileOnSave 是声明是否需要在保存时候自动触发 tsc 编译的字段,一般来说,我们的代码编译过程会通过 Rollup、Webpack 等打包构建工具,并且使用热更新,因此无需配置该项,保持缺省即可。

  1.   "compileOnSave"false

2.5 extendsextends

字段用于指明继承已有的 tsconfig 配置规则文件。

该字段可以说是非常有用了,因为我们的 tsconfig 配置其实各个项目之间大同小异,因此完全可以结合自己团队的情况,抽离一个基础且公共的 tsconfig 配置,并将其发包,然后作为 extends 字段的值来继承配置。

tsconfig 推荐默认配置可以参考官方的包:@tsconfig/recommended[2]

@tsconfig/recommended 的配置如下:

  1.   "compilerOptions": { 
  2.     "target""ES2015"
  3.     "module""commonjs"
  4.     "strict"true
  5.     "esModuleInterop"true
  6.     "skipLibCheck"true
  7.     "forceConsistentCasingInFileNames"true 
  8.   }, 
  9.   "$schema""https://json.schemastore.org/tsconfig"
  10.   "display""Recommended" 

例如继承一个发包后的 tsconfig 基础配置,并通过显示声明编译的目标代码版本为 ES2016 来覆盖覆盖 @tsconfig/recommended 中对应配置项。

  1.   "extends""@tsconfig/recommended/tsconfig.json"
  2.   "compilerOptions": { 
  3.     "target""ES2016" 
  4.   } 

作为一些实践经验,社区也提供了一些常见环境(例如:Nuxt、Vite、Node 等)最佳实践后的基础配置,推荐参阅:https://github.com/tsconfig/bases/[3]

2.6 compilerOptions

compilerOptions 是一个描述 TypeScript 编译器功能的“大”字段,其值类型是“对象”,因此包含了很多用于描述编译器功能的子字段,其子字段的功能如下:

(1). target

target 字段指明经过 TSC 编译后的 ECMAScript 代码语法版本,根据 ECMAScript 语法标准,默认值为 ES3。

TypeScript 是 JavaScript 的超集,是对 JavaScript 语法和类型上的扩展,因此我们可以使用 ES5、ES6,甚至是最新的 ESNext[4] 语法来编写 TS。例如当我们使用 ES2021 语法来编码 TS 文件,同时配置如下:

  1.   "compilerOptions": { 
  2.     "target""ES5"
  3.   } 

则会将对应使用了最新 ECMAScript 语法的 TS 文件编译为符合 ES5 语法规范的 *.js 文件。

延伸一下知识点,思考一下 tsc 是如何将高版本(ECMAScript 规范)代码向低版本代码转换的?这个转换的结果靠谱吗?与 Babel 有何差异?

一图看 ECMAScript 各版本功能差异

另外对于个版本差异有想简单了解的👬,可以阅读《1.5万字概括ES6全部特性[5]》

通过一个实验,在 src/index.ts 文件中使用了 Map、Async/Await、Promise、扩展运算符,并在 tsconfig.jon -> target 设置为 ES5:

验证 target 降级处理

然后发现在右侧的 dist/index.js 文件中,依然存在 new Map() 、Promise 语法,因此可以得出结论:tsc 的代码降级编译并不能完全处理兼容性。

通过官方文档了解到:

这里提到了 lib 字段,意思是 target 不同的值会有对应默认的 lib 字段值,当然也支持开发者显示指明 lib 字段的值,那么接下来看看 lib 是干嘛的吧!

(2). lib

lib 字段是用于为了在我们的代码中显示的指明需要支持的 ECMAScript 语法或环境对应的类型声明文件。

例如我们的代码会使用到浏览器中的一些对象 window、document,这些全局对象 API 对于 TypeScript Complier 来说是不能识别的:

lib 未显示引入 DOM 会提示类型错误

因而需要在 lib 字段中如下配置:

  1.   "compilerOptions": { 
  2.     "target""ES5"
  3.     "lib": ["ES5""ES6""DOM"], 
  4.   } 

来显式引入在 DOM 即浏览器环境下的一些默认类型定义,即可在代码中使用,window、document 等浏览器环境中的对象,TS 在运行时以及编译时就不会报类型错误。

引入类型定义后无错误提示

综合 target 和 lib 字段的实际功能表现,我们可以得出结论:

TSC 的编译结果只有部分特性做了 pollyfill 处理,ES6[6] 的一些特性仍然被保留,想要支持完全的降级到 ES5 还是需要额外引入 pollyfill(也就是我们在项目的入口文件处 import 'core-js'),但建议是将 target 字段值设置为 ES6,提升 TSC 的速度。

因此,笔者对于使用 TSC 编译的观点是:

不应该将 TSC 作为编译项目的工具,应该将 TSC 作为类型检查工具,代码编译的工作尽量交给 Rollup、Webpack 或 Babel 等打包工具!

另外推荐阅读《为什么说用 babel 编译 typescript 是更好的选择》

(3). module

module 字段指明 tsc 编译后的代码应该符合何种“模块化方案”,可以指定的枚举值有:none, commonjs, amd, system, umd, es2015, es2020, 或 ESNext,默认值为 none。

在如今的前端开发趋势来讲,主要是使用 ESM、CommonJS、UMD、IIFE 四种模块化方案,未来会趋向于 ESM,当然我们会根据项目的应用场景来决定使用何种模块化方案,例如:NodeJS 使用 CommonJS,浏览器里可以使用 ESM,不过现在的打包工具,会自动处理 CommonJS 和 ESM 的差异,并包装成符合指定模块化规范的代码,

在 tsconfig.json 可以设置 allowSyntheticDefaultImports 字段为 true,来允许合成默认导入。

(4). esModuleInterop

简单来说,就是支持合成默认导入。

在前端项目开发时,使用 ESM 编写代码引入了 CJS 的模块,由于 CJS 模块没有默认导出内容,因此需要通过我们的工具去自动化合成 CJS 的默认导出,以支持在 ESM 下流畅开发。

参阅文章《esModuleInterop 到底做了什么?[7]》,讲得非常详细也非常好。

当 esModuleInterop 字段设置为 true 时候,上述提到的 allowSyntheticDefaultImports 字段也会自动设置为 true。

(5). moduleResolution

moduleResolution 声明如何处理模块,枚举值:classic、node,会根据 module 字段决定默认值。

推荐手动设置为 node,更符合现在大家的编码认识一些,而且大部分的构建打包工具都是基于 Node。

举个,遇到 import {a} from 'a-lib'; 这样的模块引入代码应该如何去(解析)查找到对应的模块文件。

(6). baseUrl & paths

baseUrl:设置基本目录以解析非绝对模块名称(定义一个根目录,以此进行绝对文件路径解析)

paths:用于设置模块名或路径映射列表,这样就可以简写项目中自定义模块的文件路径。

举一个 :

  1.   "compilerOptions": { 
  2.     // 注意:baseUrl 必选,与 paths 成对出现,以 tsconfig.json 文件所在目录开始 
  3.     "baseUrl"".",  
  4.     "paths": { 
  5.       // 映射列表 
  6.       "@ 
  7.     "target""es6", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5''ES2015''ES2016''ES2017'or 'ESNEXT' 
  8.     "module""commonjs", // 指定使用模块: 'commonjs''amd''system''umd' or 'es2015' 
  9.     "lib": [], // 指定要包含在编译中的库文件 
  10.     "allowJs"true, // 允许编译 javascript 文件 
  11.     "checkJs"true, // 报告 javascript 文件中的错误 
  12.     "jsx""preserve", // 指定 jsx 代码的生成: 'preserve''react-native'or 'react' 
  13.     "declaration"true, // 生成相应的 '.d.ts' 文件 
  14.     "declarationDir""./dist/types", // 生成的 '.d.ts' 文件保存文件夹 
  15.     "sourceMap"true, // 生成相应的 '.map' 文件 
  16.     "outFile""./", // 将输出文件合并为一个文件 
  17.     "outDir""./dist", // 指定输出目录 
  18.     "rootDir""./", // 用来控制输出目录结构 --outDir. 
  19.     "removeComments"true, // 删除编译后的所有的注释 
  20.     "noEmit"true, // 不生成输出文件 
  21.     "importHelpers"true, // 从 tslib 导入辅助工具函数 
  22.     "isolatedModules"true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似). 
  23.  
  24.      
  25.     "strict"true, // 启用所有严格类型检查选项 
  26.     "noImplicitAny"true, // 在表达式和声明上有隐含的 any类型时报错 
  27.     "strictNullChecks"true, // 启用严格的 null 检查 
  28.     "noImplicitThis"true, // 当 this 表达式值为 any 类型的时候,生成一个错误 
  29.     "alwaysStrict"true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict' 
  30.  
  31.      
  32.     "noUnusedLocals"true, // 有未使用的变量时,抛出错误 
  33.     "noUnusedParameters"true, // 有未使用的参数时,抛出错误 
  34.     "noImplicitReturns"true, // 并不是所有函数里的代码都有返回值时,抛出错误 
  35.     "noFallthroughCasesInSwitch"true, // 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿) 
  36.  
  37.      
  38.     "moduleResolution""node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) 
  39.     "baseUrl""./", // 用于解析非相对模块名称的基础目录 
  40.     "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表 
  41.     "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容 
  42.     "typeRoots": [], // 包含类型声明的文件列表 
  43.     "types": [], // 需要包含的类型声明文件名列表 
  44.     "allowSyntheticDefaultImports"true, // 允许从没有设置默认导出的模块中默认导入。 
  45.     "esModuleInterop"true, // 支持合成模块的默认导入 
  46.    
  47.      
  48.     "sourceRoot""./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 
  49.     "mapRoot""./", // 指定调试器应该找到映射文件而不是生成文件的位置 
  50.     "inlineSourceMap"true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件 
  51.     "inlineSources"true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性 
  52.  
  53.      
  54.     "experimentalDecorators"true, // 启用装饰器 
  55.     "emitDecoratorMetadata"true // 为装饰器提供元数据的支持 
  56.   }, 
  57.    
  58.   "include": ["src*"], 
  59.   "exclude": ["node_modules""**/*.spec.ts"], 
  60.   "files": ["index.ts""test.ts"], 
  61.   // 从另一个配置文件里继承配置 
  62.   "extends""@tsconfig/recommended"
  63.   // 让 IDE 在保存文件的时候根据 tsconfig.json 重新生成文件 
  64.   "compileOnSave"true // 支持这个特性需要Visual Studio 2015, TypeScript 1.8.4 以上并且安装 atom-typescript 插件 

四、打包工具中的 TypeScript 

前文讲到了为什么不推荐直接使用 TSC 作为项目的打包编译工具,那么接下来就简单看看在常见的几款打包工具中针对 TypeScript 的编译方案是如何设计的?

4.1 Rollup + TypeScript

在 Rollup 打包中,我们一般只需要添加 @rollup/plugin-typescript[12] 插件即可,该插件会默认读取项目根目录下的 tsconfig.json 配置文件。

Rollup 的配置就像这样:

  1. // file: rollup.config.js 
  2. import typescript from '@rollup/plugin-typescript'
  3.  
  4. export default { 
  5.   input: 'src/index.ts'
  6.   output: { 
  7.     dir: 'output'
  8.     format: 'cjs' 
  9.   }, 
  10.   plugins: [typescript()] 
  11. }; 

结合其源码:

默认使用 TSC 作为 TS 的编译器

因为 typescript 声明了是 peerDependencies,因此会采用项目中安装的 typescript 版本,即是使用我们项目中的 TS 编译器。

通过阅读 @rollup/plugin-typescript 源码,可以看到该插件会默认使我们自己项目中的 tsconfig.json 文件作为 TSC 编译的配置,但会做一些配置预设覆盖:

会调用 ts.parseJsonConfigFileContent() 方法,将 FORCED_COMPILER_OPTIONS 值 merge 到用户的自定义配置中。

FORCED_COMPILER_OPTIONS

通过英文解释看到,因为需要 TSC 编译获得 JS 产物,所以会将 noEmit 设置为 false,也就是 TSC 编译会输出文件,但为什么我们在输出目录却没有看到对应的 TSC 产物呐?

TSC 编译结果存储到内存中

但是如果开启了 declaration,则会将 TSC 解析得到的 *.d.ts 文件输出到指定目录。

4.2 Webpack + TypeScript

在 Webpack 中的 TypeScript[13] 官方文档中,指明了需要安装:typescript 和 ts-loader 两个模块。

配置 Webpack 并支持 TypeScript 的配置如下:

  1. // file: webpack.config.js 
  2. const path = require('path'); 
  3.  
  4. module.exports = { 
  5.   entry: './src/index.ts'
  6.   module: { 
  7.     rules: [ 
  8.       { 
  9.         test: /\.tsx?$/, 
  10.         use: 'ts-loader'
  11.         exclude: /node_modules/, 
  12.       }, 
  13.     ], 
  14.   }, 
  15.   resolve: { 
  16.     extensions: ['.tsx''.ts''.js'], 
  17.   }, 
  18.   output: { 
  19.     filename: 'bundle.js'
  20.     path: path.resolve(__dirname, 'dist'), 
  21.   }, 
  22. }; 

可以看出 Webpack 主要是依赖 ts-loader 实现对 TypeScript 语法的编译支持,再看看对 ts-loader 的介绍:

ts-loader

换句话说,ts-loader 实际调用了 TSC 来编译 TS 文件,TSC 的配置依赖于你项目中的 tsconfig.json 文件。

如果使用了 Babel,则可以使用 @babel/preset-typescript[14] 来处理,但 Babel 不会做 TS 类型校验,在打包工具 Rollup 和 Webpack 中都可以引入 Babel,那么接下来看看 Babel 是如何处理 TypeScript 的吧!

4.3 Babel + TypeScript

Babel 处理 TS 需要安装 @babel/preset-typescript 模块,然后在 babel 项目配置文件中声明:

  1. // 配置说明:https://babeljs.io/docs/en/babel-preset-typescript 
  2.   "presets": ["@babel/preset-typescript"

但 Babel 中只会对 TS 代码转为 JS 代码(通过 parse TS 文件为 AST,并直接移除类型信息,然后打印目标代码),不会去做 TS 类型检查,所以 Babel 编译 TS 文件相较于 TSC 的速度更快!

同时,因为 Babel 会根据不同的兼容环境,按需引入 pollyfill,比 TSC 直接引入 core-js 更优雅,因此使用了 Babel 打包的体积也会更小。

TS 类型检查工作可以交给代码编辑器承担,当然同时可以新增 TS 检查的命令:

  1. // package.json 
  2.   "script": { 
  3.     "tsCheck""tsc --noEmit"
  4.   } 

可以把类型检查放到特定的 npm scripts 生命周期之前,另外其实也可以将类型检查放到 git commit 阶段,用于做必要的 TS 类型检查,保证项目的正确性。

4.4 ESbuild + TypeScript

通过 Vite 体会到了 ESbuild[15] 带来的开发热更新“极速”体验,针对 TS 项目,ESbuild 和 Babel 是相同的编译策略,即仅编译,不校验类型。

ESbuild 处理 TypeScript[16] 同样可以带来飞一般的感觉!

Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。—— Vite Docs[17]

但在 ESbuild 中需要启用 tsconfig 中的 isolatedModules 功能,然后在类型引入的时候需要替换,规则参考如下:

  1. // old 
  2. import { UserType } from './types'
  3.  
  4. // new 
  5. import type { UserType } from './types'

因为 ESbuild 是单独编译每个文件,无法判断引入的是 Type(类型) 还是 值,所以需要开发者显示地声明是“Type”。

同时还需要启用 esModuleInterop 功能,用于支持 ESM 模块合成默认导入,以兼容 CJS 和 ESM 规范。

另外 ESbuild 不支持:emitDecoratorMetadat、const enum 类型和 *.d.ts 文件

此外,关注到兼容性处理这方面,Bable 和 ESbuild 是类似的,因此会存在兼容性问题:

兼容性

对于装饰器处理不支持,因为 TS 是 JS 的超集,ESnext 的规范提案某些还不是稳定的,因此如果有这方面诉求的项目,可以借助 TSC 做预编译,例如使用 Rollup 的 typescript 插件 或 Webpack 的 ts-loader 方式。

五、总结 

针对 TypeScript 项目的类型检查和编译流程算是完整过了一遍,相信已足以支撑大家在工作中自定义化配置 TS 前端项目!

另外,tsconfig.json 推荐配置策略如下:

  1. 借助 extends 字段,并结合项目应用场景,继承官方推荐配置
  2. 针对项目特点,按需修改对应功能配置
  3. 建议启用 importHelpers、esModuleInterop,取消 noEmit 输出
  4. TS 项目的打包构建,推荐使用 Webpack、Rollup、Bable 等专业工具来保证正确性和构建优化

参考资料

[1]TSconfig.json 手册: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

[2]@tsconfig/recommended: https://www.npmjs.com/package/@tsconfig/recommended

[3]TSconfig 最佳实践配置: https://github.com/tsconfig/bases/

[4]ES.Next 语法提案: https://github.com/tc39/proposals

[5]1.5万字概括ES6全部特性: https://watesegmentfault.com/a/1190000020678240

[6]ES6 语法新特性: http://es6-features.org/#Constants

[7]esModuleInterop 到底做了什么?: https://zhuanlan.zhihu.com/p/148081795

[8]typescript-transform-paths: https://www.npmjs.com/package/typescript-transform-paths

[9]TTypescript: https://www.npmjs.com/package/ttypescript

[10]compilerOptions.plugins: https://www.typescriptlang.org/tsconfig#plugins

[11]Decorators: https://github.com/tc39/proposal-decorators

[12]@rollup/plugin-typescript: https://github.com/rollup/plugins/tree/master/packages/typescript/#readme

[13]Webpack 中的 TypeScript: https://webpack.docschina.org/guides/typescript/

[14]@babel/preset-typescript: https://babeljs.io/docs/en/babel-preset-typescript

[15]ESbuild: https://esbuild.github.io/

[16]ESbuild 处理 TypeScript: https://esbuild.github.io/content-types/#typescript

[17]Vite Docs: https://cn.vitejs.dev/guide/features.html#typescript

 

来源:DYBOY内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯