Angular 庫(kù)的原理圖

2022-07-18 10:38 更新

庫(kù)的原理圖

當(dāng)創(chuàng)建 Angular 庫(kù)時(shí),你可以為同時(shí)為它打包進(jìn)一組原理圖,并把它與 Angular CLI 集成在一起。借助原理圖,用戶(hù)可以用 ?ng add? 來(lái)安裝你這個(gè)庫(kù)的初始版本,可以用 ?ng generate? 來(lái)創(chuàng)建你在庫(kù)中定義的一些工件,可以用 ?ng update? 來(lái)調(diào)整他們的項(xiàng)目,以支持你在庫(kù)的新版本中引入的重大變更。

這三種原理圖都可以作為你打包進(jìn)庫(kù)中的集合的一部分。

下載庫(kù)的原理圖項(xiàng)目以獲取一個(gè)已完成下列步驟的例子。

創(chuàng)建一個(gè)原理圖集合

要開(kāi)始一個(gè)集合,你需要?jiǎng)?chuàng)建一些原理圖文件。下列步驟說(shuō)明了如何在不修改任何項(xiàng)目文件的情況下添加初始支持。

  1. 在庫(kù)的根文件夾中,創(chuàng)建一個(gè) ?schematics/? 文件夾。
  2. 在 ?schematics/? 文件夾中,為你的第一個(gè)原理圖創(chuàng)建一個(gè) ?ng-add/? 文件夾。
  3. 在 ?schematics/? 文件夾的根級(jí),創(chuàng)建一個(gè) ?collection.json? 文件。
  4. 編輯 ?collection.json? 文件來(lái)定義你的集合的初始模式定義。
  5. {
      "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
      "schematics": {
        "ng-add": {
          "description": "Add my library to the project.",
          "factory": "./ng-add/index#ngAdd"
        }
      }
    }
    • ?$schema? 路徑是相對(duì)于 Angular Devkit 集合模式定義的。
    • ?schematics ?對(duì)象描述了該集合中的命名原理圖。
    • 第一個(gè)條目是名為 ?ng-add? 的原理圖。它包含了描述,并指向執(zhí)行此原理圖時(shí)要調(diào)用的工廠函數(shù)。
  6. 在這個(gè)庫(kù)項(xiàng)目的 ?package.json? 文件中,添加一個(gè) “schematics” 的條目,里面帶有你的模式定義文件的路徑。當(dāng) Angular CLI 運(yùn)行命令時(shí),會(huì)根據(jù)這個(gè)條目在你的集合中查找指定名字的原理圖。
  7. {
      "name": "my-lib",
      "version": "0.0.1",
      "schematics": "./schematics/collection.json",
    }

你所創(chuàng)建的初始模式告訴 CLI 在哪里可以找到支持 ?ng add? 命令的原理圖?,F(xiàn)在,你已準(zhǔn)備好創(chuàng)建該原理圖了。

提供安裝支持

?ng add? 命令的原理圖可以增強(qiáng)用戶(hù)的初始安裝過(guò)程。可以按如下步驟定義這種原理圖。

  1. 進(jìn)入 ?<lib-root>/schematics/ng-add/? 目錄。
  2. 創(chuàng)建主文件 ?index.ts?。
  3. 打開(kāi) ?index.ts? 并添加原理圖工廠函數(shù)的源代碼。
  4. import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
    import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
    
    // Just return the tree
    export function ngAdd(): Rule {
      return (tree: Tree, context: SchematicContext) => {
        context.addTask(new NodePackageInstallTask());
        return tree;
      };
    }

提供初始 ?ng add? 支持所需的唯一步驟是使用 ?SchematicContext ?來(lái)觸發(fā)安裝任務(wù)。該任務(wù)會(huì)借助用戶(hù)首選的包管理器將該庫(kù)添加到宿主項(xiàng)目的 ?package.json? 配置文件中,并將其安裝到該項(xiàng)目的 ?node_modules ?目錄下。

在這個(gè)例子中,該函數(shù)會(huì)接收當(dāng)前的 ?Tree ?并返回它而不作任何修改。如果需要,也可以在安裝軟件包時(shí)進(jìn)行額外的設(shè)置,比如生成文件、更新配置、或者庫(kù)所需的任何其它初始設(shè)置。

定義依賴(lài)類(lèi)型

如果該庫(kù)應(yīng)該添加到 ?dependencies ?中、?devDepedencies ?中,或者不用保存到項(xiàng)目的 ?package.json? 配置文件中,請(qǐng)使用 ?ng-add? 的 ?save ?選項(xiàng)進(jìn)行配置

"ng-add": {
  "save": "devDependencies"
},

可能的值有:

詳情

false

不把此包添加到 package.json

true

把此包添加到 dependencies

"dependencies"

把此包添加到 dependencies

"devDependencies"

把此包添加到 devDependencies

構(gòu)建你的原理圖

要把你的原理圖和庫(kù)打包到一起,就必須把這個(gè)庫(kù)配置成單獨(dú)構(gòu)建原理圖,然后再把它們添加到發(fā)布包中。你必須先構(gòu)建庫(kù)再構(gòu)建原理圖,這樣才能把它們放到正確的目錄下。

  • 你的庫(kù)需要一個(gè)自定義的 Typescript 配置文件,里面帶有如何把原理圖編譯進(jìn)庫(kù)的發(fā)布版的一些指令。
  • 要把這些原理圖添加到庫(kù)的發(fā)布包中,就要把這些腳本添加到該庫(kù)的 ?package.json? 文件中。

假設(shè)你在 Angular 工作區(qū)中有一個(gè)庫(kù)項(xiàng)目 ?my-lib?。要想告訴庫(kù)如何構(gòu)建原理圖,就要在生成的 ?tsconfig.lib.json? 庫(kù)配置文件旁添加一個(gè) ?tsconfig.schematics.json? 文件。

  1. 編輯 ?tsconfig.schematics.json? 文件,添加如下內(nèi)容。
  2. {
      "compilerOptions": {
        "baseUrl": ".",
        "lib": [
          "es2018",
          "dom"
        ],
        "declaration": true,
        "module": "commonjs",
        "moduleResolution": "node",
        "noEmitOnError": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "noUnusedParameters": true,
        "noUnusedLocals": true,
        "rootDir": "schematics",
        "outDir": "../../dist/my-lib/schematics",
        "skipDefaultLibCheck": true,
        "skipLibCheck": true,
        "sourceMap": true,
        "strictNullChecks": true,
        "target": "es6",
        "types": [
          "jasmine",
          "node"
        ]
      },
      "include": [
        "schematics/**/*"
      ],
      "exclude": [
        "schematics/*/files/**/*"
      ]
    }

    選項(xiàng)

    詳情

    rootDir

    指出在你的 schematics/ 文件夾中包含要編譯的輸入文件。

    outDir

    映射到了庫(kù)的輸出目錄下。默認(rèn)情況下,這是工作區(qū)根目錄下的 dist/my-lib 文件夾。

  3. 要確保你的原理圖源文件會(huì)被編譯進(jìn)庫(kù)包中,請(qǐng)把下列腳本添加到庫(kù)項(xiàng)目的根文件夾(?projects/my-lib?)下的 ?package.json? 文件中。
  4. {
      "name": "my-lib",
      "version": "0.0.1",
      "scripts": {
        "build": "tsc -p tsconfig.schematics.json",
        "postbuild": "copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ../../dist/my-lib/"
      },
      "peerDependencies": {
        "@angular/common": "^7.2.0",
        "@angular/core": "^7.2.0"
      },
      "schematics": "./schematics/collection.json",
      "ng-add": {
        "save": "devDependencies"
      },
      "devDependencies": {
        "copyfiles": "file:../../node_modules/copyfiles",
        "typescript": "file:../../node_modules/typescript"
      }
    }
    • ?build ?腳本使用自定義的 ?tsconfig.schematics.json? 文件來(lái)編譯你的原理圖。
    • ?postbuild ?腳本會(huì)在 ?build ?腳本完成后復(fù)制原理圖文件。
    • ?build ?和 ?postbuild ?腳本都需要用到 ?copyfiles ?和 ?typescript ?依賴(lài)項(xiàng)。要安裝這些依賴(lài)項(xiàng),請(qǐng)導(dǎo)航到 ?devDependencies ?中定義的路徑,并在運(yùn)行這些腳本之前運(yùn)行 ?npm install? 命令。

提供生成器支持

你可以把一個(gè)命名原理圖添加到集合中,讓你的用戶(hù)可以使用 ?ng generate? 命令來(lái)創(chuàng)建你在庫(kù)中定義的工件。

我們假設(shè)你的庫(kù)定義了一項(xiàng)需要進(jìn)行某些設(shè)置的服務(wù) ?my-service?。你希望用戶(hù)能夠用下面的 CLI 命令來(lái)生成它。

ng generate my-lib:my-service

首先,在 ?schematics ?文件夾中新建一個(gè)子文件夾 ?my-service?。

配置新的原理圖

當(dāng)你要把一個(gè)原理圖添加到集合中時(shí),就必須在該集合的模式中指向它,并提供一些配置文件來(lái)定義用戶(hù)可以傳給該命令的選項(xiàng)。

  1. 編輯一下 ?schematics/collection.json? 文件,指向新的原理圖子文件夾,并附上一個(gè)指向模式文件的指針,該文件將會(huì)指定新原理圖的輸入。
  2. {
      "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
      "schematics": {
        "ng-add": {
          "description": "Add my library to the project.",
          "factory": "./ng-add/index#ngAdd"
        },
        "my-service": {
          "description": "Generate a service in the project.",
          "factory": "./my-service/index#myService",
          "schema": "./my-service/schema.json"
        }
      }
    }
  3. 進(jìn)入 ?<lib-root>/schematics/my-service/? 目錄。
  4. 創(chuàng)建一個(gè) ?schema.json? 文件并定義該原理圖的可用選項(xiàng)。
  5. {
      "$schema": "http://json-schema.org/schema",
      "$id": "SchematicsMyService",
      "title": "My Service Schema",
      "type": "object",
      "properties": {
        "name": {
          "description": "The name of the service.",
          "type": "string"
        },
        "path": {
          "type": "string",
          "format": "path",
          "description": "The path to create the service.",
          "visible": false
        },
        "project": {
          "type": "string",
          "description": "The name of the project.",
          "$default": {
            "$source": "projectName"
          }
        }
       },
      "required": [
        "name"
      ]
    }
    • id:這個(gè)模式定義在集合中的唯一 ID。
    • title:一個(gè)人類(lèi)可讀的模式描述。
    • type:由這些屬性提供的類(lèi)型描述符。
    • properties:一個(gè)定義該原理圖可用選項(xiàng)的對(duì)象。

    每個(gè)選項(xiàng)都會(huì)把 key 與類(lèi)型、描述和一個(gè)可選的別名關(guān)聯(lián)起來(lái)。該類(lèi)型定義了你所期望的值的形態(tài),并在用戶(hù)請(qǐng)求你的原理圖給出用法幫助時(shí)顯示這份描述。

  6. 創(chuàng)建一個(gè) ?schema.ts? 文件,并定義一個(gè)接口,用于存放 ?schema.json? 文件中定義的各個(gè)選項(xiàng)的值。
  7. export interface Schema {
      // The name of the service.
      name: string;
    
      // The path to create the service.
      path?: string;
    
      // The name of the project.
      project?: string;
    }

    選項(xiàng)

    詳情

    name

    你要為創(chuàng)建的這個(gè)服務(wù)指定的名稱(chēng)。

    path

    覆蓋為原理圖提供的路徑。默認(rèn)情況下,路徑是基于當(dāng)前工作目錄的。

    project

    提供一個(gè)具體項(xiàng)目來(lái)運(yùn)行原理圖。在原理圖中,如果用戶(hù)沒(méi)有給出該選項(xiàng),你可以提供一個(gè)默認(rèn)值。

添加模板文件

要把工件添加到項(xiàng)目中,你的原理圖就需要自己的模板文件。原理圖模板支持特殊的語(yǔ)法來(lái)執(zhí)行代碼和變量替換。

  1. 在 ?schematics/my-service/? 目錄下創(chuàng)建一個(gè) ?files/? 文件夾。
  2. 創(chuàng)建一個(gè)名叫 ?__name@dasherize__.service.ts.template? 的文件,它定義了一個(gè)可以用來(lái)生成文件的模板。這里的模板會(huì)生成一個(gè)已把 Angular 的 ?HttpClient ?注入到其構(gòu)造函數(shù)中的服務(wù)。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class <%= classify(name) %>Service {
  constructor(private http: HttpClient) { }
}
  • ?classify ?和 ?dasherize ?方法是實(shí)用函數(shù),你的原理圖會(huì)用它們來(lái)轉(zhuǎn)換你的模板源碼和文件名。
  • ?name ?是工廠函數(shù)提供的一個(gè)屬性。它與你在模式中定義的 ?name ?是一樣的。

添加工廠函數(shù)

現(xiàn)在,你已經(jīng)有了基礎(chǔ)設(shè)施,可以開(kāi)始定義一個(gè) main 函數(shù)來(lái)執(zhí)行要對(duì)用戶(hù)項(xiàng)目做的各種修改了。

Schematics 框架提供了一個(gè)文件模板系統(tǒng),它支持路徑和內(nèi)容模板。系統(tǒng)會(huì)操作在這個(gè)輸入文件樹(shù)(?Tree?)中加載的文件內(nèi)或路徑中定義的占位符,用傳給 ?Rule ?的值來(lái)填充它們。

關(guān)于這些數(shù)據(jù)結(jié)構(gòu)和語(yǔ)法的詳細(xì)信息,請(qǐng)參閱 Schematics 的 README。

  1. 創(chuàng)建主文件 ?index.ts? 并為你的原理圖工廠函數(shù)添加源代碼。
  2. 首先,導(dǎo)入你需要的原理圖定義。Schematics 框架提供了許多實(shí)用函數(shù)來(lái)創(chuàng)建規(guī)則或在執(zhí)行原理圖時(shí)和使用規(guī)則。
  3. import {
      Rule, Tree, SchematicsException,
      apply, url, applyTemplates, move,
      chain, mergeWith
    } from '@angular-devkit/schematics';
    
    import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';
  4. 導(dǎo)入已定義的模式接口,它會(huì)為你的原理圖選項(xiàng)提供類(lèi)型信息。
  5. import {
      Rule, Tree, SchematicsException,
      apply, url, applyTemplates, move,
      chain, mergeWith
    } from '@angular-devkit/schematics';
    
    import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';
    
    import { Schema as MyServiceSchema } from './schema';
  6. 要想構(gòu)建 "生成器原理圖",我們從一個(gè)空白的規(guī)則工廠開(kāi)始。
  7. export function myService(options: MyServiceSchema): Rule {
      return (tree: Tree) => tree;
    }

這個(gè)規(guī)則工廠返回樹(shù)而不做任何修改。這些選項(xiàng)都是從 ?ng generate? 命令傳過(guò)來(lái)的選項(xiàng)值。

定義一個(gè)生成器規(guī)則

你現(xiàn)在有了一個(gè)框架,可用來(lái)創(chuàng)建一些真正修改用戶(hù)程序的代碼,以便對(duì)庫(kù)中定義的服務(wù)進(jìn)行設(shè)置。

用戶(hù)安裝過(guò)此庫(kù)的 Angular 工作區(qū)中會(huì)包含多個(gè)項(xiàng)目(應(yīng)用和庫(kù))。用戶(hù)可以在命令行中指定一個(gè)項(xiàng)目,也可以使用它的默認(rèn)值。在任何一種情況下,你的代碼都需要知道應(yīng)該在哪個(gè)項(xiàng)目上應(yīng)用此原理圖,這樣才能從該項(xiàng)目的配置中檢索信息。

可以使用傳給工廠函數(shù)的 ?Tree ?對(duì)象來(lái)做到這一點(diǎn)。通過(guò) ?Tree ?的一些方法,你可以訪問(wèn)此工作區(qū)的完整文件樹(shù),以便在運(yùn)行原理圖時(shí)讀寫(xiě)文件。

獲取項(xiàng)目配置

  1. 要確定目標(biāo)項(xiàng)目,可以使用 ?workspaces.readWorkspace? 方法在工作區(qū)的根目錄下讀取工作區(qū)配置文件 ?angular.json? 的內(nèi)容。要想使用 ?workspaces.readWorkspace?,你要先從這個(gè) ?Tree ?創(chuàng)建出一個(gè) ?workspaces.WorkspaceHost?。將以下代碼添加到工廠函數(shù)中。
  2. import {
      Rule, Tree, SchematicsException,
      apply, url, applyTemplates, move,
      chain, mergeWith
    } from '@angular-devkit/schematics';
    
    import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';
    
    import { Schema as MyServiceSchema } from './schema';
    
    function createHost(tree: Tree): workspaces.WorkspaceHost {
      return {
        async readFile(path: string): Promise<string> {
          const data = tree.read(path);
          if (!data) {
            throw new SchematicsException('File not found.');
          }
          return virtualFs.fileBufferToString(data);
        },
        async writeFile(path: string, data: string): Promise<void> {
          return tree.overwrite(path, data);
        },
        async isDirectory(path: string): Promise<boolean> {
          return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
        },
        async isFile(path: string): Promise<boolean> {
          return tree.exists(path);
        },
      };
    }
    
    export function myService(options: MyServiceSchema): Rule {
      return async (tree: Tree) => {
        const host = createHost(tree);
        const { workspace } = await workspaces.readWorkspace('/', host);
    
      };
    }

    一定要檢查此上下文是否存在,并拋出相應(yīng)的錯(cuò)誤。

  3. 現(xiàn)在你有了項(xiàng)目名稱(chēng),用它來(lái)檢索指定項(xiàng)目的配置信息。
  4. const project = (options.project != null) ? workspace.projects.get(options.project) : null;
    if (!project) {
      throw new SchematicsException(`Invalid project name: ${options.project}`);
    }
    
    const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';

    此 ?workspace.projects? 對(duì)象包含指定項(xiàng)目的全部配置信息。

  5. ?options.path? 決定了應(yīng)用原理圖之后,要把原理圖模板文件移動(dòng)到的位置。
  6. 原理圖模式中的 ?path ?選項(xiàng)默認(rèn)會(huì)替換為當(dāng)前工作目錄。如果未定義 ?path?,就使用項(xiàng)目配置中的 ?sourceRoot ?和 ?projectType ?來(lái)確定。

    if (options.path === undefined) {
      options.path = `${project.sourceRoot}/${projectType}`;
    }

定義規(guī)則

?Rule ?可以使用外部模板文件,對(duì)它們進(jìn)行轉(zhuǎn)換,并使用轉(zhuǎn)換后的模板返回另一個(gè) ?Rule ?對(duì)象??梢杂媚0鍋?lái)生成原理圖所需的任意自定義文件。

  1. 將以下代碼添加到工廠函數(shù)中。
  2. const templateSource = apply(url('./files'), [
      applyTemplates({
        classify: strings.classify,
        dasherize: strings.dasherize,
        name: options.name
      }),
      move(normalize(options.path as string))
    ]);

    方法

    詳情

    apply()

    將多個(gè)規(guī)則應(yīng)用于源并返回轉(zhuǎn)換后的源。它需要 2 個(gè)參數(shù)、一個(gè)源和一個(gè)規(guī)則數(shù)組。

    url()

    相對(duì)于原理圖,從文件系統(tǒng)中讀取源文件。

    applyTemplates()

    接收你希望使其可用于原理圖模板和原理圖文件名的方法和屬性的參數(shù)。它返回一個(gè) Rule。這是你定義 classify() 和 dasherize() 方法以及 name 屬性的地方。

    classify()

    接受一個(gè)值并以標(biāo)題大小寫(xiě)形式返回值。例如,如果提供的名稱(chēng)是 my service,它會(huì)作為 MyService 返回。

    dasherize()

    接受一個(gè)值并以虛線和小寫(xiě)形式返回值。例如,如果提供的名稱(chēng)是 MyService,則它將作為 my-service 返回。

    move()

    應(yīng)用原理圖時(shí),將提供的源文件移動(dòng)到它們的目標(biāo)。

  3. 最后,規(guī)則工廠必須返回一條規(guī)則。
  4. return chain([
      mergeWith(templateSource)
    ]);

    該 ?chain()? 方法允許你把多個(gè)規(guī)則組合到一個(gè)規(guī)則中,這樣就可以在一個(gè)原理圖中執(zhí)行多個(gè)操作。這里你只是把模板規(guī)則和原理圖要執(zhí)行的代碼合并在一起。

請(qǐng)看原理圖規(guī)則函數(shù)的一個(gè)完整例子。

import {
  Rule, Tree, SchematicsException,
  apply, url, applyTemplates, move,
  chain, mergeWith
} from '@angular-devkit/schematics';

import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';

import { Schema as MyServiceSchema } from './schema';

function createHost(tree: Tree): workspaces.WorkspaceHost {
  return {
    async readFile(path: string): Promise<string> {
      const data = tree.read(path);
      if (!data) {
        throw new SchematicsException('File not found.');
      }
      return virtualFs.fileBufferToString(data);
    },
    async writeFile(path: string, data: string): Promise<void> {
      return tree.overwrite(path, data);
    },
    async isDirectory(path: string): Promise<boolean> {
      return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
    },
    async isFile(path: string): Promise<boolean> {
      return tree.exists(path);
    },
  };
}

export function myService(options: MyServiceSchema): Rule {
  return async (tree: Tree) => {
    const host = createHost(tree);
    const { workspace } = await workspaces.readWorkspace('/', host);


    const project = (options.project != null) ? workspace.projects.get(options.project) : null;
    if (!project) {
      throw new SchematicsException(`Invalid project name: ${options.project}`);
    }

    const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';

    if (options.path === undefined) {
      options.path = `${project.sourceRoot}/${projectType}`;
    }

    const templateSource = apply(url('./files'), [
      applyTemplates({
        classify: strings.classify,
        dasherize: strings.dasherize,
        name: options.name
      }),
      move(normalize(options.path as string))
    ]);

    return chain([
      mergeWith(templateSource)
    ]);
  };
}

關(guān)于規(guī)則和實(shí)用工具方法的詳細(xì)信息,請(qǐng)參閱預(yù)定義規(guī)則

運(yùn)行你的庫(kù)原理圖

在構(gòu)建庫(kù)和原理圖之后,你就可以安裝一個(gè)原理圖集合來(lái)運(yùn)行你的項(xiàng)目了。下面的步驟介紹了如何使用上面創(chuàng)建的原理圖來(lái)生成服務(wù)。

構(gòu)建你的庫(kù)和原理圖

在工作區(qū)的根目錄下,運(yùn)行庫(kù)的 ?ng build? 命令。

ng build my-lib

然后,進(jìn)入庫(kù)目錄,構(gòu)建原理圖

cd projects/my-lib
npm run build

鏈接這個(gè)庫(kù)

這些庫(kù)和原理圖都已打包好了,就放在你工作區(qū)根目錄下的 ?dist/my-lib? 文件夾中。要運(yùn)行這個(gè)原理圖,你需要把這個(gè)庫(kù)鏈接到 ?node_modules ?文件夾中。在工作區(qū)的根目錄下,運(yùn)行 ?npm link? 命令,并把你的可分發(fā)庫(kù)的路徑作為參數(shù)。

npm link dist/my-lib

運(yùn)行原理圖

現(xiàn)在你的庫(kù)已經(jīng)安裝完畢,可以使用 ?ng generate? 命令來(lái)運(yùn)行原理圖了。

ng generate my-lib:my-service --name my-data

在控制臺(tái)中,你會(huì)看到原理圖已經(jīng)運(yùn)行過(guò)了,?my-data.service.ts? 文件被創(chuàng)建在了你的 app 文件夾中。

CREATE src/app/my-data.service.ts (208 bytes)


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)