打造极致electron脚手架

1,347 阅读4分钟

前言

  • 为什么会有想法做这个东西?
    • 之前做过类似的项目,很烂,本着强迫症的性格,不得不重构(相信你也有类似的经历)。
      • 当前的文档介绍的脚手架,已经是基于之前项目中重构过的基础上,再次做了重构,力求精益求精
    • 内心的一个心愿吧,一直想做跨端应用开发。这个心愿从大学开始就有,如今已经过去了将近四年,回想从之前的qt(c/c++)做的桌面和安卓应用,到后面wpf(c#)纯pc开发,再到后面的electron(js),一路走来,始终有这样一个心愿【做跨端软件】,不止于pc。而前端给了我们无限可能。
    • 接触到的市面上的脚手架都不能满足业务需求,比较基础,需要进行定制化处理。本着赠人玫瑰,手留余香的精神,为后来者铺路(emm...)。

介绍

  • 该脚手架会集成electron端、前端、node服务端
  • 技术选型
    • electron(必须)
    • react
    • typescript
    • webpack(分别对前端和electron端进行打包)
    • gulp(做集成打包)

目标

  • 前端代码和electron代码分离,前端代码脱离electron可以单独部署
  • electron打包全面配置化
    • 将配置从package.json文件中抽离成可扩展的打包配置文件,方便定制化打包需求
    • 对electron端代码进行webpack打包(源码保护,相当于简单的混淆)

工程介绍

  • 整体目录结构
- app                     // electron相关代码目录
- client                  // 前端相关代码目录
- gulpfile.js             // 集成打包目录 
- internals               // webpack打包和electron-builder打包目录
- babel.config.js
- package.json
- tsconfig.json
- README.md
- yarn.lock
  • electron端
- app
    - build               // 生成最终构建好的代码,包括前端代码、node服务端代码、electron相关代码
    - src
        - server          // node服务端代码
        - preloadScript   // 注入到前端页面,赋予前端页面electron的部分能力
        - components      // 封装一些electron组件
        - config          // electron和server相关配置【两者存在共用配置的场景】
        - index.ts        // 程序主入口
  • web前端
- client
    - common              // 抽离所有模块的公共部分,包括图标、组件等等
        - platform        // 抽象前端和electron端通信部分,使前端代码脱离electron环境可以正常运行
    - modules             // 前端业务模块
        login             // 登录模块【单独模块,可以单独部署】
  • webpack打包集成
    • 抽取公共基础配置[webpack.base.js]
    const path = require("path");
    
    const buildModules = function (externalModule) {
      let rules = [
        {
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
        {
          rules: [
            {
              test: /\.jsx?$/,
              exclude: /node_modules/,
              use: [
                {
                  loader: require.resolve("babel-loader"),
                },
              ],
            },
          ],
        },
      ];
    
      if (externalModule && externalModule.rules) {
        rules = rules.concat(externalModule.rules);
      }
    
      return {
        rules,
      };
    };
    
    const buildPlugins = function (externalPlugins) {
      const plugins = [];
      return plugins.concat(externalPlugins);
    };
    
    module.exports = (options) => ({
      mode: options.mode || "production",
      entry: options.entry,
      output: Object.assign(
        {
          path: "/",
          globalObject: "this",
        },
        options.output
      ),
      module: buildModules(options.module),
      plugins: buildPlugins(options.plugins || []),
      externals: options.externals || {},
      devtool: options.devtool,
      target: options.target,
      node: options.node || {},
      resolve: options.resolve,
    });
    
    
    • 渲染进程(前端代码)
    const getEntry = require("../scripts/getEntry");
    const path = require("path");
    const { AssetOutputPlugin } = require("../plugins");
    
    module.exports = require("./webpack.base")({
      devtool: "inline-source-map",
      mode: "development",
      // 【重要】:需要设置打包类型为electron-render
      target: "electron-renderer",
      entry: getEntry(),
      output: {
        path: path.join(process.cwd(), "app/build/statics"),
        filename: "[name].js",
        publicPath: "/statics",
      },
      node: {
        __dirname: false,
        __filename: false,
      },
      // 【重要】:自定义asset插件,输出打包之后的模块依赖文件表
      plugins: [new AssetOutputPlugin()],
      resolve: {
        modules: ["client", "node_modules"],
        extensions: [".js", ".jsx", ".ts", ".tsx", ".react.js"],
        mainFields: ["browser", "jsnext:main", "main"],
        alias: {
          "@common": path.join(process.cwd(), "client/common"),
          "@login": path.join(process.cwd(), "client/modules/login"),
        },
      },
    });
    
    • 主进程(electron相关代码)
    const path = require("path");
    const webpack = require("webpack");
    const CopyWebpackPlugin = require("copy-webpack-plugin");
    
    module.exports = require("./webpack.base")({
      mode: "production",
      devtool: "eval-source-map",
      entry: {
        main: path.join(process.cwd(), "app/src/index.ts"),
        // 【重要】:需要对注入到渲染进程的preload脚本做打包,因为在electron中,没有对该文件使用,不会进行打包
        preloadMain: path.join(process.cwd(), "app/src/preloadScript/main.ts"),
        preloadRender: path.join(process.cwd(), "app/src/preloadScript/render.ts"),
      },
      output: {
        path: path.join(process.cwd(), "app/build"),
        globalObject: "this",
        libraryTarget: "commonjs2",
      },
      // 【重要】:需要设置打包类型为electron-main
      target: "electron-main",
      node: {
        __dirname: false,
        __filename: false,
      },
      plugins: [
        new webpack.EnvironmentPlugin({
          NODE_ENV: "production",
          DEBUG_PROD: false,
          START_MINIMIZED: false,
        }),
        // 【重要】:node服务端使用了ejs模版,在打包的时候,需要将模版文件移动到打包输入文件夹
        new CopyWebpackPlugin({
          patterns: [
            {
              from: path.join(process.cwd(), "app/src/server/views"),
              to: path.join(process.cwd(), "app/build/views"),
            },
          ],
        }),
      ],
      resolve: {
        modules: ["app", "node_modules"],
        extensions: [".js", ".jsx", ".ts", ".tsx"],
        mainFields: ["browser", "jsnext:main", "main"],
      },
    });
    
    • electron-builder集成打包
      • 文件结构
      - internals
          - package
              - config   // 打包配置
                  - base.js
                  - mac.js
                  - windows.js
                  - linux.js
              - ci      // 真正的打包build
                  - base.js
                  - mac.js
                  - windows.js
                  - linux.js
              - build.js
      
      • 打包配置代码【配置完善中,具体可以参照electron-builder配置说明】
      module.exports = (options) => ({
        // 打包出来的安装包的名字
        productName: options?.productName || "demonbe-electron",
        appId: options?.appId || "com.demonbe.app",
        copyright: options?.copyright || "Copyright © year Jake",
        // 打包输出路径
        directories: {
          output: "packages",
        },
        // 需要打包进安装包的文件【!xxx 表示不需要打进安装包】
        files: ["!src/", "!tsconfig.json"],
      });
      
      • 打包build
      const electronBuilder = require("electron-builder");
      class Base {
        // 打包配置
        config = {};
      
        /**
         * @description 打包钩子,可以在打包之前定义一些需要做的事情,比如文件拷贝
         */
        beforBuild() {}
      
        /**
         * @description 打包,集成打包流程
         */
        build() {
          this.beforBuild();
          this.electronBuild();
          this.beforBuild();
        }
      
        /**
         * @description 打包,使用electron-builder构建打包
         */
        electronBuild() {
          electronBuilder.build(this.config);
        }
      
        /**
         * @description 打包钩子,可以在打包之后定义一些需要做的事情,比如安装包的定制,需要进行二次打包的
         */
        afterBuild() {}
      }
      
      module.exports = Base;
      

后续规划

  • 设计electron端支持插件化能力
  • 将electrin-cli打造成类似于create-react-app的cli
  • ...(有想法可以一起沟通,逐步完善)

项目地址