
公司去年对 CDN 资源服务器进行了迁移,由原来的通过 FTP 方式的文件存储改为了使用 S3 协议上传的对象存储,部门内 @柴俊堃 同学开发了一个命令行脚本工具 RapidTrans(睿传),使用睿传可以很方便将本地目录下的资源上传到 S3 中。
睿传运行时接收两个主要参数,一个为待上传的本地路径,一个为上传到 CDN 后的路径,我们可以在项目的 package.json 中去配置 scripts执行上传。
npm run rapid-trans -- -s "/home/demo/work/mall2016/release/列表页" -p "2016/m/list"
用了一段时间后觉得如果选择本地路径的时候可以通过可视化的文件选择器的方式选择就太好了,团队一直在做客户端方向技术的储备,所以为了更方便团队的使用产生了将睿传封装成 GUI 的跨平台客户端的想法。

桌面客户端,支持 Windows、Mac 系统
本地路径可以通过文件对话框或拖拽的方式进行选择
CDN 路径可以通过输入框的方式输入
上传成功后将当前选择的本地路径和 CDN 的映射关系存储,下次再选择到当前目录的话直接使用之前 CDN 的路径地址,无需再次输入
S3 参数配置化
自动升级
覆盖上传
Electron
Vue
LowDB
Electron 是由 Github 开发,基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建跨平台桌面应用的开源框架。

Electron 可以让你使用纯 JavaScript 调用丰富的原生(操作系统) APIs 来创造桌面应用。 你可以把它看作一个专注于桌面应用的 Node. js 的变体,而不是 Web 服务器。
简单点说,用 Electron 可以让我们在网页中使用 Node.js 的 API 和调用系统 API。
使用 vue-cli 脚手架和 electron-vue模板进行搭建,此处需要注意,由于 electron-vue 模板不支持 vue-cli@3.0,所以要使用 2.0 版本。
# 安装 vue-cli@2.0,若已安装则无需重复安装
npm install -g vue-cli
vue init simulatedgreg/electron-vue s3_upload_tool
# 安装依赖并运行
cd s3_upload_tool
npm install
npm run dev
├─ .electron-vue
│ ├─ webpack.main.config.js
│ ├─ webpack.renderer.config.js
│ └─ webpack.web.config.js
├─ build
│ └─ icons/
├─ dist
│ ├─ electron/
│ └─ web/
├─ node_modules/
├─ src
│ ├─ main
│ │ ├─ index.dev.js
│ │ └─ index.js
│ ├─ renderer
│ │ ├─ components/
│ │ ├─ router/
│ │ ├─ store/
│ │ ├─ App.vue
│ │ └─ main.js
│ └─ index.ejs
├─ static/
├─ .babelrc
├─ .eslintignore
├─ .eslintrc.js
├─ .gitignore
├─ package.json
└─ README.md
应用的目录结构和平常我们用 Vue 做 WEB 端时生成的结构基本差异不大,所以本文我只介绍下与 Web 不同的几个目录。
.electron-vue该目录下包含 3 个独立的 Webpack 配置文件
.electron-vue/webpack.main.config.js 针对于 Electron 的 main 主进程打包的配置,配置比较简单,主要就是将 src/main/index.js 通过 babel-loader 打包,并且生成 commonjs2模块规范。
.electron-vue/webpack.renderer.config.js 针对于 Electron 的 renderer 渲染进程打包的配置,此配置主要用来打包 Vue 的应用程序,这个配置就和平常我们做 Web 端时 Webpack 的配置基本一样,处理 Vue、Sass、Image、Html等。
.electron-vue/webpack.web.config.js 为浏览器构建 render渲染进程的配置,主要针对于发布到 Web 的情况。
src/main主进程代码存放位置,涉及到调取 Node API 、调用原生系统功能的代码。
src/renderer渲染进程代码存放位置,和平常的 Vue 项目基本一样。
在 Electron 中有两个进程,分别为主进程和渲染进程,主进程负责 GUI 部分,渲染进程负责页面的展示。
主进程通常是在 package.json 的 main字段的脚本进程。
一个 Electron 应用只有一个主进程。
主进程一般用来处理 App 生命周期、系统事件的处理、系统原生GUI。
main.js
const { app, BrowserWindow } = require('electron')
function createWindow () {
// 创建浏览器窗口
let win = new BrowserWindow({ width: 800, height: 600 })
// 然后加载 app 的 index.html.
win.loadFile('index.html')
}
app.on('ready', createWindow)
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
主进程使用 BrowserWindow 实例创建页面。 每个BrowserWindow实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
进程间通信(IPC,Interprocess communication)是一组编程接口,让开发者能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。
Electron 使用 IPC 的机制,由主进程来创建应用,渲染进程来负责绘制页面,而两个进程之间是无法直接通信的。

渲染进程通过ipcRenderer向主进程发送消息,主进程通过 ipcMain监听事件,当事件响应时对消息进行处理。
主进程监听事件的回调函数中会存在 event 对象及arg 对象。arg 对象为渲染进程传递过来的参数。
如果主进程执行的是同步方法,回复同步信息时,需要设置event.returnValue,如果执行的是异步方法回复时需要使用 event.sender.send向渲染进程发送消息。
下面代码为渲染进程主动向主进程发送消息,在主进程接收后回复渲染进程。
// 主进程
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
// 渲染器进程
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
有时候我们也需要由主进程主动向渲染进程发送消息,面对这种情况我们可以在主进程中通过 BrowserWindow对象的 webContets.send方法向渲染进程发送消息。
// 主进程
const { app, BrowserWindow } = require('electron')
function createWindow () {
let win = new BrowserWindow({ width: 800, height: 600 })
win.loadFile('index.html')
// 向渲染进程发送消息
win.webContents.send('main-process-message', 'ping')
}
app.on('ready', createWindow)
// 渲染器进程
const { ipcRenderer } = require('electron')
// 监听主进程发送的消息
ipcRenderer.on('main-process-message', (event, arg) => {
console.log(arg) // prints "ping"
})
在桌面端应用中一些用户设置通常需要进行存持久化存储,方便以后使用的时候获取。 我们做 Web 时候通常是使用像 MySQL、Mongodb等数据库进行持久化存储, 但是当用户安装桌面软件时候不可能让用户在本地安装这类数据库,所以我们需要一个轻量级的本地化数据库。
lowdb 是一个基于 Lodash API 的轻量级本地 JSON 数据库,支持 Node.js、browser、Electron。
在我们要开发的工具中,用户的 S3 配置,已上传文件的 CDN目录等信息是需要进行持久化存储的,所有我们采用的 lowdb进行数据的存储。

使用也是非常的简单,数据的读写和平常使用 Lodash差不多。
npm install lowdb -save
Electron 提供了获取系统目录的方法,可以很方便的进行一些系统目录的获取。
const { app, remote } = require('electron')
app.getPath('home'); // 获取用户的 home 文件夹(主目录)路径
app.getPath('userData'); // 获取当前用户的应用数据文件夹路径
app.getPath('appData'); // 获取应用程序设置文件的文件夹路径,默认是 appData 文件夹附加应用的名称
app.getPath('temp'); // 获取临时文件夹路径
app.getPath('documents'); // 获取用户文档目录的路径
app.getPath('downloads'); // 获取用户下载目录的路径
app.getPath('music'); // 获取用户音乐目录的路径
app.getPath('pictures'); // 获取用户图片目录的路径
app.getPath('videos'); // 获取用户视频目录的路径
app.getPath('logs'); // 获取应用程序的日志文件夹路径
app.getPath('desktop'); // 获取系统桌面路径
'use strict'
const DataStore = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
const path = require('path')
const fs = require('fs-extra')
const { app, remote } = require('electron')
const APP = process.type === 'renderer' ? remote.app : app
const STORE_PATH = APP.getPath('userData') // 将数据库存放在当前用户的应用数据文件夹
if (process.type !== 'renderer') {
if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
}
const adapter = new FileSync(path.join(STORE_PATH, '/data.json'))
const db = DataStore(adapter)
// 初始化默认数据
db.defaults({
project: [], // 存储已上传项目的 CDN 配置信息
settings: {
ftp: '', // ftp 用户配置
s3: '', // s3 用户配置
}
}).write()
module.exports = db
由于睿传是一个命令行工具,并没有对外提供 Node.js API,所以用户点击上传按钮时候需要通过 Electron在后台运行命令行程序,并且将命令行运行的日志实时渲染到应用的日志界面中,所以在这里利用 Node.js 的 child_process子进程的方式来处理。
'use strict'
import { ipcMain } from 'electron'
import { exec } from 'child_process'
import path from 'path'
import fixPath from 'fix-path'
import { logError, logInfo, logExit } from './log'
const cmdPath = path.resolve(__static, 'lib/rapid_trans') // 睿传路径
let workerProcess
ipcMain.on('upload', (e, {dirPath, cdnPath, isCover}) => {
runUpload(dirPath, cdnPath, isCover)
})
function runUpload (dirPath, cdnPath, isCover) {
let cmdStr = `node src/rapid-trans.js -s "${dirPath}" -p "${cdnPath}" -q`
if (isCover) {
cmdStr += ' -f'
}
fixPath()
logInfo('================== 开始上传 ================== \n')
workerProcess = exec(cmdStr, {
cwd: cmdPath
})
workerProcess.stdout.on('data', function (data) {
logInfo(data)
})
workerProcess.stderr.on('data', function (data) {
logError(data)
})
workerProcess.on('close', function (code) {
logExit(code)
logInfo('================== 上传结束 ================== \n')
})
}
// log.js
'use strict'
const win = global.mainWindow
export function logInfo (msg) {
win.webContents.send('logInfo', msg)
}
export function logError (msg) {
win.webContents.send('logError', msg)
}
export function logExit (msg) {
win.webContents.send('logExit', msg)
}
export default {
logError,
logExit,
logInfo
}
应用开发完成后需要进行打包,我们可以使用 electron-builder 将应用打包成 Windows、Mac 平台的应用。
在执行npm run build之前需要在 package.json进行打包配置的编辑。
{
"build": {
"productName": "S3上传工具", // 应用名称,最终生成的可执行文件的名称
"appId": "com.autohome.s3", // 应用 APP.ID
"directories": {
"output": "build" // 打包后的输出目录
},
"asar": false, // 关闭 asar 格式
"publish": [
{
"provider": "generic", // 服务器提供商
"url": "http://xxx.com:8003/oss" // 更新服务器地址
}
],
"releaseInfo": {
"releaseNotes": "新版更新" // 更新说明
},
"files": [
"dist/electron/**/*",
{
"from": "dist/electron/static/lib/rapid_trans/node_modules",
"to": "dist/electron/static/lib/rapid_trans/node_modules"
} // 将睿传的依赖打包进应用
],
// 平台的一些配置
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
// 应用图标
"mac": {
"icon": "build/icons/icon.icns"
},
"win": {
"icon": "build/icons/icon.ico"
},
"linux": {
"icon": "build/icons"
}
}
}
由于软件不进行 App Store 的上架,只在团队内部使用没有配置证书,不配置证书的话 Mac 中无法进行自动更新安装,所以我们在检测到用户的当前版本不是最新版本的时候是采用的弹层提示的方式让用户自己下载。
使用 electron-updater 打包的应用自动更新非常方便,将打包后 build 目录下的 latest-mac.yml文件上传至package.json 中配置的 publish.url 目录下,并且在主进程文件中监听 update-availabl事件。
// 主进程 main.js
import { autoUpdater } from 'electron-updater'
// 关闭自动下载
autoUpdater.autoDownload = false
// 应用可更新
autoUpdater.on('update-available', (info) => {
// 通知渲染进程应用需要更新
mainWindow.webContents.send('updater', info)
})
app.on('ready', () => {
if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})
// 渲染进程 updater.js
import { ipcRenderer, shell } from 'electron'
import { MessageBox } from 'element-ui'
ipcRenderer.on('updater', (e, info) => {
MessageBox.alert(info.releaseNotes, `请升级${info.version}版本`, {
confirmButtonText: '立即升级',
showClose: false,
closeOnClickModal: false,
dangerouslyUseHTMLString: true,
callback (action) {
if (action === 'confirm') {
// 在用户的默认浏览器中打开存放应用安装包的网络地址
shell.openExternal('http://10.168.0.49/songjinda/s3_tool/download/')
return false
}
}
})
})
作者|宋金达
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub