Nx Series Part3: Good architect for a seamless Electron app

Nx Series Part3: Good architect for a seamless Electron app

In the realm of software architecture, two primary objectives stand out: meeting business requirements and ensuring cost-effectiveness. In this article, we delve into the rationale behind selecting the right toolset and explore why Nx serves as an ideal monorepo tool for achieving project goals.


To contextualize our discussion, let's address a specific challenge: the verification of analytics deployment using Google Tag Manager (GTM). Traditionally, this process involves manual inspection, consuming considerable time to ensure alignment with specifications. Moreover, manual checks are prone to human error, eroding client trust over time. Therefore, the objective is to develop a tool that primarily:

  1. Automates website actions to trigger specific behaviors.
  2. Validates the actual deployment against predefined specifications.

Additional features would complement these core functionalities.

Crucially, the chosen solution must be accessible to developers and incur minimal costs. Given limited resources and the non-commercial nature of the project, prioritizing low-cost or free toolsets is essential. With considerations for workload distribution and version control, opting for separate frontend and backend components within the architecture is prudent. Additionally, the deployment process should be economically viable, making Electron an apt choice for the task at hand.


By leveraging Nx as a monorepo tool, developers can streamline the development process, ensuring efficient collaboration and maintenance. Its features such as code sharing, dependency management, and consistent tooling align well with the project's requirements. Furthermore, Nx's integration with popular frameworks like Angular, React, and Node.js provides flexibility without compromising on performance or scalability.


One of the most advantageous aspects of Nx is its provision of a comprehensive workspace. This feature enables me to develop both the frontend and backend components seamlessly, and package them together using Electron with the following script setup:

"scripts": {
  "build-ng": "cross-env-shell NODE_ENV=ng-frontend \"npm run build-ng-frontend && npm run build-backend\"",
  "packager-ng": "cross-env-shell NODE_ENV=ng-frontend \"npm run build-ng && electron-forge package\""
}
        

In the build-ng script, I leverage the cross-env-shell package to inject environment variables during the building phase. This ensures smooth integration between different components of the project.

Meanwhile, the packager-ng script simplifies the process of building the Electron app by bundling necessary resources from both the frontend and backend. To achieve this, two essential files are required for the electron-forge package:

  1. main.js: This file serves as the main entry point for the Electron application, orchestrating the communication between frontend and backend components.
  2. forge.config.js: This configuration file provides directives for electron-forge on how to package the Electron application, including settings for bundling resources and defining build targets.

By incorporating these scripts and leveraging the capabilities of Nx and Electron, we streamline the development and deployment process, ultimately enhancing productivity and facilitating seamless integration across different layers of the application.

// main.js at the root of the directory
const isDev = require('electron-is-dev');
const path = require('path');
const { app, BrowserWindow } = require('electron');
const { spawn } = require('child_process');
const { existsSync } = require('fs');

('use strict');

function startBackend() {
  let serverPath = path.join(process.resourcesPath, 'main.js');

  if (!existsSync(serverPath)) {
    serverPath = path.join(
      __dirname,
      'dist',
      'apps',
      'nest-backend',
      'main.js'
    );
  }

  if (!existsSync(serverPath)) {
    throw new Error('Backend server not found');
  }

  const serverProcess = spawn('node', [serverPath]);

  serverProcess.stdout.on('data', (data) => {
    console.log(`Backend: ${data}`);
  });

  serverProcess.stderr.on('data', (data) => {
    console.error(`Backend Error: ${data}`);
  });

  serverProcess.on('close', (code) => {
    console.log(`Backend process exited with code ${code}`);
  });
}

function getProductionFrontendPath() {
  const ngFrontendPath = path.join(
    process.resourcesPath,
    'ng-frontend',
    'browser',
    'index.html'
  );
  if (existsSync(ngFrontendPath)) {
    return ngFrontendPath;
  }

  // Return null or throw an error if neither path exists
  return null;
}

function getDevFrontendPath(environment) {
  switch (environment) {
    case 'ng-frontend':
      return 'dist/apps/ng-frontend/browser/index.html';
    default:
      throw new Error('Invalid NODE_ENV');
  }
}

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 1400,
    height: 900,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  if (isDev) {
    console.log('isDev', isDev);
    const devFrontendPath = getDevFrontendPath(process.env.NODE_ENV);
    mainWindow.loadFile(devFrontendPath);
    mainWindow.webContents.openDevTools(); // Open DevTools in development
  } else {
    // https://www.electronjs.org/docs/latest/tutorial/application-distribution
    try {
      const entryPath = getProductionFrontendPath();
      mainWindow.loadFile(entryPath);
      mainWindow.webContents.openDevTools(); // Open DevTools in development
    } catch (e) {
      console.error(e);
    }
  }
}

app.whenReady().then(() => {
  startBackend();
  createWindow();
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});
        

And the configuration:

// forge.config.js at the root directory
if (process.env.NODE_ENV === 'ng-frontend') {
  module.exports = {
    packagerConfig: {
      asar: true,
      extraResource: [
        './dist/apps/ng-frontend',
        './dist/apps/nest-backend/main.js',
      ],
    },
    rebuildConfig: {},
    makers: [
      {
        name: '@electron-forge/maker-squirrel',
        config: {},
      },
      {
        name: '@electron-forge/maker-zip',
        platforms: ['darwin'],
      },
      {
        name: '@electron-forge/maker-deb',
        config: {},
      },
      {
        name: '@electron-forge/maker-rpm',
        config: {},
      },
    ],
    plugins: [
      {
        name: '@electron-forge/plugin-auto-unpack-natives',
        config: {},
      },
    ],
  };
}        

Achieving seamless CI/CD within a single repository is made effortless with Nx.

In previous projects, I've encountered the challenges of managing separate repositories for frontend and backend components. This approach often resulted in difficulties during development and debugging. With the frontend relying on pre-built backend services, developers lacked visibility into the backend's implementation, leading to complexities in identifying and resolving issues. Moreover, since the frontend and backend were decoupled, troubleshooting problems originating from either side proved to be cumbersome.


However, by consolidating both frontend and backend within a unified Nx workspace, these challenges are effectively mitigated. Developers benefit from enhanced visibility and streamlined collaboration, as the entire application stack is readily accessible within a single repository. This not only simplifies debugging processes but also fosters a more cohesive development environment.


In conclusion, embracing modern development practices and utilizing powerful tools like Nx, we empower developers to deliver high-quality solutions while maximizing productivity and minimizing overhead. With the right tools and methodologies in place, we can confidently navigate the complexities of software architecture and drive innovation in the ever-evolving landscape of technology.

要查看或添加评论,请登录

Guan Xin Wang的更多文章

社区洞察

其他会员也浏览了