头图

When the ng add command adds a library to the project, the schematic will run. The ng generate command will run the schematic to create applications, libraries, and Angular code blocks.

Some terms:

rule

In the schematic diagram, it refers to a function running on the file tree, used to create, delete or modify files in a specified way, and return a new Tree object.

File tree

In schematics, a virtual file system represented by the Tree class. Schematic rules take a tree object as input, operate on them, and return a new tree object.

Developers can create the following three schematic diagrams:

  • Install the schematic so that ng add can add your library to the project.
  • Generate schematics so that ng generate can support the defined artifacts (components, services, tests, etc.) in the project.
  • Update the schematic so that ng update can update the dependencies of your library and provide some migrations to break the changes in the new version.

Let's make an example by hand.

In the root folder of the library, create a schematics/ folder.

In the schematics/ folder, create an ng-add/ folder for your first schematic.

At the root level of the schematics/ folder, create a collection.json file.

Edit the collection.json file to define the initial schema definition of your collection.

As shown below:

The content of the collection.json file is as follows:

{
  "$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"
    }
  }
}

The highlighted line in the figure below means: when ng add is executed, the index.ts file under the folder ng-add is called.

Namely this file:

We need to declare the reference to the above collection.json file in package.json in the root directory of the my-lib library:

The schematic diagram of the ng add command can enhance the user's initial installation process. You can define this schematic diagram as follows.

(1) Enter the <lib-root>/schematics/ng-add/ directory.
(2) Create the main file index.ts.
(3) Open index.ts and add the source code of the schematic factory function:

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

// Just return the tree
export function ngAdd(options: any): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask());
    return tree;
  };
}

The only step required to provide initial ng add support is to use the SchematicContext to trigger the installation task. This task will use the user's preferred package manager to add the library to the package.json configuration file of the host project and install it in the node_modules directory of the project.

In this example, the function will receive the current Tree and return it without any modification. If necessary, you can also make additional settings when installing the package, such as generating files, updating the configuration, or any other initial settings required by the library.

Define dependency type

If the library should be added to dependencies, devDepedencies, or does not need to be saved to the project’s package.json configuration file, please use the save option of ng-add to configure

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

The possible values are:

  • false-Do not add this package to package.json
  • true-add this package to dependencies
  • "dependencies"-add this package to dependencies
  • "devDependencies"-add this package to devDependencies

Build your schematic

You must first build the library itself, and then build Schematics.

Your library needs a custom Typescript configuration file with instructions on how to compile the schematic into the release version of the library.

To add these schematics to the library's release package, these scripts must be added to the library's package.json file.

Suppose you have a library project my-lib in the Angular workspace. To tell the library how to build the schematic, it is necessary to add a tsconfig.schematics.json file next to the generated tsconfig.lib.json library configuration file.

Create a new tsconfig.schematics.json file and maintain the following source code:

{
  "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/**/*"
  ]
}

rootDir pointed out that the input file to be compiled is contained in your schematics/ folder, that is, the file highlighted in the following figure:

outDir is mapped to the output directory of the library. By default, this is the dist/my-lib folder in the root directory of the workspace, which is the files in the following figure:

To ensure that your schematic source files will be compiled into the library package, please add the following script to the package.json file under the root folder of the library project (projects/my-lib).

{
  "name": "my-lib",
  "version": "0.0.1",
  "scripts": {
    "build": "../../node_modules/.bin/tsc -p tsconfig.schematics.json",
    "copy:schemas": "cp --parents schematics/*/schema.json ../../dist/my-lib/",
    "copy:files": "cp --parents -p schematics/*/files/** ../../dist/my-lib/",
    "copy:collection": "cp schematics/collection.json ../../dist/my-lib/schematics/collection.json",
    "postbuild": "npm run copy:schemas && npm run copy:files && npm run copy:collection"
  },
  "peerDependencies": {
    "@angular/common": "^7.2.0",
    "@angular/core": "^7.2.0"
  },
  "schematics": "./schematics/collection.json",
  "ng-add": {
    "save": "devDependencies"
  }
}

The build script uses a custom tsconfig.schematics.json file to compile your schematics.

The copy:* statement copies the compiled schematic file to the correct location under the output directory of the library to maintain the structure of the directory.

The postbuild script will copy the schematic file after the build script is completed.

Provide generator support

You can add a named schematic to the collection so that your users can use the ng generate command to create artifacts that you define in the library.

We assume that your library defines a service my-service that requires certain settings. You want users to be able to generate it with the following CLI command.

ng generate my-lib:my-service

First, create a new subfolder my-service in the schematics folder.

Edit the schematics/collection.json file, point to the new schematic subfolder, and attach a pointer to the model file, which will specify the input of the new schematic.

Enter the <lib-root>/schematics/my-service/ directory.

Create a schema.json file and define the available options for the schematic.

Each option associates the key with the type, description, and an optional alias. This type defines the shape of the value you expect, and displays this description when the user requests your schematic for usage help.

Create a schema.ts file and define an interface to store the value of each option defined in the schema.json file.

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;
}

name: The name you want to specify for the created service.

path: Override the path provided for the schematic. By default, the path is based on the current working directory.

project: Provide a specific project to run the schematic diagram. In the schematic, if the user does not give this option, you can provide a default value.

To add artifacts to the project, your schematic will need your own template file. The schematic template supports a special syntax to perform code and variable substitution.

Create a files/ folder under the schematics/my-service/ directory.

Create a file called __name@dasherize__.service.ts.template, which defines a template that can be used to generate the file. The template here will generate a service that has injected Angular's HttpClient into its constructor.

The contents of the file are as follows:

// #docregion template
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class <%= classify(name) %>Service {
  constructor(private http: HttpClient) { }
}

The classify and dasherize methods are utility functions, and your schematic will use them to convert your template source code and file name.

name is an attribute provided by the factory function. It is the same as the name you defined in the pattern.

Add factory function

Now that you have the infrastructure, you can start to define a main function to perform various modifications to the user project.

The Schematics framework provides a file template system that supports path and content templates. The system will manipulate the placeholders defined in the file or path loaded in the input file tree (Tree), and fill them with the value passed to Rule.

For detailed information about these data structures and syntax, please refer to the README of Schematics.

Create the main file index.ts and add the source code for your schematic factory function.

First, import the schematic definition you need. The Schematics framework provides many useful functions to create rules or use rules when executing schematics.

code show as below:

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

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

Import the defined schema interface and redefine it to MyServiceSchema using an alias. It will provide type information for your schematic options.

To build a "generator schematic", we start with a blank rule factory.

In the index.js file:

export function myService(options: MyServiceSchema): Rule {
  return (tree: Tree) => {
    return tree;
  };
}

This rule factory returns the tree without any modification. These options are the option values passed from the ng generate command.

Define a generator rule

We now have a framework that can be used to create some code that actually modifies the user program in order to set up the services defined in the library.

The Angular workspace where the user has installed this library will contain multiple projects (applications and libraries). The user can specify an item on the command line, or use its default value. In either case, your code needs to know on which project the schematic should be applied so that it can retrieve information from the configuration of that project.

You can use the Tree object passed to the factory function to do this. Through some methods of Tree, you can access the complete file tree of this workspace so that you can read and write files when you run the schematic.

Get project configuration

To determine the target project, you can use the workspaces.readWorkspace method to read the content of the workspace configuration file angular.json in the root directory of the workspace. To use workspaces.readWorkspace, you must first create a workspaces.WorkspaceHost from this Tree. Add the following code to the factory function.

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);

  };
}

Workspaces are exported from @angular-devkit/core, and readWorkspace is the standard method. The second input parameter host required by this method is returned from another custom function createHost.

The following line of default logic processing:

if (!options.project) {
  options.project = workspace.extensions.defaultProject;
}

This workspace.extensions property contains a defaultProject value to determine which project to use if this parameter is not provided. If no item is explicitly specified in the ng generate command, we will use it as a fallback value.

After having the project name, use it to retrieve the configuration information of the specified project.

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

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

options.path determines where to move the schematic template file after applying the schematic.
The path option in the schematic mode will be replaced by the current working directory by default. If the path is not defined, use the sourceRoot and projectType in the project configuration to determine it.

The logic is reflected in the following code:

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

sourceRoot is defined in angular.json:

Define rules

Rules can use external template files, convert them, and use the converted template to return another Rule object. You can use the template to generate any custom files needed for the schematic.

Add the following code to the factory function.

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

The apply() method will apply multiple rules to the source code and return the converted source code. It requires two parameters, a source code and an array of rules.

The url() method will read the source file from the path relative to the schematic in the file system.

The applyTemplates() method receives a parameter, and its methods and properties can be used on the schematic template and schematic file name. It returns a Rule. You can define the classify() and dasherize() methods, as well as the name attribute here.

The classify() method accepts a value and returns the value of the title case. For example, if the provided name is my service, it will return MyService. Title case is similar to camel case nomenclature and is a variable spelling rule.

The dasherize() method accepts a value and returns the value in lowercase and separated by a midline. For example, if the provided name is MyService, it will return the form "my-service".

When the schematic is applied, the move method will move the provided source file to the destination. Therefore, my service is converted to MyService, and then to my-service.

The rule factory must return a rule.

return chain([
  mergeWith(templateSource)
]);

The chain() method allows you to combine multiple rules into one rule, so that you can perform multiple operations in a schematic. Here you just merge the template rules with the code to be executed in the schematic.

So far, the Schematics of this Angular library has been developed. Please continue to pay attention to Jerry's follow-up articles. I will introduce how to consume this Schematics.

More original articles by Jerry, all in: "Wang Zixi":


注销
1k 声望1.6k 粉丝

invalid