Electron + Vue + Vscode构建跨平台应用(五)Electron-Vue项目源码分析

2,364 阅读10分钟

Electron + Vue + Vscode构建跨平台应用(一)知识点补充
Electron + Vue + Vscode构建跨平台应用(二)Electron + Vue环境搭建
Electron + Vue + Vscode构建跨平台应用(三)利用webpack搭建vue项目
Electron + Vue + Vscode构建跨平台应用(四)利用Electron-Vue构建Vue应用详解

  通过上面几篇文章我们已经基本了解了如何利用Electron-Vue来构建Vue项目,但是项目是这么运行的,初始项目代码之间有什么关系,我们还不太清楚,所以这篇我们将主要解决如下四个问题

  1: 整个初始项目是怎么运行的
  2: npm run dev背后做了什么
  3: npm install到底根据什么策略去下载对应的依赖
  4: 如何打包成跨平台的文件,比如打包成windows平台的exe文件
针对第1个问题:整个初始项目是怎么运行的

  我们需要先了解一下electron当中的 app 模块和 BrowserWindow 模块

app模块 简介:

  1) app模块属于Electron里面的一个子模块,他负责整个应用的生命周期,有点类似Android里面的Applicatio类,当通过import关键字导致之后,便可以在项目当中使用了。

  2) 运行在主线程当中

  3)主要生命周期方法     3.1)ready:当 Electron 完成初始化时被触发,可用来做初始化动作

    3.2)window-all-closed:当所有的窗口都被关闭时触发。 如果没有监听此事件,当所有窗口都已关闭时,默认行为是退出应用程序。但如果你监听此事件, 将由你来控制应用程序是否退出。 如果用户按下了 Cmd + Q, 或者开发者调用了 app.quit() ,Electron 将会先尝试关闭所有的窗口再触发 will-quit 事件, 在这种情况下 window-all-closed 不会被触发。

    3.3)activate:当应用被激活时触发,常用于点击应用的 dock 图标的时候。

    3.4)before-quit:在应用程序开始关闭它的窗口的时候被触发。 调用 event.preventDefault() 将会阻止终止应用程序的默认行为。

    3.5)browser-window-created:当一个 BrowserWindow 被创建的时候触发。

  4)监听方法:可以通过app.on方法可监听Electron的整个生命周期中的事件,如监听ready生命周期

app.on('ready', createWindow)

BrowserWindow模块 简介:

BrowserWindow模块属于Electron里面的一个子模块,他负责创建和控制浏览器窗口,当通过import关键字导致之后,便可以在项目当中使用了。

2)常见属性设置 2.1)width Integer - 窗口宽度,单位像素. 默认是 800 2.2)height Integer - 窗口高度,单位像素. 默认是 600 2.3)center Boolean - 窗口屏幕居中

3)主要事件方法 3.1)page-title-updated 当文档改变标题时触发,使用 event.preventDefault() 可以阻止原窗口的标题改变.

3.2)close 在窗口要关闭的时候触发. 它在DOM的 beforeunload and unload 事件之前触发.使用 event.preventDefault() 可以取消这个操作

3.3)focus 在窗口获得焦点的时候触发.

4)监听事件 可以通过BrowserWindow.on方法可监听整个窗体的的事件,如监听closee事件

 mainWindow = new BrowserWindow({
    height: 563,
    useContentSize: true,
    width: 1000
  })
  mainWindow.on('closed', () => {
    mainWindow = null
  })

有了这些知识的铺垫,我们就能更好的解答第1个问题了,先看下主进程的入口文件src/main/index.js

'use strict'

import { app, BrowserWindow } from 'electron'

/**
 * Set `__static` path to static files in production
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
 */
if (process.env.NODE_ENV !== 'development') {
  global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}

let mainWindow
const winURL = process.env.NODE_ENV === 'development'
  ? `http://localhost:9080`
  : `file://${__dirname}/index.html`

function createWindow () {
  /**
   * Initial window options
   */
  mainWindow = new BrowserWindow({
    height: 563,
    useContentSize: true,
    width: 1000
  })

  mainWindow.loadURL(winURL)

  mainWindow.on('closed', () => {
    mainWindow = null
  })
}

app.on('ready', createWindow)

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

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

第1行,在js文件的开头引入严格模式,严格模式的意义简单理解就是规范代码编写,提早发现代码缺陷 第3行,通过import的方式,引入app模块和BrowserWindow模块 第18~26行,定义createWindow方法,创建BrowserWindow实例mainWindow,并设置窗口宽高分别为1000px,563px 第30行,监听窗口closed事件,当窗口关闭,将mainWindow赋值为null 第35行,监听应用程序ready事件,当应用程序准备运行的时候,调用createWindow方法

  总结: 主进程的入口文件src/main/index.js采用严格模式,对窗口和应用的生命周期进行检测,创建出窗体,这样渲染进程就可以在这个窗体展示内容了

  当主进程运行之后,这时候就到渲染进程了,渲染进程的入口文件是src/renderer/main.js

import Vue from 'vue'
import axios from 'axios'

import App from './App'
import router from './router'
import store from './store'

if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.http = Vue.prototype.$http = axios
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  components: { App },
  router,
  store,
  template: '<App/>'
}).$mount('#app')

  第1行,通过import的方式引入vue模块,我们看下引入vue的路径:from 'vue',之前我们通过npm install命令安装了一些依赖,你可以在node_modules文件夹中看到vue的源码,这里的from ‘vue’的具体位置其实就是node_modules文件夹中的vue   第4行:引入App模板,引入位置from './App',你可以参考项目源码,可以发现这个目录其实是src/renderer/App.vue   第5行:引入路由router,路由简单理解就是配置了跳转链接,它和a href标签最大的区别是通过路由进行的跳转页面不会发生刷新,但是通过a href标签进行的跳转页面会刷新一次   第6行:引入状态管理仓库,你可以理解为一个小型的数据库,其他模块可以对这个数据库的数据进行读写操作   第13行,匿名创建了Vue的实例类   第14行,声明组件App   第15行,在整个Vue实例中有路由属性router   第16行,在整个Vue实例中有仓储属性store   第17行,使用组件App,这个App组件的具体位置为src/renderer/App.vue,当使用了这个组件之后,那么App.vue即将开始渲染   第18行,将创建的Vue实例和元素id为app的标签进行绑定,这个id为app的标签来自于App.vue,我们也可以通过el:的方式将Vue实例和标签元素进行绑定

  总结: 渲染进程的入口文件src/renderer/main.js主要目的是挂载Vue实例到App.vue模板中的app元素当中,开始渲染整个窗口

  终于开始在窗口里面展示内容了,我们移步到App.vue中

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>
  export default {
    name: 'ele-vue-learning'
  }
</script>

<style>
  /* CSS */
</style>

  第1行,申明一个模板   第2行,创建一个id为app的层,这个就是vue挂载的具体位置,为什么要给vue挂载到一个具体的位置上,我的理解是类似java的类作用域,此处可理解为vue在层app内可见   第3行,通过‘router-view’标签使用路由,路由的位置为src/renderer/router/index.js,有了路由之后,就可以完成页面的跳转   第7行~第10行,一个Vue可以理解为一个模板或者一个java类,当使用export对其进行导出之后,其他模板或者类就可以通过import的方式使用它

  当加载完App.vue的时候,App.vue会根据路由route的配置跳转的其他页面,我们看下route的配置,其文件位置为:src/renderer/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'landing-page',
      component: require('@/components/LandingPage').default
    },
    {
      path: '*',
      redirect: '/'
    }
  ]
})

  第2行,引入vue-router路由,这里的vue-router和我们之前的src/renderer/router的路由不一样,vue-router是内置的路由模块,页面的跳转是由vue-router完成的,而src/renderer/router是一个配置文件   第2行,使vue具备路由功能   第7行,路由的地址配置是由routes数组负责创建   第9~11行,配置的路由名为‘landing-page’,具体地址为components/LandingPage.vue 这里使用LandingPage.vue模板是通过component: require方式执行的,他是异步加载组件的方法

  总结: App.vue中app标签挂在到Vue组件中,然后通过route-view的方式进入到router/index.js中,在index.js配置了路由功能,是窗口展示LandingPage.vue组件内容


  针对第2个问题:npm run dev背后做了什么

  在建立项目的时候,可以看见一个package.json的文件,如下图

在这里插入图片描述
  这个文件里面封装了很多脚本命令,当我们执行npm run dev的时候,其实就是执行package.json中的scripts中的dev命令,这个dev其实执行的是

node .electron-vue/dev-runner.js"

  这段脚本代码可以理解为在命令行模式中,执行 .electron-vue/dev-runner.js,我们看下dev-runner.js文件,我只截取了关键代码进行分析

function init () {
  greeting()

  Promise.all([startRenderer(), startMain()])
    .then(() => {
      startElectron()
    })
    .catch(err => {
      console.error(err)
    })
}

init()

dev-runner.js的主方法为init方法

  第2行,调用greeting方法,greeting方法主要作用是打印一些欢迎信息,所以当你执行npm run dev之后,你会发现如下的打印信息

在这里插入图片描述
他就是greeting方法打印出来的,他使用的字体是simple3d,黄色....

  重点看下第4行,通过调用 Promise.all方法,传入参数为数组[startRenderer(), startMain()];all方法传入的数组参数其内容返回的是Promise对象,我们可以看一下startRenderer方法,他返回的就是一个Promise,同理startMain方法也是返回一个Promise对象;

function startRenderer () {
  return new Promise((resolve, reject) => {
  ......
  }
function startMain () {
  return new Promise((resolve, reject) => {
  ......
  }

  Promise.all方法的含义是只有当参数成功执行之后,才会执行then之后的方法,也就是第6行的startElectron()方法,如果其中任意一个方法执行失败,则执行第9行的代码,打印erro信息

  我们看一下startRenderer方法的具体实现

function startRenderer () {
  return new Promise((resolve, reject) => {
    rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(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,
        before (app, ctx) {
          app.use(hotMiddleware)
          ctx.middleware.waitUntilValid(() => {
            resolve()
          })
        }
      }
    )

    server.listen(9080)
  })
}

  第22行,创建WebpackDevServer对象,WebpackDevServer用于创建一个http服务器   第22行,第36行,监听9080端口

  总结: startRenderer主要作用是创建一个监听9080端口的http服务器

  我们看一下startMain方法的具体实现

function startMain () {
  return new Promise((resolve, reject) => {
    mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
    mainConfig.mode = 'development'
    const compiler = webpack(mainConfig)
    ... ...
    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()
    })
  })
}

  第5行,调用webpack方法,传入mainConfig作为参数   第7行,启用 Watch 模式。这意味着在初始构建之后,webpack 将继续监听任何已解析文件的更改。

  总结: startMain方法主要作用是启用watch模式,当项目文件发生变化的时候,重新编译;所以你会发现当你修改项目并保存之后,npm run dev似乎有重新执行了一遍;

  当startRenderer和startMain顺利执行之后,便开始执行startElectron方法

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()
  })
}

  第14行,通过调用spawn方法,spawn方法实际实现是require('child_process'),这样开启一个子线程   第16~24行,在线程上监听服务器流信息


  针对第3个问题:npm install到底根据什么策略去下载对应的依赖

    当执行完npm install命令之后,我们可以发现在项目结构中多了一个node_modulesde的文件夹,这个文件夹存储的就是依赖文件包,这些依赖的配置也是根据package.json中的devDependencies配置来下载的,如下图

在这里插入图片描述

  在package.json中出现了两个关键属性devDependencies和dependencies, 其中devDependencies用于本地开发环境,是只会在开发环境下依赖的模块,生产环境不会被打入包内 而dependencies用于用户发布环境,dependencies依赖的包不仅开发环境能使用,生产环境也能使用


  针对第4个问题:如何打包成跨平台的文件,比如打包成windows平台的exe文件   在package.json中其实已经为我们配置好了相关的命令

在这里插入图片描述
所以我们只要执行如下命令即可

npm run build:win32

编译如果没有问题,会有如下提示

在这里插入图片描述

然后查看项目路径,你会发现在项目文件夹中的build目录中多了一个ele-vue-learning-win32-x64文件夹,这里面就是我们生成的exe文件

在这里插入图片描述

打开界面如下

在这里插入图片描述