可视化构建工具小结(1-5)

2018-02-23  本文已影响361人  Gopal

项目简介
为了更加快速便捷的开发前端,将一些常见的功能进行可视化

项目结构

项目结构

Electron入门
首先是Electron的学习,项目中使用到了electron-vue。
electron-vue
基于vue来构造electron应用程序的样板代码

主要是为了避免使用vue手动建立起electron应用程序 。electron-vue充分利用了 vue-cli作为脚手架,加上拥有 vue-loader的webpack、可以使用electron-builder或者electron-packager打包,以及一些常见的插件,比如vue-router、vuex等
electron-vue的GitHub地址
使用的话,不多说。

electron视频教程
electron官方文档

重点:

重点1:electron通信
通过IPC(进程间通信)模块允许你在主进程和渲染进程之间发送和接受同步和异步消息
要清楚知道主进程和渲染进程的关系,而且渲染进程中可以通过.remote调用主进程中的模块,但是这样会造成性能损耗???(具体还是没有弄清楚)

const BrowserWindow = require('electron').remote.BrowserWindow

渲染进程:

const ipc = require('electron').ipcRenderer

const asyncMsgBtn = document.getElementById('async-msg')

asyncMsgBtn.addEventListener('click', function () {
  ipc.send('asynchronous-message', 'ping')
})

ipc.on('asynchronous-reply', function (event, arg) {
  const message = `异步消息回复: ${arg}`
  document.getElementById('async-reply').innerHTML = message
})

主进程:

const ipc = require('electron').ipcMain

ipc.on('asynchronous-message', function (event, arg) {
  event.sender.send('asynchronous-reply', 'pong')
})

渲染进程:

const ipc = require('electron').ipcRenderer

const syncMsgBtn = document.getElementById('sync-msg')

syncMsgBtn.addEventListener('click', function () {
  const reply = ipc.sendSync('synchronous-message', 'ping')
  const message = `同步消息回复: ${reply}`
  document.getElementById('sync-reply').innerHTML = message
})

主进程:

const ipc = require('electron').ipcMain

ipc.on('synchronous-message', function (event, arg) {
  event.returnValue = 'pong'
})
const BrowserWindow = require('electron').remote.BrowserWindow
const ipcRenderer = require('electron').ipcRenderer
const path = require('path')

const invisMsgBtn = document.getElementById('invis-msg')
const invisReply = document.getElementById('invis-reply')

invisMsgBtn.addEventListener('click', function (clickEvent) {
  const windowID = BrowserWindow.getFocusedWindow().id
  const invisPath = 'file://' + path.join(__dirname, '../../sections/communication/invisible.html')
  // 创建一个新的窗口
  let win = new BrowserWindow({ width: 400, height: 400, show: false })
  win.loadURL(invisPath)
  // 当导航完成时候发出事件,onload事件也要完成
  win.webContents.on('did-finish-load', function () {
    const input = 100
    // 不同的发送方式,第一个是事件名称,第二个是参数,第三个是窗口的ID
    // 通过channel发送异步消息给渲染进程,你也可以发送任意参数,渲染进程可以通过ipcRenderer监听channel处理信息。
    win.webContents.send('compute-factorial', input, windowID)
  })
})

ipcRenderer.on('factorial-computed', function (event, input, output) {
  const message = `${input} 的阶乘是 ${output}`
  invisReply.textContent = message
})

隐藏窗口页面的HTML:

<html>
  <script type="text/javascript">
    const ipc = require('electron').ipcRenderer
    const BrowserWindow = require('electron').remote.BrowserWindow

    ipc.on('compute-factorial', function (event, number, fromWindowId) {
      const result = factorial(number)
      // 根据ID查找窗口
      const fromWindow = BrowserWindow.fromId(fromWindowId)
      fromWindow.webContents.send('factorial-computed', number, result)
      window.close()
    })

    function factorial (num) {
      if (num === 0) return 1
      return num * factorial(num - 1)
    }
  </script>
</html>

重点2:background进程的实现
主要是用到了上面打开不可见窗口的方式实现

疑问点1

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

const childURL = process.env.NODE_ENV === 'development'
  ? `http://localhost:9081/background.html`
  : `file://${__dirname}/background.html`

为什么这里的路径要这么设置?还有为什么端口要不一样???

  // 创建一个不可见的窗口
  var winChild = new BrowserWindow({
    width: 700,
    height: 550,
    show: false
  })
  // 打开调试工具
  winChild.openDevTools()
  
  winChild.loadURL(childURL)

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

上面我们就创建了一个不可见的窗口了,这里的代码放在主进程中执行。

那么,我们主要是在background进程中做哪些操作呢?又怎么进行通信呢??

这里是要使用了大佬封装的Notifier

emmmmm,这个要自己看另一篇笔记。

主要的使用如下:
在background.js中注册模块和方法

/**
 * @file: 本文件主要实现background进程的操作
 * @description: 在后台进程中实施操作,防止阻塞主进程
 * @author: fenggp1
 * @copyright: COPYRIGHT (©) 2016-2017 深圳美云智数科技有限公司
 * @Created Time: 2017-12-27 16:44:51
 * @Last Modified By: fenggp1
 * @Last Modified Time: 2017-12-27 16:44:51
 */

import { ipcRenderer } from 'electron'
import bg from './lib/bg'
import Notifier from '../main/notifier/index.js'
import { resolve } from 'dns';
const rq = require('electron-require')
const path = require('path')
const remote = require('electron').remote
const dialog = remote.dialog
const ipcMain = require('electron').ipcMain
const BrowserWindow = require('electron').BrowserWindow
var shell = require('shelljs')
var child_process = require('child_process')
var exce = require('child_process').exec

// 通过registerModule注册一个模块,将模块的winId和模块暴露的方法记录在主进程
Notifier.getInstance().registerModule('ProjectManage', {
  a: () => {
    console.log('Gpout')
    return Promise.resolve(1)
  },
  /**
   * @description 选择一个项目
   * @returns {string} 项目的路径
   */
  selectProject: () => {
    console.log('replySel')
    return new Promise((resolve, reject) => {
      dialog.showOpenDialog({
        properties: ['openFile', 'openDirectory']
      }, (files) => {
        if (files) {
          console.log(files)
          resolve(files)
        }
      })
    })
  },
  /**
   * @description 新建一个项目
   * @param {object} form 文件的相关配置
   * @return {str} 创建的结果
   */
  createProject: (form) => {
    
    var projectName = form.settings.tplConfig.name
    console.log('项目名称:' + projectName)

    var workspace = form.workspace
    console.log('工作空间:' + workspace)

    var localMxCli = process.env.NODE_ENV === 'development' ? path.join(process.cwd(), './src/mx-cli/') : path.join(app.getAppPath(), '/../../MXCLI')
    // var localMxCli = process.env.NODE_ENV === 'development' ? path.join(process.cwd(), './src/mx-cli/') : path.join(app.getAppPath(), './mx-cli')
    console.log(localMxCli, "--------------------------")
    rq.set('local', localMxCli);

    var localMxFramework = process.env.NODE_ENV === 'development' ? path.join(process.cwd(), './src/templates/mx-framework/') : path.join(app.getAppPath(), '/../../Templates/MXFramework')
    // var localMxFramework = process.env.NODE_ENV === 'development' ? path.join(process.cwd(), './src/templates/mx-framework/') : path.join(app.getAppPath(), './templates/mx-framework')
    var mxInit = rq.local( 'index.js')

    // form = require('/Users/zhongjw/work/h5-res-mx-visual-builder/src/templates/mx-framework/meta.js')


    return new Promise((resolve, reject) => {
      mxInit(localMxFramework, path.join(workspace, projectName), form, function(result){
        resolve(result)
      })
    })

  }
})

疑问点2:如果很多模块的时候要怎么设置呢??全部放在background.js中?会不会很“臃肿”

然后在主进程index.js中初始化一下

import MainNotifier from './notifier/mainNotifier.js'
// Notifier初始化
MainNotifier.getInstance()

最后在渲染进程(其实background进程也是渲染进程)中调用模块和方法

Notifier.getInstance().resolveModule('ProjectManage').selectProject().then(filePath => {
    self.filepath = filePath[0]
    self.form.workspace = filePath[0]
})

重点3:类的使用

在主进程中需要进行一些操作,或者对页面的一些基础设置,如果全部将这些写在主进程中,显得不直观,而且很“臃肿”,这里使用了类进行管理
项目结构
主要使用了ES6中类的相关知识

项目结构

项目中读取和修改文件

/**
 * @file: 对文件进行管理操作
 * @description: 读取文件、修改文件等
 * @author: fenggp1
 * @copyright: COPYRIGHT (©) 2016-2017 深圳美云智数科技有限公司
 * @Created Time: 2017-12-27 17:12:14
 * @Last Modified By: fenggp1
 * @Last Modified Time: 2017-12-27 17:12:14
 */

var path = require('path')
var fsUtils = require('fs-utils')
var fs = require('fs')

/**
 * @module ./lib/file-manage
 * @default 'fileManage'
 * @constant {class} 文件管理类
 * @description 管理文件的类
 * @export
 * @class FileManage
 */
export default class FileManage {
  /**
   * @class
   * @classdesc 这里是一个构造函数
   * @memberof FileManage
   */
  constructor () {

  }
  /**
   * @description 将修改内容写入目标yaml文件
   * @param {string} dest 目标文件的路径
   * @param {object} str 写入的内容
   * @param {object} opts 配置
   * @access public
   * @memberof FileManage
   */
  writeFileYAML (dest, str, opts) {
    fsUtils.writeYAMLSync(dest, str, opts)
  }

  /**
   * @description 读取yaml文件的内容
   * @access public 
   * @param {string} filepath 目标文件的路径
   * @returns {object} 返回读取yaml的结果
   * @memberof FileManage
   */
  readFileYAML (filepath) {
    return fsUtils.readYAMLSync(filepath)
  }
  
}

在主进程中使用

import FileManage from './lib/file-manager'
var fileManage = new FileManage()
fileManage.writeFileYAML(targetFile.fileTotalPath, meta, {encoding: 'utf8'})
var readYamlResult = fileManage.readFileYAML(filePath)

重点4:模拟终端
这个路途有点曲折,首先开始用的是node.js中的exec方法或者说shell.js
但是将一个进程跑起来之后怎么杀死成为一个问题,也从网上找了一个解决方法,但是觉得还是不够严谨

下面是开始执行命令的方法和结束(将进程kill掉),但是这样只是解决了进程方面的问题,界面的话,还是没有得到解决(而且不够好)

Notifier.getInstance().registerModule('LocalDebug', {
  startRun: (currentProject) => {
    
    shell.cd(currentProject.path)
    this.childProcess = child_process.exec('npm run dev', (error, stdout, stderr) => {
      console.log('Gperror: '+ error + 'Gpstdout: ' + stdout + 'Gpstderr: ' + stderr)
    })
    return new Promise((resolve, reject) => {
      this.childProcess.stdout.on('data', (result) => {
        console.log('dataGp:' + result)
        resolve(result)
      })
    })
    
  },
  stopRun: () => {
    console.log(process.platform)
    var cmd = process.platform == 'win32'?'netstat -ano':'ps aux'
    var exec = require('child_process').exec
    var port = '8019'
    
    exec(cmd, function(err, stdout, stderr) {
      if (err) { 
        return console.log(err)
      }

      stdout.split('\n').filter(function(line) {
        var p = line.trim().split(/\s+/)
        var address = p[1]
        console.log(address)
  
        if (address != undefined) {
          if (address.split(':')[3] === port) {
            if (process.platform === 'win32') {
              exec('taskkill /F /pid ' + p[4], function(err, stdout, stderr) {
                if(err) {
                  resolve('释放指定端口失败!!')
                }
                resolve('占用指定端口的程序被成功杀掉!')
              })
              
            } else {
              exec('kill -9 ' + p[4], function(err, stdout, stderr) {
                if(err) {
                  return console.log('释放指定端口失败!!')
                }
                console.log('占用指定端口的程序被成功杀掉!')
              })
            }
          }
        }
      })
    })
  }
})

最后的解决方法:使用node-pty以及xterm.js相结合
node-pty的GitHub地址
xterm.js
xterm.js文档
node-pty的一个简单例子

var os = require('os');
var pty = require('node-pty');

var shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';

var ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-color',
  cols: 80,
  rows: 30,
  cwd: process.env.HOME,
  env: process.env
});

ptyProcess.on('data', function(data) {
  console.log(data);
});

ptyProcess.write('ls\r');
ptyProcess.resize(100, 40);
ptyProcess.write('ls\r');

下面是xterm.js的一个简单例子:

<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
      <script src="node_modules/xterm/dist/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        var term = new Terminal();
        term.open(document.getElementById('terminal'));
        term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
      </script>
    </body>
  </html>

上面的例子可以看出,我们可以通过node-pty执行命令相关操作,然后通过xterm.js显示界面,下面是一个两者结合使用的例子
demo地址

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>node-pty Electron example</title>
    <link rel="Stylesheet" href="./node_modules/xterm/lib/xterm.css">
  </head>
  <body>
    <div id="xterm"></div>
  </body>
  <script>
    require('./renderer.js')
  </script>
</html>
var os = require('os');
var pty = require('node-pty');
var Terminal = require('xterm');

// Initialize node-pty with an appropriate shell
const shell = process.env[os.platform() === 'win32' ? 'COMSPEC' : 'SHELL'];
const ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-color',
  cols: 80,
  rows: 30,
  cwd: process.cwd(),
  env: process.env
});

// Initialize xterm.js and attach it to the DOM
const xterm = new Terminal();
xterm.open(document.getElementById('xterm'));

// Setup communication between xterm.js and node-pty
xterm.on('data', (data) => {
  ptyProcess.write(data);
});
ptyProcess.on('data', function (data) {
  xterm.write(data);
});

项目中的实现基本借鉴于这个demo

重点5:命令行的了解
因为涉及到命令行的相关操作,而自己这方面不是很熟悉,理解一些问题或者遇到一些困难的时候找不到突破口,这其实也谈不上重点,只是应该引起自己足够的重视吧。
比如,不同操作系统之间的一些命令行的区别等等。

重点6:工具类的封装
场景:在项目中多处使用到了提醒和存储功能
目的:为了便于管理和方便使用
(其实也是参考了文档系统的相关配置)

utils文件夹

localStorage的封装

/**
 * @file: 实现本地储存功能
 * @description: 封装本地储存功能
 * @author: fenggp1
 * @copyright: COPYRIGHT (©) 2016-2017 深圳美云智数科技有限公司
 * @Created Time: 2017-12-28 10:18:29
 * @Last Modified By: fenggp1
 * @Last Modified Time: 2017-12-28 10:18:29
 */

class WebStoarge {
  constructor () {
    if (!window.localStorage) {
      console.warn('该浏览器不支持window.localStorage')
    }
    this.storage = {}
  }

  injection (name) {
    if (name && typeof name === 'string') {
      this.storage[name] = 'BUILDER-' + name.toUpperCase()
    }
  }

  get (name) {
    if (name in this.storage) {
      return window.localStorage.getItem(this.storage[name])
    }
    return ''
  }

  set (name, value) {
    if (name in this.storage) {
      window.localStorage.setItem(this.storage[name], value)
    }
  }

  remove (name) {
    if (name in this.storage) {
      window.localStorage.removeItem(this.storage[name])
    }
  }

  clear () {
    window.localStorage.clear()
  }
}

export default new WebStoarge()

提示tip的封装:

/**
 * @file: 实现提示的功能
 * @description: 使用element-UI的提示,可以根据不同的类型和提示语进行提示
 * @author: fenggp1
 * @copyright: COPYRIGHT (©) 2016-2017 深圳美云智数科技有限公司
 * @Created Time: 2017-12-28 10:13:10
 * @Last Modified By: fenggp1
 * @Last Modified Time: 2017-12-28 10:13:10
 */

 /**
  * @description 提示语工具函数
  * 
  * @param {string} type 
  * @param {string} msg 
  */
 function tip(type, msg) {
   this.$message({
     type: type,
     message: msg
   })
 }

 export default tip

然后在main.js中

Vue.prototype.$tip = tip
Vue.prototype.$storage = webStorage

这样就可以在项目中愉快的使用了

this.$tip('success', result)

疑问3:localStorage的代替方案????

重点7:Promise的使用以及对象的深浅复制

其他方面

上一篇 下一篇

猜你喜欢

热点阅读