Demand background

A nodeJS service developed with egg.js needs to be deployed to the customer site. According to the normal deployment operation, nodeJS development environment needs to be installed on the server, then proxy and mirror source are configured, dependencies are installed, and the service is started by NPM start command.

In actual business, the server is operated and maintained by the customer. This complex operation process is obviously not acceptable. What the user needs is an executable EXE file, which can automatically start the service by double-clicking.

The NodeJs runtime environment, NPM dependencies, and source code are all packaged together.

EncloseJS ‘writers strongly recommend users switch to PKG.

Dear users of EncloseJS. I highly encourage you to switch to github.com/zeit/pkg.

To open.

Initialize the Egg project

Egg’s official documentation goes into great detail, starting with a project that follows the instructions. If you’re already familiar with the Egg project, skip this section.

npm init egg --type=simpleCopy the code

If you encounter template download timeout, try adding proxy parameters to the initialization command line:

npm init egg --type=simple -r https://registry.npm.taobao.org/Copy the code

After execution, the project directory structure is as follows:

An egg - project ├ ─ ─ package. Json ├ ─ ─ app | ├ ─ ─ the router. The js │ ├ ─ ─ controller │ | └ ─ ─ home. Js ├ ─ ─ the config | ├ ─ ─ plugin. Js | ├ ─ ─ The config. Default. Js └ ─ ─test└ ─ ─ controller └ ─ ─ home. Test. JsCopy the code

NPM install install project dependencies:

npm installCopy the code

To ensure that the service starts properly, you also need to install egg-bin and egg-scripts globally:

npm install -g egg-bin egg-scriptsCopy the code

At this point, execute the NPM run dev script to start the server in development mode:

npm run devCopy the code

Then open the browser to port localhost:7001 and display hi, egg, and you have debugged the egg service environment.

Began to hit the pit

First install PKG globally so that PKG commands can be executed on the command line:

npm install -g pkgCopy the code

According to the PKG README, we can specify project entry files in three ways:

The entrypoint of your project is a mandatory CLI argument. It may be:

  • Path to entry file. Suppose it is /path/app.js, then packaged app will work the same way as node /path/app.js
  • Path to package.json. Pkg will follow bin property of the specified package.json and use it as entry file.
  • Path to directory. Pkg will look for package.json in the specified directory. See above.

Translation to the translator:

  • If you specify an entry file explicitly, such as /path/app.js, PKG will behave like node /path/app.js.
  • Specify package.json and PKG will use the bin property of package.json as the entry file.
  • Specify a directory where PKG will use the bin property of package.json as the entry file.

In the express project root directory, there is an entry file called app.js. Where is the entry file?

It was hidden by egg.

Following the example of using pm2 to start an Egg application, we can add server.js to the root directory as the entry file.

// server.js
const egg = require('egg');

const workers = Number(process.argv[2] || require('os').cpus().length);
egg.startCluster({
  workers,
  baseDir: __dirname,
});Copy the code

Also specify the entry file in package.json

{... +"bin": "server.js". }Copy the code

How is PKG packaged?

The PKG iterates through the entire project dependencies starting with the user-specified entry file, and when the javascript file is imported, the PKG compiles it into v8 bytecode and packages it into the executable (Scripts). When importing a static resource, the PKG copies the original content directly into the executable.

Will all the JS files and static resources PKG handle it for you?

No, it depends on how the resource is loaded. PKG automatically packages resources with paths specified by __dirname and __filename into executables, such as the require method (which internally uses __direname to locate resources) and via path.join(__dirname, “.. /path/to/asset”)

require('./router.js')
path.join(__dirname, './app/public/index.html')Copy the code

However, PKG is not recognized and automatically introduced for resource introduction of non-literal parameters, such as:

require('./build/' + cmd + '.js')
path.join(__dirname, 'views/' + viewName)Copy the code

At this point, we need to actively display the resources declared to be packaged in the package.json PKG property. In egg’s project, we first follow the following configuration, and we will adjust when we encounter pits:

// package.json
{
  ...
  "pkg": {
    "scripts": [
        "./app/**/*.js"."./config/*.js"]."assets": [
      "./app/public/**/*"]}... }Copy the code

For example, require(“./server.js”) is written in the code, but the current file directory only has one executable. How does the program run without server.js?

Here comes the PKG file resource loading mechanism.

The PKG modifies node’s FS API to intercept file operations and proxy them into its own virtual snapshot filesystem. For example, at runtime, the PKG will be propped to the /snapshot/server.js path (c:\snapshot on Windows) for server.js loads in require(“./server.js”). And returns the previously packed file contents from the virtual file snapshot system.

What types of file reads and writes hit the PKG virtual file snapshot system? The authors present the following table:

value with node packaged
__filename /project/app.js /snapshot/project/app.js
__dirname /project /snapshot/project
process.cwd() /project /deploy
process.execPath /usr/bin/nodejs /deploy/app-x64
process.argv[0] /usr/bin/nodejs /deploy/app-x64
process.argv[1] /project/app.js /snapshot/project/app.js
process.pkg.entrypoint undefined /snapshot/project/app.js
process.pkg.defaultEntrypoint undefined /snapshot/project/app.js
require.main.filename /project/app.js /snapshot/project/app.js

That is, if you want resources to be automatically packaged into an executable, use path locators such as __filename and __direname. On the other hand, if you want to access a real file system, such as loading environment variable files, you should use path locators such as process.cwd(), which makes it possible to dynamically change the startup or running parameters of the service, such as changing the service port number.

At this point, let’s consider a question: how to handle a situation like local run logging?

Yes, we need to specify the log path through process.cwd() in the Egg configuration file to perform a write to the real file system. Similarly, all reads and writes to the real file system are declared in the configuration file through process.cwd() :

// config/config.default.js
const path = require("path"); module.exports = appInfo => { ... // Config.rundir = process.cwd() +'/run'; Logger = {dir: path.join(process.cwd(),'logs'),}; // Configure the static resource path config.static = {prefix:'/',
    dir: process.cwd() + '/public'}; . }Copy the code

When executing the package command, we need to add the –targets option to specify the target format of the package: [nodeRange]-[platform]-[arch], where:

  • NodeRange: Specifies the node version, such as node${n} or latest
  • Platform: Specifies the operating system, such as FreeBSD, Linux, Alpine, MacOS, and Win
  • Arch: Specifies CPU architectures such as X64, x86, ARMV6, armV7

For example, node12-WIN-x64, multiple packaging target formats can be separated by commas.

You can also specify the targets option in the package.json PKG property:

// package.json
{
  "pkg": {
    "scripts": [
        "./app/**/*.js"."./config/*.js"]."assets": [
      "./app/public/**/*"]."targets": [
        "node12-win-x64"]}}Copy the code

At the same time, to facilitate packaging, we create a new NPM packaging script:

// package.json
{
  "scripts": {
    "build": "pkg . --out-path D:/ --debug"}}Copy the code

Note, specially through – out – the path parameter specifies the path and the current project is not the same as the output directory, is to ensure that packaging of executable file is also available from the current project environment, avoid some resources while no package to come in, but an executable file can still access from the actual file system, It helps to expose problems early.

At this point, we execute the following command to make an initial packaging attempt:

npm run buildCopy the code

This is where the first problem occurs: Node binaries fail to download due to Intranet proxy or other network reasons. Refer to Issues #419, you can manually download the node binary file from the PKg-fetch Release page and place it in the C:\Users\ user name \. Pkg-cache \v2.6\ directory and continue with the NPM run build command. Finally packaged:

The following error occurs when the executable file is started:

Take a look at the error message:

Error: [egg-core] load file: D:\snapshot\egg-pkg-template\node_modules\egg-security\app\extend\ context.js, error: Cannot find module ‘nanoid/non-secure’

This essentially means that one of the files in the egg-Security package was not packaged into our executable, causing the file to be lost at runtime.

We can explicitly package missing JS resources in PKG scripts:

// package.json
{
    "pkg": {
    "scripts": [
      "./app/**/*.js"."./config/*.js",
+     "./node_modules/nanoid/**/*.js"]."assets": [
      "./app/public/**/*"]."targets": [
      "node12-win-x64"]}}Copy the code

Execute the package command again, then run the executable:

At this time, although the service started successfully, but there is still an error, we then analyze the error log:

2020-07-28 23:11:43,493 ERROR 37628 nodejs.ENOENTError: ENOENT: no such file or directory, watch ‘D:\snapshot\egg-pkg-template\app’

  at FSWatcher.start (internal/fs/watchers.js:169:26)


  at Object.watch (fs.js:1415:11)


  at Walk.<anonymous> (D:\snapshot\egg-pkg-template\node_modules\wt\index.js:183:20)


  at Walk.emit (events.js:315:20)


  at D:\snapshot\egg-pkg-template\node_modules\ndir\lib\ndir.js:107:16


  at zalgoSafe (pkg/prelude/bootstrap.js:260:10)


  at pkg/prelude/bootstrap.js:962:9


  at pkg/prelude/bootstrap.js:336:5


  at pkg/prelude/bootstrap.js:314:14


  at FSReqCallback.wrapper [as oncomplete] (fs.js:516:5)

We need to add env to the config directory to specify that the current environment is production. Also, remember to modify the PKG configuration to ensure that the file is packaged.

// package.json
{
    "pkg": {
    "scripts": [
      "./app/**/*.js",
-     "./config/*.js",
+     "./config/*"."./node_modules/nanoid/**/*.js"]."assets": [
      "./app/public/**/*"]."targets": [
      "node12-win-x64"]}}Copy the code

Execute the package command again and run the executable program, starting smoothly:

At this time, the localhost:7001 port is opened, and the Egg service runs normally. At the same time, the log, RUN and public folders are added in the current directory, which confirms the configuration written by the real file system.

This is just a primer, the subsequent business code development is left to everyone to play. This project has been uploaded to github Coodool/ egg-pkG-template.

The resources

  • Package node.js projects (the Egg framework) as executable packages using PKG
  • Online deployment of egg.js – Package egg.js project with PKG
  • Package Node.js applications using PKG
  • github vercel/pkg
  • Egg Official Document