Encapsulates an automatic import module method in NestJs

What to do?

  • Let’s look at some code first

  • I don’t know what you feel when you see these two pieces of code. Anyway, my feeling is not very good ๐Ÿ˜„, every time you add a module, you have to introduce and register in app.module, which is very troublesome, we will solve this problem today.

Why?

  • Recently, my brother-in-law found me to let me do a XX official website, I listened to that must be no problem ah, very readily answered down, and patted the chest to ensure that: no problem ~ no problem ~. It happened that my brother-in-law was not too anxious and I did it alone, so THIS time I wanted to use a bit of radical technology or a bit of unfamiliar technology, among which NestJs was used as the framework of the back end, and the front end used a project built by Quasar CLI.
  • NestJs and Quasar were used by the author before, why say strange? Because before in the use of the process, is only to use, and did not ask the root of the study, just to use and use, without any significance, so this time phase take advantage of this opportunity to explore the mystery of it (do not know why, recently very like to see the source code).

Step 1: Thinking

  1. We want to be in thenodeTo import a module, two methods are often used(require(), import())And a keyword(import)Since we are doing dynamic import or programmatic import, the keywords in it must not be used, so we can only choose two methods, here we chooseimport()Method, because we’re going to do atsVersion of the bag, withrequire()It’s not a good idea, andimport()Method returnsPromiseTo be more controlled(He almost got killed because of Promise.).
  2. We want to write an import method that involves file and folder operations (file operations), but here we just need to read.
  3. We need the user to pass in something:Folder path, ignore rule, filter rule, current file path, and we finally return one to the userkeyFor the pathvalueFor the contents of the fileMap<path, content>Can.
  4. With the above points confirmed, we can begin to write our own import methods.

Step 2: Rough implementation

  • So let’s start with a little bit of pseudocode. Hey, ๐Ÿ˜, this is what I had in mind when I was thinking about this.
// 1. Define a method to import. Let's call it single
const single = () = > { /* Import single file */ }

// 2. Define a main method that calls the single method to import files in bulk, called requireAll for now
const requireAll = () = > { /* Call single batch import file */ }

requireAll({ / * * / })
Copy the code
  • The single method here is very good to implement, so let’s implement it first
export const requireSingle = (fullpath: string) = > {
  return new Promise((resolve, reject) = > {
    import(fullpath)
      .then(data= > resolve(data))
      .catch(error= > reject(error));
  });
}
Copy the code

It accepts a file path and returns a promise, where the resolve and reject methods correspond to the THEN and catch methods of the import.

  • And then, let’s do it againrequireAllMethods.
// Merge modules
export const mergeMap = (targetMap: Map<any.any>, subMap: Map<any.any>) = > {
  for (let key ofsubMap.keys()) { targetMap.set(key, subMap.get(key)); }}// Ignore files, folders, node_modules,.d.ts,.d.js
const DEFAULT_IGNORE_RULE = /(^\.|^node_modules|(\.d\.. *) / $);
// Only files ending in.ts or.js
const DEFULAT_FILTER = /^([^\.].*)\.(ts|js)$/;
// The current file path
const DEFAULT_CURRENT_FILE = __filename;

// Define the requireAll parameter types
interface IOptions {
  dirname: string;
  ignore: RegExp | Function;
  filter: RegExp | Function;
  currentFile: string;
}

The Partial interface defines all properties in the passed generic type as optional properties
export const requireAll = (options: Partial<IOptions>) = > {
  // (required) get the folder path
  const dirname = options.dirname;
  // Get the ignore rule. If not, use the default
  const ignore = options.ignore || DEFAULT_IGNORE_RULE;
  // Used to save imported modules
  const modules: Map<any.any> = new Map(a);// Get the filter rule, if not, use the default one
  const filter = options.filter || DEFULAT_FILTER;
  // (required) The path to the file to perform this method
  const currentFile = options.currentFile || DEFAULT_CURRENT_FILE;
  // Get the folder name or file name under the folder
  const files = readdirSync(dirname);

  // Define a method to determine whether a file is ignoreFile. This method is called ignoreFile. True: ignores the file, false: does not ignore the file
  const ignoreFile: (f: string) = > boolean = (fullpath: string) = > {
    if (typeof ignore === 'function') {
      return ignore(fullpath);
    } else {
      returnfullpath === currentFile || !! fullpath.match(ignoreas RegExp); }}// Define a method to determine whether a file meets the filter conditions. Here, call filterFile. True: yes, false: no
  const filterFile = (fullpath: string) = > {
    if (typeof filter === 'function') {
      return filter(fullpath);
    } else {
      return filter.test(fullpath);
    }
  }
  
  files.forEach(async filename => {
     // Since the readdirSync method gets the file name or folder name, it is not the full path, so here is the full path splicing
     const fullpath = join(dirname, filename);
     // If true, ignore the file
     if (ignoreFile(fullpath)) return;

     // If it is a folder, re-call the requireAll method
     if (statSync(fullpath).isDirectory()) {
       const subModules = requireAll({
         dirname: fullpath,
         ignore: ignore
       });
       // Merge existing modules and submodules
       mergeMap(modules, subModules);
      } else {
       // If it is a file
       try {
         // If the filter conditions are met, the file is read and added to modules
         if (filterFile(fullpath)) {
           // The requireSingle method loads the file, which was implemented above
           const data = awaitrequireSingle(fullpath); modules.set(fullpath, data); }}catch (error) {
         throwerror; }}});return modules;
}
Copy the code

In the above code, there are several important points to emphasize:

  1. We can see thatfiles.forEachThe callback method in “async” is decorated with async, which means I might use it in my appawaitAnd that’s the truth. We dorequireSingleMethodawait.
  2. So each of theseforEachThe callback method for theasynchronousMethod, in whichrequireSingleThe method can indeed be executed, but let’s look at the last line of codereturn modulesWhen we putforEachIs set toasynchronousLater,return modulesYou can’t get the return value.

So what are we gonna do about it?

Let’s rewrite the forEach method:

 await Promise.all(
   files.forEach(async filename => {
   // Since the readdirSync method gets the file name or folder name, it is not the full path, so here is the full path splicing
   const fullpath = join(dirname, filename);
   // If true, ignore the file
   if (ignoreFile(fullpath)) return;

    // If it is a folder, re-call the requireAll method
    if (statSync(fullpath).isDirectory()) {
      const subModules = requireAll({
        dirname: fullpath,
        ignore: ignore
      });
      // Merge existing modules and submodules
      mergeMap(modules, subModules);
     } else {
      // If it is a file
      try {
        // If the filter conditions are met, the file is read and added to modules
        if (filterFile(fullpath)) {
          // The requireSingle method loads the file, which was implemented above
          const data = awaitrequireSingle(fullpath); modules.set(fullpath, data); }}catch (error) {
        throwerror; }}}));Copy the code
  1. willforEachChange method to map method.
  2. tomapMethod Put a layer over itawait Promise.all

This solves the problem where we can’t get data from return modules because of async.

Note, however, that the requireAll method also requires an async modifier

RequireAll complete code

// Merge modules
export const mergeMap = (targetMap: Map<any.any>, subMap: Map<any.any>) = > {
  for (let key ofsubMap.keys()) { targetMap.set(key, subMap.get(key)); }}// Ignore files, folders, node_modules,.d.ts,.d.js
const DEFAULT_IGNORE_RULE = /(^\.|^node_modules|(\.d\.. *) / $);
// Only files ending in.ts or.js
const DEFULAT_FILTER = /^([^\.].*)\.(ts|js)$/;
// The current file path
const DEFAULT_CURRENT_FILE = __filename;

// Define the requireAll parameter types
interface IOptions {
  dirname: string;
  ignore: RegExp | Function;
  filter: RegExp | Function;
  currentFile: string;
}

The Partial interface defines all properties in the passed generic type as optional properties
export const requireAll = (options: Partial<IOptions>) = > {
  // (required) get the folder path
  const dirname = options.dirname;
  // Get the ignore rule. If not, use the default
  const ignore = options.ignore || DEFAULT_IGNORE_RULE;
  // Used to save imported modules
  const modules: Map<any.any> = new Map(a);// Get the filter rule, if not, use the default one
  const filter = options.filter || DEFULAT_FILTER;
  // (required) The path to the file to perform this method
  const currentFile = options.currentFile || DEFAULT_CURRENT_FILE;
  // Get the folder name or file name under the folder
  const files = readdirSync(dirname);

  // Define a method to determine whether a file is ignoreFile. This method is called ignoreFile. True: ignores the file, false: does not ignore the file
  const ignoreFile: (f: string) = > boolean = (fullpath: string) = > {
    if (typeof ignore === 'function') {
      return ignore(fullpath);
    } else {
      returnfullpath === currentFile || !! fullpath.match(ignoreas RegExp); }}// Define a method to determine whether a file meets the filter conditions. Here, call filterFile. True: yes, false: no
  const filterFile = (fullpath: string) = > {
    if (typeof filter === 'function') {
      return filter(fullpath);
    } else {
      returnfilter.test(fullpath); }}await Promise.all(
       files.forEach(async filename => {
       // Since the readdirSync method gets the file name or folder name, it is not the full path, so here is the full path splicing
       const fullpath = join(dirname, filename);
       // If true, ignore the file
       if (ignoreFile(fullpath)) return;

        // If it is a folder, re-call the requireAll method
        if (statSync(fullpath).isDirectory()) {
          const subModules = requireAll({
            dirname: fullpath,
            ignore: ignore
          });
          // Merge existing modules and submodules
          mergeMap(modules, subModules);
         } else {
          // If it is a file
          try {
            // If the filter conditions are met, the file is read and added to modules
            if (filterFile(fullpath)) {
              // The requireSingle method loads the file, which was implemented above
              const data = awaitrequireSingle(fullpath); modules.set(fullpath, data); }}catch (error) {
            throwerror; }}}));return modules;
}
Copy the code

Here we gorequireAllThe method is done, but at this pointrequireAllCan only be used to load normaltsThe document is not enough to reach toNestJsDegree of application.

The sample application

Let’s look at the directory structure first
โ”‚ โ”œโ”€โ”€ b.ts โ”‚ โ”œโ”€ children โ”‚ โ”œโ”€ C.t sCopy the code

The file content

Call way

Let’s take a look at the output

Hey hey ๐Ÿ˜, isn’t it cool?

Step 3: Refine

  • We’ve just talked about that. The presentrequireAllThe method is far from givingNestJsTo what extent, what should we do? I might need a little bit heretsA decorator andReflect-metadataIf you have not understood the basic, welcome to look at the author’s several articles:
  • TypeScript decoration organ web note: juejin.cn/post/697242…
  • Reflect Metadata learning notes: juejin.cn/post/697242…
  • Proxy&Reflectๅฎ˜็ฝ‘ note: juejin.cn/post/697390…
  • Metaprogramming in VUE3 & Code extraction: juejin.cn/post/697457…
  • Again, if you haven’t read this, read it in the order the author gives you (top to bottom)

Let’s open a NestJs project and take a look at it.

Attached here is the address of NestJs Chinese website. Follow the official website to docs.nestjs.cn/

Let’s take a look at the directory structure, which the author has changed a little, but it’s not important:

โ”œ โ”€ โ”€ assets โ”œ โ”€ โ”€ main. Ts โ”œ โ”€ โ”€ modules โ”‚ โ”œ โ”€ โ”€ app โ”‚ โ”‚ โ”œ โ”€ โ”€ app. The controller. The ts โ”‚ โ”‚ โ”œ โ”€ โ”€ app. The module. The ts โ”‚ โ”‚ โ”” โ”€ โ”€ app. Service. Ts โ”‚ โ”œโ”€โ”€ config โ”‚ โ”œโ”€โ”€ config.controller. Ts โ”‚ โ”œโ”€โ”€ config.module. Ts โ”‚ โ”œโ”€โ”€ config.service. Ts โ”‚ โ”œโ”€ user โ”‚ โ”œโ”€ Exercises โ”‚ โ”œโ”€ user.module. Ts โ”‚ โ”œโ”€ user.service. Ts โ”‚ โ”œโ”€โ”€ types โ”œโ”€ utils โ”œโ”€ requreAll. Ts โ”œโ”€ utilsCopy the code

Let’s go back to what we said at the beginningapp.module.tsFile:As we can see, all the modules are to be inapp.moduleIn theimportsIs registered as an array item in the2One module is acceptable for us to operate. If we are working on a large project with dozens or even hundreds of modules, we need to introduce every module again and again.

So how do we address this with the requireAll approach?

  • First of all, we know,app.moduleIn theimportsFor now onlyThe module (module)Did not acceptcontrollerandservice, and the presentrequireAllMethod either waymoduleorcontrollerorserviceI’m going to jump right inimportsInside, even if this does not report an error is bound to be unreasonable.
  • How do we distinguish between module, Controller or service files? Let’s take a look at the differences between these three.
config.controller.ts

config.service.ts

config.module.ts

  • For example, the ConfigController decorator is @Controller(), the ConfigServeice decorator is @Injectable(), and the ConfigServeice decorator is @Injectable(). The ConfigModule decorator is @Module(). All we need to do is to determine whether the class is a Module, controller, or Service based on the decorator
  • However, we know that there is no way to know which decorators are decorating the class itself (at least not yet), so we can only adopt a compromise. We can define our own decorator to record which decorators are decorating the current class. Let’s look at the code implementation.
export function defineDecorator (metadata: string[]) {
  return function (target: any) {
    Reflect.defineMetadata('decorator', metadata, target); }}Copy the code

We defined a defineDecorator method that defines a decorator, which is a factory decorator (if you don’t know what a factory decorator is, see the author’s article about decorators above), and accepts metadata that is an array of strings. Return a decorator that is a class decorator that takes the target parameter and defines metadata for the target key in the decorator. The metadata value is the passed metadata parameter.

Let’s see how it works:Referenced in the above codedefineDecoratorApproach toConfigModuleClass to decorate,metadatafor['Module']Why is this fromv-require-allWhat about the methods imported in? Because the author has already published the wrapped methodnpmAbove, after you want to use or view the source code, you can directlynpm i v-required-all --save), what does that do? Take a look at this code:This code usesReflect.getMetadata('decorator', ConfigModule)To get the metadata that we defined. With this, we can distinguish between the files that are currently being importedmoduleorcontrollerorserviceThe concrete is how to do? Let’s move on.

Step 4: Dust settles

We just said that we can tell whether a class is a Module or not by decorating it with defineDecorator. How?

  1. First of all, we need to be inrequireAllMethod to encapsulate a method that is given separatelyNestJsUse, let’s call itrequireAllInNest.
// Read the properties
export const readKeys = (map: Object, callback: (key: any, value: any) = >void) = > {
  const results = [];
  if (map instanceof Map) {
    for (let key of map.keys()) {
      const returns = callback(key, map.get(key));
      if(returns ! = =undefined) { results.push(returns); }}}else {
    for (let key of Reflect.ownKeys(map)) {
      // @ts-ignore
      const returns = callback(key, map[key]);
      if(returns ! = =undefined) { results.push(returns); }}}if (results.length > 0) {
    returnresults; }}export const requireAllInNest = (options: Partial<IOptions>, type: 'Controller' | 'Injectable' | 'Module' = 'Module') = > {
  return new Promise((resolve, reject) = > {
    if (type= = ='Controller') {
      options.filter = /^([^\.].*)\.controller\.ts$/;
    } else if (type= = ='Injectable') {
      options.filter = /^([^\.].*)\.service\.ts$/;
    } else {
      options.filter = /^([^\.].*)\.module\.ts$/;
    }
    requireAll(options).then(modules= > {
      try {
        const importsModule: any[] = readKeys(modules, (key, value) = > {
          return readKeys(value, (vKey, target) = > {
            if (typeof target === 'function') {
              const metadata = Reflect.getMetadata('decorator', target);
              if (Array.isArray(metadata) && metadata.length > 0 && metadata.indexOf(type)! = = -1) {
                returntarget; }}}); });const results: any[] = [];
        importsModule.forEach((chunk: any[]) = >results.push(... chunk)); resolve(results); }catch (error) {
        reject(error);
      }
    }).catch(error= >reject(error)); })}Copy the code

Let’s look at what this method does:

  1. First, the method takes two arguments, the first argumentoptionsIs equivalent torequireAlltheoptionsThe second parameter is used to specify that themoduleorcontrollerorservice.
  2. throughtypeParameter to rewritefilterProperties (filter criteria for files)
  3. performrequireAllMethod to get the returnedmodulesWe returnedmodulesIs aMap<string, Object>So we’ve defined one herereadKeysMethod (this method is similarforEachMethod), why use two layers herereadKeysMethod? Because we gotmodulesthevalueIt’s an object{ a: 'xxx', default: xxx }The first layer,readKeysUsed to getmodulesthevalue, the second layer is used to obtainvalueThe contents of.
  4. By judgingvalueIs the content offunctionTo proceed to the next step, if it is function and the method containsdecoratormetadatakey, and the metadata contains the passed intype('Controller' | 'Injectable' | 'Module'), we conclude that the content is what we needModule (for example, we need module here)To return the content.
  5. What we finally gotmodulesIt’s going to be a multi-dimensional array nested, and we need to structure it a little bit, so we have these two lines of code.
const results: any[] = [];
importsModule.forEach((chunk: any[]) = >results.push(... chunk));Copy the code

Now that the requireAllInNest method is wrapped, let’s take a look at how to use it.

We can see that our ideas are very good, but helplessNestJsthe@ModuleIn the decoratorimportsDon’t acceptPromiseType parameter, and here we have what we almost had beforePromiseThe foreshadows of death. So how do we solve it? It’s really easy, we can rewrite it or we can customize itModuleDecorator. The purpose of this decorator is to add to the class being decoratedimports, controllers, providersThese three metadata, then we can also achieve their own.

type IDepend = Promise<any> | Promise<any| > []any[];
type IPrototypeClass<T = any> = new(... args:any[]) => T;
// Bind the Module parameters
interface IMetadata {
  imports: IDepend;
  controllers: IDepend;
  providers: IDepend;
}
const metadataHandler = {
  setImports: (imports: any, target: IPrototypeClass) = > Reflect.defineMetadata('imports', toArray(imports), target),
  setControllers: (controllers: any, target: IPrototypeClass) = > Reflect.defineMetadata('controllers', toArray(controllers), target),
  setProviders: (providers: any, target: IPrototypeClass) = > Reflect.defineMetadata('providers', toArray(providers), target),
  handler(data: any, target: IPrototypeClass, method: (v:any, t:IPrototypeClass) => void) {
    const single = (value: any) = > (value && isPromise(value)) ? (value as Promise<any>).then(d= > method(d, target)) : method(value, target);
    isArray(data) ? (data as any[]).forEach(v= > single(v)) : single(data);
  },
  imports(imports: IDepend, target: IPrototypeClass) {
    this.handler(imports, target, this.setImports);
  },
  controllers(controllers: IDepend, target: IPrototypeClass) {
    this.handler(controllers, target, this.setControllers);
  },
  providers(providers: IDepend, target: IPrototypeClass) {
    this.handler(providers, target, this.setProviders); }}// Basically overwrite @module
export function Module (metadata: Partial<IMetadata>) {
  return function (target: IPrototypeClass) { metadataHandler.imports(metadata.imports, target); metadataHandler.controllers(metadata.controllers, target); metadataHandler.providers(metadata.providers, target); }}Copy the code

We can see that the Module method itself is a factory decorator that adds metadata to the target by calling the metadataHandler object, which is not explained here. A collection of methods that determine whether it is an array, a promise, or plain data and then add metadata to the target.

Let’s see how to use it:And as you can see, it’s very, very simple, just take the originalModuleChange the method to self – definedModuleMethod is ok.

Effect:

Hey hey ๐Ÿ˜, is it cool!

Final step: Publish to NPM

To NPM, you can refer to these two articles, some of which will be noted below.

  • How do I publish an NPM package? : cloud.tencent.com/developer/a…
  • Upload ts code written to NPM: blog.csdn.net/Dilomen/art…
  • Here we want to emphasize that before doing the above operations, if the local NPM source is Taobao source or other sources, to change them to the default, or directly delete the.npmrc file, the author is directly deleted.

conclusion

At this point the method is encapsulated; to use it, simply search for v-require-all in NPM. Some friends asked me why I chose this name, why does it have a V – prefix? “Require -all” and “@require-all” failed, so “V -” is the first letter of veloma.