如何从零实现一个todo list(4)

项目地址 希望收到一个小小的star

一、Electron版本实现

因为electron本身就有个chrome浏览器内核,从开发的角度上讲其实就是在开发浏览器的页面以及nodejs编写自己的逻辑和调用electron底层实现的api。如果只是单纯性想展示一个页面的话就直接把开发好的vue地址贴上去,创建一个窗口并且不需要任何除了窗口创建以外的node.js代码,但是有一些存在的缺陷

Electron快速入门

我的Electron开发方式:

  1. 前端与electron底层项目分离 开发环境下以localhost:port的形式渲染 打包后以域名形式或者打包的前端项目读取本地文件的形式 需要两个IDE 左边调完右边调(目前常用)
  2. electron与前端项目存在同一个文件夹下 通过webpack的配置让项目可以同时运行前端项目和electron项目 并且支持electron随代码改动进行重载 打包后以域名形式或者打包的前端项目读取本地文件的形式(昨天研究了一下 并且测试开发环境正常并且打包出来的exe是正常的)

确认开发方式

因为这个项目是非正式的项目 所以我们使用第二种方式 所以我们开始对文件目录结构进行调整

package.json调整

json 复制代码
{
  "name": "todo-list",
  "displayName": "TodoList",
  "version": "0.1.0",
  "private": true,
  "author": "yt",
  "main": "main.js",
  "scripts": {
    "dev": "node ./runner/dev-runner.js",
    "pack": "node ./runner/pack.js && node ./runner/build.js",
    "pack-render": "node ./runner/pack-render.js",
    "only-build": "node ./runner/build.js"
  },
  "dependencies": {   // vue原来的依赖不变
    "core-js": "^3.6.5",
    "moment": "^2.30.1",
    "vue": "^2.6.11",
    "vue-contextmenujs": "^1.4.11",
    "vue-uuid": "^3.0.0",
    "vuetify": "^2.7.2",
    "vuex": "^3.4.0"
  },
  "devDependencies": {  // 新增webpack开发依赖 打包时不会把他们打包进去
    "spinnies": "^0.5.1",
    "chalk": "^2.4.1",
    "del": "^6.1.1",
    "@vue/cli-plugin-babel": "~4.5.4",
    "@vue/cli-plugin-vuex": "~4.5.4",
    "@vue/cli-service": "~4.5.4",
    "deepmerge": "^4.3.1",
    "sass": "^1.32.13",
    "sass-loader": "^10.1.1",
    "vue-template-compiler": "^2.6.11",
    "babel-minify-webpack-plugin": "^0.3.1",
    "electron": "27.3.6",
    "vue-html-loader": "^1.2.4",
    "vue-loader": "^15.2.4",
    "vue-style-loader": "^4.1.0",
    "webpack-cli": "^3.0.8",
    "webpack": "^4.15.1",
    "webpack-dev-server": "^3.1.4",
    "webpack-hot-middleware": "^2.22.2",
    "webpack-merge": "^4.1.3"
  }
}

创建runner文件夹 以及开发环境与打包环境的webpack配置文件夹

开发环境热重载的功能编写(以下涉及到webpack的部分)

创建dev-runner.js 此部分是直接复用electron-vue的代码 为啥呢 虽然electron-vue这个项目废弃了 但是这个功能还是很好用的 并且集成了electron和vue在一个项目下 开启两个webpack 一个监听electron代码变动 去重载electron的应用 另一个去监听vue的代码变动

javascript 复制代码
//runner/dev-runner.js
"use strict";

const chalk = require("chalk");
const electron = require("electron");
const path = require("path");
const { spawn } = require("child_process");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const webpackHotMiddleware = require("webpack-hot-middleware");

const mainConfig = require("./dev/webpack.main.config");
const rendererConfig = require("./dev/webpack.renderer.config");

let electronProcess = null;
let manualRestart = false;
let hotMiddleware;

function logStats(proc, data) {
  let log = "";
  log += chalk.yellow.bold(`┏ ${proc} Process ${new Array(19 - proc.length + 1).join("-")}`);​
  log += "\n\n";

  if (typeof data === "object") {
    data
      .toString({
        colors: true,
        chunks: false,
      })
      .split(/\r?\n/)
      .forEach((line) => {
        log += "  " + line + "\n";
      });
  } else {
    log += `  ${data}\n`;
  }

  log += "\n" + chalk.yellow.bold(`┗ ${new Array(28 + 1).join("-")}`) + "\n";

  console.log(log);
}

function startRenderer() {
  return new Promise((resolve, reject) => {
    rendererConfig.entry.renderer = rendererConfig.entry.renderer;
    rendererConfig.mode = "development";
    const compiler = webpack(rendererConfig);
    hotMiddleware = webpackHotMiddleware(compiler, {
      log: false,
      heartbeat: 2500,
    });

    compiler.hooks.compilation.tap("compilation", (compilation) => {
      compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync("html-webpack-plugin-after-emit", (data, cb) => {
        hotMiddleware.publish({ action: "reload" });
        cb();
      });
    });

    compiler.hooks.done.tap("done", (stats) => {
      logStats("Renderer", stats);
    });

    const server = new WebpackDevServer(compiler, {
      contentBase: path.join(__dirname, "../"),
      quiet: true,
      hot: true,
      before(app, ctx) {
        // app.use(hotMiddleware)
        ctx.middleware.waitUntilValid(() => {
          resolve();
        });
      },
    });

    server.listen(9080);
  });
}

function startMain() {
  return new Promise((resolve, reject) => {
    mainConfig.entry.main = [path.join(__dirname, "../src/main/index.js")];
    mainConfig.mode = "development";
    const compiler = webpack(mainConfig);

    compiler.hooks.watchRun.tapAsync("watch-run", (compilation, done) => {
      logStats("Main", chalk.white.bold("compiling..."));
      hotMiddleware.publish({ action: "compiling" });
      done();
    });

    compiler.watch({}, (err, stats) => {
      if (err) {
        console.log(err);
        return;
      }

      logStats("Main", stats);

      if (electronProcess && electronProcess.kill) {
        manualRestart = true;
        process.kill(electronProcess.pid);
        electronProcess = null;
        startElectron();

        setTimeout(() => {
          manualRestart = false;
        }, 5000);
      }

      resolve();
    });
  });
}

function startElectron() {
  var args = ["--inspect=5858", path.join(__dirname, "../dist/electron/main.js")];​
​
  // detect yarn or npm and process commandline args accordingly​
  if (process.env.npm_execpath.endsWith("yarn.js")) {​
    args = args.concat(process.argv.slice(3));​
  } else if (process.env.npm_execpath.endsWith("npm-cli.js")) {​
    args = args.concat(process.argv.slice(2));​
  }​
​
  electronProcess = spawn(electron, args);​
​
  electronProcess.stdout.on("data", (data) => {​
    electronLog(data, "blue");​
  });​
  electronProcess.stderr.on("data", (data) => {​
    electronLog(data, "red");​
  });​
​
  electronProcess.on("close", () => {​
    if (!manualRestart) process.exit();​
  });​
}​
​
function electronLog(data, color) {​
  let log = "";​
  data = data.toString().split(/\r?\n/);​
  data.forEach((line) => {​
    log += `  ${line}\n`;​
  });​
  if (/[0-9A-z]+/.test(log)) {​
    console.log(chalk[color].bold("┏ Electron -------------------") + "\n\n" + log + chalk[color].bold("┗ ----------------------------") + "\n");​
  }​
}​
​
function init() {​
  Promise.all([startRenderer(), startMain()])​
    .then(() => {​
      startElectron();​
    })​
    .catch((err) => {​
      console.error(err);​
    });​
}​
​
init();​

开发环境所用的webpack配置

electron代码的webpack配置
javascript 复制代码
//runner/dev/wepack.main.config.js
"use strict";

process.env.BABEL_ENV = "main";

const path = require("path");
const { dependencies } = require("../../package.json");
const webpack = require("webpack");

let mainConfig = {
  entry: {
    main: path.join(__dirname, "../../src/main/index.js"),
  },
  // 排除的打包依赖
  externals: [...Object.keys(dependencies || {})],
  module: {
    rules: [
      {
        test: /.js$/,
        use: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /.node$/,
        use: "node-loader",
      },
    ],
  },
  node: {
    __dirname: process.env.NODE_ENV !== "production",
    __filename: process.env.NODE_ENV !== "production",
  },
  output: {
    filename: "[name].js",
    libraryTarget: "commonjs2",
    path: path.join(__dirname, "../../dist/electron"),
  },
  plugins: [
    new webpack.DefinePlugin({
      __static: `"${path.join(__dirname, "../../static").replace(/\/g, "\\")}"`,
    }),
  ],
  resolve: {
    extensions: [".js", ".json", ".node"],
  },
  target: "electron-main",
};

module.exports = mainConfig;
vue代码的webpack
javascript 复制代码
//runner/dev/wepack.renderer.config.js
"use strict";

process.env.BABEL_ENV = "renderer";

const path = require("path");
const { dependencies } = require("../../package.json");
const webpack = require("webpack");

const MinifyPlugin = require("babel-minify-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");

/**
 * List of node_modules to include in webpack bundle
 *
 * Required for specific packages like Vue UI libraries
 * that provide pure *.vue files that need compiling
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
 */
let whiteListedModules = ["vue", "vuetify", "vue-contextmenujs"];

let rendererConfig = {
  devtool: "#cheap-module-eval-source-map",
  entry: {
    renderer: path.join(__dirname, "../../src/renderer/main.js"),
  },
  externals: [...Object.keys(dependencies || {}).filter((d) => !whiteListedModules.includes(d))],
  module: {
    rules: [
      {
        test: /.s(c|a)ss$/,
        use: ["vue-style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /.css$/,
        use: ["vue-style-loader", "css-loader"],
      },
      {
        test: /.html$/,
        use: "vue-html-loader",
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@vue/cli-plugin-babel/preset"],
          },
        },
      },
      {
        test: /.node$/,
        use: "node-loader",
      },
      {
        test: /.vue$/,
        use: {
          loader: "vue-loader",
        },
      },
      {
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
        use: {
          loader: "url-loader",
          query: {
            limit: 10000,
            name: "imgs/[name]--[folder].[ext]",
          },
        },
      },
    ],
  },
  node: {
    __dirname: process.env.NODE_ENV !== "production",
    __filename: process.env.NODE_ENV !== "production",
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({ filename: "styles.css" }),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: path.resolve(__dirname, "../../src/index.ejs"),
      title: require("../../package.json").name,
      templateParameters(compilation, assets, options) {
        return {
          compilation: compilation,
          webpack: compilation.getStats().toJson(),
          webpackConfig: compilation.options,
          htmlWebpackPlugin: {
            files: assets,
            options: options,
          },
          process,
        };
      },
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
      },
      nodeModules: process.env.NODE_ENV !== "production" ? path.resolve(__dirname, "../../node_modules") : false,
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.DefinePlugin({
      __static: `"${path.join(__dirname, "../../static").replace(/\/g, "\\")}"`,
    }),
  ],
  output: {
    filename: "[name].js",
    libraryTarget: "commonjs2",
    path: path.join(__dirname, "../../dist/electron"),
  },
  resolve: {
    alias: {
      "@": path.join(__dirname, "../../src/renderer"),
      vue$: "vue/dist/vue.esm.js",
    },
    extensions: [".js", ".vue", ".json", ".css", ".node"],
  },
  target: "electron-renderer",
};

module.exports = rendererConfig;

这样子我们就实现了基础的热重载功能 运行npn run dev即可

src文件夹目录结构

在根目录下创建一个src文件夹 并且创建两个子文件夹 main 与 renderer 一个是存放electron代码 一个是存放vue代码的 并且创建一个index.ejs 这个地方使用了webpack打包的HtmlWebpackPlugin 用这个index.ejs作为模板去生成最后的index.html 这个部分代替了vue下的public 所以vue的public可以删除了

html 复制代码
// src/index.ejs
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title><%= htmlWebpackPlugin.options.title %></title>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
  <% if (htmlWebpackPlugin.options.nodeModules) { %>
    <!-- Add `node_modules/` to global paths so `require` works properly in development -->
    <script>
      require('module').globalPaths.push('<%= htmlWebpackPlugin.options.nodeModules.replace(/\/g, '\\') %>')
    </script>
    <% } %>
      <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
      </style>
</head>

<body>
  <div id="app"></div>
  <!-- Set `__static` path to static files in production -->
  <% if (!process.browser) { %>
    <script>
      if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\/g, '\\')
    </script>
    <% } %>

      <!-- webpack builds are automatically injected -->
</body>

</html>

src/renderer的结构

将原来的整个vue移进来就好了 并且删除public文件夹

src/main的结构

一个index.js 作为主入口 后续的listener.js 和listeners下是我自己写的脚本

二、进行electrton的开发

src/main/index.js编写

javascript 复制代码
const { app, BrowserWindow, Menu } = require("electron");
const { ListenerManager } = require("./listener");

const createWindow = () => {
  Menu.setApplicationMenu(null); // null值取消顶部菜单栏
  const win = new BrowserWindow({
    transparent: false,
    // resizable: false,
    width: 400,
    height: 700,
    minHeight: 700,
    minWidth: 400,
    // resizable: false,
    title: "Todo List",
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
    frame: false, // 顶部操作栏
  });
  const winURL = process.env.NODE_ENV === "development" ? `http://localhost:9080` : `file://${__dirname}/index.html`;

  // 后续可打包放在本地也可以 使用在线的地址
  win.loadURL(winURL);

  return win;
};

app.on("ready", () => {
  const window = createWindow();
  new ListenerManager(app, window);
  if (!app.isPackaged) {
    // 如果不打包,默认开启开发模式,开发者模式控制台
    window.webContents.openDevTools({ mode: "detach" });
  }
});

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

src/main/listener.js

此脚本用于electron创建on与handle监听用

javascript 复制代码
/*
 * @Author: 羊驼
 * @Date: 2022-01-25 11:30:52
 * @LastEditors: 羊驼
 * @LastEditTime: 2024-04-18 14:40:31
 * @Description: electron 监听定义类
 */

const process = require("process");
const { ipcMain } = require("electron");
class BaseListener {
  // 是否开启监听
  enable = true;
  // electron 存储的数据
  window = null;
  app = null;

  mounted = false;
  listener = {};
  handler = {};
  constructor(app, window, eventCollect, handlerCollect) {
    this.window = window;
    this.app = app;

    setTimeout(() => {
      try {
        if (this.enable) {
          let listener = this.listener;
          let handler = this.handler;
          for (let kv in listener) {
            eventCollect.push(kv);
            ipcMain.on(kv, listener[kv]);
          }
          for (let kv in handler) {
            handlerCollect.push(kv);
            ipcMain.handle(kv, handler[kv]);
          }
          this.mounted = true;
        }
      } catch (e) {
        console.log(`脚本初始化出现问题 原因:${e}`);
        // process.exit(1);
      }
    }, 100);
  }
}

class ListenerManager {
  listenerMap = new Map();
  listenerCollect = [];
  handlerCollect = [];
  constructor(app, window) {
    const fs = require("fs");
    let scripts = fs.readdirSync(__dirname + "\listeners");
    scripts.forEach((item) => {
      item = item.replace(".js", "");
      let listener = require(`./listeners/${item}`);
      this.listenerMap.set(item, listener);
      try {
        let event = new listener(app, window, this.listenerCollect, this.handlerCollect);
        this.listenerMap.set(item, event);
      } catch (e) {
        console.log(`该文件夹下不存在继承BaseListener的脚本 ${e}`);
        process.exit(1);
      }
    });
    this.checkRepeatListener();
  }

  checkRepeatListener() {
    for (let value of this.listenerMap.values()) {
      if (!value.mounted) {
        setTimeout(() => {
          this.checkRepeatListener();
        }, 100);
        return;
      }
    }
    let set = new Set();
    this.listenerCollect.forEach((item) => {
      if (set.has(item)) {
        console.log(`存在同名监听:${item}`);
        // process.exit(1);
      }
      set.add(item);
    });
    set = new Set();
    this.handlerCollect.forEach((item) => {
      if (set.has(item)) {
        console.log(`存在同名handle:${item}`);
        // process.exit(1);
      }
      set.add(item);
    });
  }
}

module.exports = {
  ListenerManager,
  BaseListener,
};

src/main/listeners/common.js

javascript 复制代码
const { BaseListener } = require("../listener");

class commonListener extends BaseListener {
  listener = {};

  handler = {
    // 关闭
    exit: () => {
      this.app.quit();
    },
    // 最小化
    minimize: () => {
      this.window.minimize();
    },
    // 全屏调整
    fullscreen: () => {
      this.window.setFullScreen(!this.window.isFullScreen());
    },
  };
}

module.exports = commonListener;

electron的ipcMain.handle 与 vue中的ipcRenderer.invoke是一对 如果要获取handle的return数据 必须要await或者then

运行开发环境代码 npm run dev

编写App.vue工具栏的代码

html 复制代码
<!--src/renderer/App.vue -->
<template>
  <v-app :class="{dark}">

    <v-system-bar
      color="#EFEFEF"
      height="36px"
      window
      app
    >
      <v-icon>mdi-infinity</v-icon>
      <div class="app-name">Yangtuo To Do</div>
      <v-spacer></v-spacer>
      <v-btn
        text
        class="system-btn"
        x-small
        @click="invoke('minimize')"
      ><v-icon>mdi-window-minimize</v-icon></v-btn>
      <v-btn
        text
        class="system-btn"
        x-small
        @click="invoke('fullscreen')"
      > <v-icon>mdi-window-maximize</v-icon></v-btn>
      <v-btn
        text
        class="system-btn"
        x-small
        @click="invoke('exit')"
      > <v-icon>mdi-close</v-icon></v-btn>
    </v-system-bar>
    <!-- 这里使用我们自己封装的drawer 要根据页面宽度去解决侧边栏常驻 -->
    <left-bar />
    <!-- 编辑栏 -->
    <edit-bar />
    <!-- 建议栏 -->
    <suggest-bar />
    <v-main v-if="active_key">
      <v-toolbar
        :dark="dark"
        color="transparent"
        flat
        :height="active_key.id==1?'120px':'84px'"
        class="pr-2"
      >
        <v-toolbar-title>
          <v-app-bar-nav-icon
            @click="drawer=!drawer"
            class="d-sm-flex d-md-none"
          ></v-app-bar-nav-icon>
          <div class="pl-3 text-h5">{{active_key&&active_key.name}}</div>
          <div
            class="pl-3 mt-2 date"
            v-if="active_key.id==1"
          >{{today}}</div>
        </v-toolbar-title>
        <v-spacer></v-spacer>
        <v-btn
          text
          fab
          small
          v-if="active_key&&active_key.id==1"
          @click="suggest_drawer=!suggest_drawer"
        >
          <v-icon>mdi-lightbulb-outline</v-icon>
        </v-btn>

      </v-toolbar>

      <task-list />

    </v-main>
  </v-app>
</template>

<script>
import TaskList from "./components/TaskList.vue";
import mixin from "@/mixins/store";
import LeftBar from "./components/LeftBar.vue";
import SuggestBar from "./components/SuggestBar.vue";
import EditBar from "./components/EditBar.vue";
import moment from "moment";
moment.locale("zh-cn");
const { ipcRenderer } = require("electron");
export default {
  mixins: [mixin],
  name: "App",
  components: {
    TaskList,
    LeftBar,
    SuggestBar,
    EditBar,
  },
  computed: {
    today() {
      return moment().format("M月D日 dddd");
    },
  },
  methods: {
    // 调用electron的代码
    invoke(message) {
      ipcRenderer.invoke(message);
    },
  },
};
</script>,

<style lang="scss">
html {
  overflow-y: hidden !important;
}
.task-item {
  background-color: #eee;
  border-radius: 10px;
  cursor: pointer;
  &:hover {
    background-color: #ccc;
  }
}
.task-item.complete {
  .v-list-item__title,
  .v-list-item__subtitle {
    text-decoration: line-through;
    color: #ccc;
  }
}
.menu_item_icon {
  margin-right: 20px !important;
}
.edit-drawer {
  z-index: 1001 !important;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  width: 100vw;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  background-color: #6478c7;

  .v-system-bar {
    z-index: 1002;
    -webkit-app-region: drag;
    .system-btn {
      cursor: pointer;
      -webkit-app-region: no-drag !important;
    }
    .v-icon {
      font-size: 1rem !important;
    }
  }

  .menu_item_label {
    margin-left: 10px !important;
    font-size: 16px;
  }
  .app-name {
    font-size: 14px;
    user-select: none;
  }
  .date {
    font-size: 14px;
  }
  &.dark {
    .todo .v-input__slot {
      background-color: #fff !important;
      input {
        color: #000 !important;
        &::placeholder {
          color: #ccc !important;
        }
      }
      .v-icon,
      .white-text {
        color: #616161 !important;
      }
    }
    .title {
      color: #fff;
    }
    $subtitle: #616161;
    .task-item {
      background-color: #f6f6f6;
      color: #717171;
      .v-list-item__subtitle {
        color: $subtitle;
      }
      .v-input--selection-controls__input .v-icon {
        color: $subtitle !important;
      }
      .v-icon {
        color: $subtitle !important;
      }
    }
  }
}
</style>

三、electron打包的实现

编写prod/webpack.main.config.js的配置

javascript 复制代码
"use strict";

process.env.BABEL_ENV = "main";
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const { dependencies } = require("../../package.json");

let mainConfig = {
  target: "electron-main",
  entry: {
    main: path.join(__dirname, "../../src/main/index.js"),
  },
  // 没有用到打包依赖 所以全部排除
  externals: [...Object.keys(dependencies || {})],
  module: {
    rules: [
      {
        test: /.js$/,
        use: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /.node$/,
        use: "node-loader",
      },
    ],
  },
  node: {
    __dirname: false,
    __filename: false,
  },
  output: {
    filename: "[name].js",
    libraryTarget: "commonjs2",
    path: path.join(__dirname, "../../dist/electron"),
  },
  plugins: [
    new webpack.DefinePlugin({
      __static: `"${path.join(__dirname, "../../static").replace(/\/g, "\\")}"`,
    }),
    new webpack.DefinePlugin({
      "process.env": {
        NODE_ENV: '"production"',
      },
    }),
    new CopyWebpackPlugin([
      {
        from: path.join(__dirname, "../../src/main"),
        to: path.join(__dirname, "../../dist/electron"),
        ignore: ["index.js"],
      },
    ]),
  ],
  resolve: {
    extensions: [".js", ".json", ".node"],
  },
  performance: {
    maxEntrypointSize: 1024 * 1024 * 10,
    maxAssetSize: 1024 * 1024 * 20,
  },
  optimization: {
    minimize: true,
  },
};

module.exports = mainConfig;

编写prod/webpack.renderer.config.js的配置

javascript 复制代码
"use strict";

process.env.BABEL_ENV = "renderer";

const path = require("path");
const { dependencies } = require("../../package.json");
const webpack = require("webpack");

const MinifyPlugin = require("babel-minify-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");

/**
 * List of node_modules to include in webpack bundle
 *
 * Required for specific packages like Vue UI libraries
 * that provide pure *.vue files that need compiling
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
 */
let whiteListedModules = ["vue", "vuetify", "vue-contextmenujs"];

let rendererConfig = {
  mode: "production",
  devtool: false,
  entry: {
    renderer: path.join(__dirname, "../../src/renderer/main.js"),
  },
  externals: [],
  module: {
    rules: [
      {
        test: /.s(c|a)ss$/,
        use: ["vue-style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /.css$/,
        use: ["vue-style-loader", "css-loader"],
      },
      {
        test: /.html$/,
        use: "vue-html-loader",
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@vue/cli-plugin-babel/preset"],
          },
        },
      },
      {
        test: /.node$/,
        use: "node-loader",
      },
      {
        test: /.vue$/,
        use: {
          loader: "vue-loader",
        },
      },
      {
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
        use: {
          loader: "url-loader",
          query: {
            limit: 10000,
            name: "imgs/[name]--[folder].[ext]",
          },
        },
      },
    ],
  },
  node: {
    __dirname: false,
    __filename: false,
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({ filename: "styles.css" }),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: path.resolve(__dirname, "../../src/index.ejs"),
      title: require("../../package.json").name,
      templateParameters(compilation, assets, options) {
        return {
          compilation: compilation,
          webpack: compilation.getStats().toJson(),
          webpackConfig: compilation.options,
          htmlWebpackPlugin: {
            files: assets,
            options: options,
          },
          process,
        };
      },
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
      },
      nodeModules: false,
    }),
    // new MinifyPlugin(),
    new CopyWebpackPlugin([
      {
        from: path.join(__dirname, "../../static"),
        to: path.join(__dirname, "../../dist/electron/static"),
        ignore: [".*"],
      },
      {
        from: path.join(__dirname, "../../package.json"),
        to: path.join(__dirname, "../../dist/electron/package.json"),
      },
    ]),
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": '"production"',
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true,
    }),
  ],
  optimization: {
    minimize: true,
    splitChunks: {
      chunks: "initial",
      minChunks: 2,
    },
  },
  performance: {
    maxEntrypointSize: 1024 * 1024 * 10,
    maxAssetSize: 1024 * 1024 * 20,
    hints: "warning",
  },
  output: {
    filename: "[name].js",
    libraryTarget: "commonjs2",
    path: path.join(__dirname, "../../dist/electron"),
  },
  resolve: {
    alias: {
      "@": path.join(__dirname, "../../src/renderer"),
      vue$: "vue/dist/vue.esm.js",
    },
    extensions: [".js", ".vue", ".json", ".css", ".node"],
  },
  target: "electron-renderer",
};

module.exports = rendererConfig;

编写webpack打包electron与vue用的代码 runner/pack.js

javascript 复制代码
process.env.NODE_ENV = "production";

const chalk = require("chalk");
const del = require("del");
const webpack = require("webpack");
const Spinnies = require("spinnies");

const mainConfig = "./prod/webpack.main.config";
const rendererConfig = "./prod/webpack.renderer.config";
const errorLog = chalk.bgRed.white(" ERROR ") + " ";
const okayLog = chalk.bgGreen.white(" OKAY ") + " ";

const { Worker, isMainThread, parentPort } = require("worker_threads");

function build() {
  console.time("build");
  del.sync(["dist/**", "build/**"]);

  const spinners = new Spinnies({ color: "blue" });
  spinners.add("main", { text: "main building" });
  spinners.add("renderer", { text: "renderer building" });
  let results = "";

  // m.on('success', () => {
  //   process.stdout.write('\x1B[2J\x1B[0f')
  //   console.log(`\n\n${results}`)
  //   console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
  //   process.exit()
  // })
  function handleSuccess() {
    process.stdout.write("\x1B[2J\x1B[0f");
    console.log(`\n\n${results}`);
    console.timeEnd("build");
    process.exit();
  }

  Promise.all([
    pack(mainConfig)
      .then((result) => {
        results += result + "\n\n";
        spinners.succeed("main", { text: "main build success!" });
      })
      .catch((err) => {
        spinners.fail("main", { text: "main build fail :(" });
        console.log(`\n  ${errorLog}failed to build main process`);
        console.error(`\n${err}\n`);
        process.exit(1);
      }),
    pack(rendererConfig)
      .then((result) => {
        results += result + "\n\n";
        spinners.succeed("renderer", { text: "renderer build success!" });
      })
      .catch((err) => {
        spinners.fail("renderer", { text: "renderer build fail :(" });
        console.log(`\n  ${errorLog}failed to build renderer process`);
        console.error(`\n${err}\n`);
        process.exit(1);
      }),
  ]).then(handleSuccess);
}

function pack(config) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(__filename);
    const subChannel = new MessageChannel();
    worker.postMessage({ port: subChannel.port1, config }, [subChannel.port1]);
    subChannel.port2.on("message", ({ status, message }) => {
      switch (status) {
        case "success":
          return resolve(message);
        case "error":
          return reject(message);
      }
    });
  });
}

function runPack(config) {
  return new Promise((resolve, reject) => {
    config = require(config);
    config.mode = "production";
    webpack(config, (err, stats) => {
      if (err) reject(err.stack || err);
      else if (stats.hasErrors()) {
        let err = "";

        stats
          .toString({
            chunks: false,
            modules: false,
            colors: true,
          })
          .split(/\r?\n/)
          .forEach((line) => {
            err += `    ${line}\n`;
          });

        reject(err);
      } else {
        resolve(
          stats.toString({
            chunks: false,
            colors: true,
          })
        );
      }
    });
  });
}

if (isMainThread) build();
else {
  parentPort.once("message", ({ port, config }) => {
    // assert(port instanceof MessagePort)
    runPack(config)
      .then((result) => {
        port.postMessage({
          status: "success",
          message: result,
        });
      })
      .catch((err) => {
        port.postMessage({
          status: "error",
          message: err,
        });
      })
      .finally(() => {
        port.close();
      });
  });
}

编写生成exe的脚本

我这边使用的是electron-packager 没有使用electron-builder

javascript 复制代码
const config = require("../package.json");
let name = `./${config.displayName}-win32-x64`;
const del = require("del");
del.sync([`${name}/**`]);

const spawnObj = require("child_process").exec(`electron-packager ./dist/electron ${config.displayName} --overwrite --asar `, {
  encoding: "utf-8",
  stdio: "pipe",
});

spawnObj.stdout.on("data", function (data) {
  console.log(data);
});

spawnObj.stderr.on("data", (data) => {
  console.log(data);
});

spawnObj.on("exit", (code) => {
  if (code === 0) {
    console.log("打包完成");
  } else {
    console.log("打包失败");
  }
});

为package.json添加命令

bash 复制代码
    "pack": "node ./runner/pack.js && node ./runner/build.js"

运行脚本

相关推荐
SleepyZone3 分钟前
Cline 源码浅析 - 从输入到输出
前端·ai编程·cline
Struggler2816 分钟前
pinia-基于monorepo的项目结构管理
前端
Struggler28110 分钟前
SSE的使用
前端
用户58061393930017 分钟前
前端文件下载实现深度解析:Blob与ObjectURL的完美协作
前端
Lin866620 分钟前
Vue 3 + TypeScript 组件类型推断失败问题完整解决方案
前端
coding随想20 分钟前
从零开始:前端开发者的SEO优化入门与实战
前端
前端工作日常23 分钟前
我理解的JSBridge
前端
Au_ust23 分钟前
前端模块化
前端
顺丰同城前端技术团队23 分钟前
还不会用 Charles?最后一遍了啊!
前端
BUG收容所所长25 分钟前
二分查找的「左右为难」:如何优雅地找到数组中元素的首尾位置
前端·javascript·算法