HarmonyOS development: NodeJs script implements component-based dynamic switching

Foreword

In the last article, we used NodeJs script to complete the componentized operation of the HarmonyOS project. However, since the script was developed based on the 4.0.0.400 version of DevEco Studio, there may be some differences in the modification of the configuration file. So what should we do if we encounter this situation? One way is to write another set of targeted script files or add configuration version parameters to the original script. The second way is to make one yourself. As the saying goes, it is better to teach a man to fish than to teach him to fish, so I will simply write this article. , let me explain to you how to implement the script in the previous article, so that you can operate it yourself.

Analyze requirements

The overall summary of the requirements is very simple. The module of the dynamic shared package can be dynamically switched between the running package and the dynamic shared package, eliminating the manual configuration step. From the previous article, we have learned that the dynamic shared package The difference between the running package and the running package mainly comes from three places, namely the hvigorfile.ts file, the module.json5 file and the lack of entry ability .

First of all, there is definitely a need for a controllable switch. Use this switch to determine whether to dynamically switch the module. If switching is required, then execute the dynamic shared package to switch the running package, otherwise restore it. The general process is as follows:

Cleaning template

Whether we switch from a dynamic shared package to a running package, or from a running package to a dynamic shared package, what we change are the configuration files, that is, the three files with differences in the above. How can the contents of the files be changed back and forth? , of course, you can set unified content and only change the differences, but for intuitive and convenient viewing and modification, it is undoubtedly simpler to use templates.

First prepare two files, one is the dynamic sharing package and the other is the running package. When switching, just select different templates.

Dynamic sharing package template

To dynamically share a package, you need to provide two templates, namely the hvigorfile.ts file and the module.json5 file.

1. hvigorfile.ts
import { hspTasks } from '@ohos/hvigor-ohos-plugin';
 
export default {
    system: hspTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
}
2. module.json5
{
  "module": {
    "name": "mine",
    "type": "shared",
    "description": "$string:shared_desc",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "pages": "$profile:main_pages"
  }
}

Run package template

In addition to the two different configuration files, the running package must also have Ability, which is essential as the main entry.

1. hvigorfile.ts
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
 
export default {
    system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
    plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
}
2. module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ts",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

3. Ability

import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)  '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data)  '');
    });
  }

  onWindowStageDestroy() {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground() {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground() {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

Technical implementation

1. Create configuration file

The first step is to write the switch, that is, the configuration file. Of course, how to define the configuration file depends on your own arrangement. No matter what form it is displayed, there must be certain parameters, such as whether to enable componentization and enable componentized modules. Name, as for other parameters, can be added as needed. The configuration file I currently define is as follows. The specific explanations are all commented. A series of interpretations were also made in the previous article.

#Component configuration file
#Whether to enable componentization
startModule=false
#The name of the component that is enabled. After it is enabled, the current component can run independently.
startModuleName=
#After the above components are enabled, whether other non-essential components are changed to dynamic package mode, the default is not changed
startOtherShared=false
#Filter component names, which will never run independently and should be separated by commas.
filterModuleName=
#The page loaded by default by the current script, the default is Index.ets if not filled in
loadPage=

Configuration file, I customized the suffix here, it doesn’t matter what file it is, the main thing is the content in the file.

With the configuration file, we can implement relevant logic layer by layer based on the configuration file.

2. Initialization project

The nodeJs environment has been configured when DevEco Studio is installed. To check whether it is installed, you can execute the following command on the command line:

node -v

If the version number can be displayed normally, the installation is successful.

In the directory where the script needs to be created, perform the initialization operation:

npm init

Specific steps explained:

package name is the package name, which is the project name. The default is the content in brackets.
version: version number, the default is the content in brackets
description: description information
entry point: entry file name, the default is the content in brackets
test command: test command
git repository:git warehouse address
keywords: password
author: author's name
license: (ISC) license

Follow the relevant prompts along the way and perform the next step. In fact, a json configuration file is produced. You can also create it manually.

After execution, a json file will be created in the current directory.

json file content:

3. Create execution js file

This js file is where all our logic is written. In order to allow the js file to run normally, we need to configure it in the package.json file, as follows:

When executing the script in the future, just execute npm run module directly on the command line. We first simply output a “Hello world” and print it in module.js first, as shown below:

Results of executing the command:

3. Complete the final logic

Due to the need to operate files, the core module fs in Node.js is used here. In one sentence, the fs module provides a wealth of functions and methods, which can perform operations such as reading, writing, copying, and deleting files. At the same time It also supports operations such as directory creation, traversal and modification. If you want to know more in detail, you can check out my previous articles or search online. There is a lot of information.

1), read configuration file

All function implementations are based on configuration files, so the parameters in the configuration file are very important. It is also the first step of the program. Read the configuration file, get the relevant parameters set one by one, and record them.

//Read file information
    let path = require('path');
    let dirName = path.join(__dirname); //Get the directory
    try {
        //Read the configuration file and find the corresponding configuration information
        let data = fs.readFileSync(dirName + "/module.harmony", 'utf-8');
        var startModule;
        var startModuleName;
        var filterModuleName;
        var startOtherShared;
        var loadContentPage;
        data.split(/\r?\\
/).forEach((line, position) => {
            if (position === 2) {
                let open = line.split("=")[1];
                startModule = open.toString();
            }
            if (position === 4) {
                let moduleName = line.split("=")[1];
                startModuleName = moduleName.toString();
            }
            if (position === 6) {
                let otherName = line.split("=")[1];
                startOtherShared = otherName.toString();
            }
            if (position === 8) {
                let filterName = line.split("=")[1];
                filterModuleName = filterName.toString();
            }
            if (position === 10) {
                //load page information
                let loadPage = line.split("=")[1];
                loadContentPage = loadPage.toString();
            }
        });
        //After componentization is turned on, individual modules can run independently
        //If componentization is not turned on, then the entry can run independently, but nothing else can. You need to change the configuration files one by one.
        traverseFolder(dirName, startModule.indexOf("true") !== -1,
            startModuleName, startOtherShared, filterModuleName, loadContentPage);

    } catch (e) {
        console.log("An error occurred, please check whether the configuration file exists, or feedback to AbnerMing");
    }
2), dynamically modify configuration file information

According to the configuration file information, componentized operation is performed, that is, the dynamic shared package is switched to the running package. How to switch is to get the difference file, then read the template information and write it.

It should be noted that when a dynamic shared package is switched to a running package, the ability needs to be dynamically created. In addition, the name of the component and the ability name should be consistent with the component as much as possible.

To switch the running package to a dynamic shared package, you can also read the configuration file and then write it.

function traverseFolder(folderPath, isModule, startModuleName,
                        startOtherShared, filterModuleName, loadContentPage) {
    const items = fs.readdirSync(folderPath);

    items.forEach(item => {
        let dir = folderPath + "/" + item;
        const stats = fs.statSync(dir);
        if (stats.isDirectory()) {
            let hvigorFilePath = dir + "/hvigorfile.ts";
            fs.readFile(hvigorFilePath, "utf8", (err, dataStr) => {
                if (err) {
                    return;
                }
                if (isModule) {
                    //Enable componentization
                    //Change the current component to running state
                    if (item == startModuleName) {

                        let moduleName = item.substring(0, 1).toUpperCase()
                             + item.substring(1, item.length)

                        //Modify to runnable state
                        let entryHvigorFile = getEntryHvigorFile();
                        //Read the string.json file and add label
                        let jsonName = dir + "/src/main/resources/base/element/string.json";
                        fs.readFile(jsonName, "utf8", (err, dataStr) => {
                            if (err) {
                                return;
                            }
                            let obj = JSON.parse(dataStr);
                            let array = obj["string"];
                            let label = { "name": "shared_label", "value": item };
                            let isSharedLabel = false;

                            for (var i = 0; i < array.length; i + + ) {
                                let name = array[i]["name"];
                                if (name == "shared_label") {
                                    isSharedLabel = true;
                                    break;
                                }
                            }

                            if (!isSharedLabel) {
                                array.push(label);
                            }
                            writeContent(jsonName, JSON.stringify(obj));
                            //Further changes to the json5 file
                            let json5 = dir + "/src/main/module.json5";
                            writeContent(json5, getEntryModuleJson5(item, moduleName));
                        });

                        if (loadContentPage == null || loadContentPage == "") {
                            //Create it only when it is empty
                            //Create Index.ets file
                            let indexPath = dir + "/src/main/ets/pages";
                            const indexItem = fs.readdirSync(indexPath);
                            let isHaveIndex = false;
                            indexItem.forEach(item => {
                                if (item == "Index.ets") {
                                    //Prove existence
                                    isHaveIndex = true;
                                }
                            });

                            if (!isHaveIndex) {
                                //If it doesn’t exist, we need to create it.
                                writeContent(indexPath + "/Index.ets", getIndex());
                            }
                        }
                        //Create Ability file
                        let etsPath = dir + "/src/main/ets/" + item + "ability/" + moduleName + "Ability.ts";
                        fs.mkdir(dir + "/src/main/ets/" + item + "ability", function (err) {
                            if (err) {
                                writeContent(etsPath, getAbility(moduleName, loadContentPage));
                                return;
                            }
                            //Write to file
                            writeContent(etsPath, getAbility(moduleName, loadContentPage));
                        });

                    } else {
                        //Non-current components, do they need to be changed to dynamic package mode? Change according to the configuration file. There are two types that can never be changed.
                        if (item != "entry" & amp; & amp; filterModuleName.indexOf(item) == -1 & amp; & amp; startOtherShared) {
                            //Change other modules to dynamic packages and they cannot run
                            let moduleJson5 = getSharedModuleJson5(item);
                            let hvigorFile = getSharedHvigorFile();
                            writeContent(hvigorFilePath, hvigorFile);
                            writeContent(dir + "/src/main/module.json5", moduleJson5);
                        }
                    }
                } else {
                    //The main module and modules that need to be filtered do not perform dynamic package settings.
                    if (item != "entry" & amp; & amp; filterModuleName.indexOf(item) == -1) {
                        //Change other modules to dynamic packages and they cannot run
                        let moduleJson5 = getSharedModuleJson5(item);
                        let hvigorFile = getSharedHvigorFile();
                        writeContent(hvigorFilePath, hvigorFile);
                        writeContent(dir + "/src/main/module.json5", moduleJson5);
                    }
                }
            });
        }
    });
}

Related summary

Since the logic is relatively simple and complete, you can view the source code:

https://gitee.com/abnercode/harmony-os-module

Due to different development environments, the configuration file information is also different. It is just a different template to change. You only need to replace the content of the configuration file in your environment in the script.