混合开发本身已经不是一个新概念了,现在也有很多种方案,比如:Hybrid App,用原生和Web/html5来开发应用;Facebook的开源框架 React Native;阿里的开源框架 Weex和最简单的Web App。Web App 是指基于 Web 的应用,运行于网络和标准浏览器上,相当于一个网页然后加一个 App 的壳。本专栏主要讲的就是运用Vue 、 Node.js和Electron来实现Web App。他对前端开发人员是相当友好并且成本相对较低的。
写在前面
当我们在实际工作中有这种需求( 前端工程师开发客户端 )的时候,往往会考虑很多,并且,针对实际项目要求,还要解决一些特定的问题。所以,在一开始动手前,需要把一些东西想清楚。比如:做一个本地工具,就要考虑如何实时展现出日志,那么日志又分服务端日志和客户端日志,需要作不同的处理。如果是Web App的话,那么,很可能你是一个前端开发工程师,那有些地方就需要换一个思维去思考问题,例如:前端与后台通信只能通过发常规的Http请求吗?不是的,前端还可以与客户端通信,以Electron为例,可以通过 IpcRenderer模块实现前端与客户端之间的通信,然后客户端与后台通信,或者直接通过写本地文件进行交互。下面是简单的IpcRenderer实现样例:
/preload.js
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.ipcRenderer = require('electron').ipcRenderer;
将ipcRenderer对象挂载到Window上,在客户端启动的时候通过webPreference配置去将它挂载到Window对象上:
/main.js
// 这里省略了其他参数
let mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
官方的解释是:
在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入node的全局引用标志
设置了这些之后,就可以在前端去调用它,用它来发送/接收消息,实现通信的目的,具体如下:
/app.vue
const ipcRenderer = window.ipcRenderer;
// 发送消息
ipcRenderer.send('message', 'this is a message');
ipcRenderer.on('receive', (message) => {
this.message = message;
// do something
});
/main.js
// 客户端用IpcMain来接收消息
ipcMain.on('message', async (event, msg)=>{
// do something
event.sender.send('receive', msg);
})
整个过程,不需要发起任何Http
请求,就能实现前端与客户端的通信,与常规的前端开发是不一样的。
主要架构
前面有提到,混合开发主要涉及到前端、服务端(后端)和客户端,与传统的前后端的架构有所不同,却又紧密相连,那要实现这样一个东西,它的架构是怎样的呢?别着急,请继续往下看。首先,要考虑的是,它们之间都能够”互相通气“,这里的通气就是指它们能够传输消息。前端与服务端是没有问题的,走Http请求就行了,前端与客户端,前面也讲了可以通过客户端的组件实现消息通信,那服务端与客户端呢?这里其实也是一样,如果是本地服务,可以直接通过写本地文件的形式通信,也可以发起Node.js
的Http
请求,但是很多情况下,客户端与后台服务是很少的。其次,除了这些之外,还有其他相关的地方,比如,退出客户端需要关闭服务端的本地进程,避免内存占用,或者二次启动的时候报端口被占用的错误。最后,通过上面这些简单的分析,我们可以知道一个大概的架构,如下图:
上图中,日志文件是一个根据实际情况调整的选项,如果服务端是在远程服务器上,那么这里的本地文件就通过Http
接口的形式发送。如果是本地的服务端就可以采用上面的方式去写本地文件。
值得注意的是,这里需要客户端去做一个心跳检测的事情,因为,客户端在这里只是一个壳子,有时候因为异常或者其他因素,服务挂了,需要能够自动拉起它,这个在本地客户端的情况尤其重要。
心跳检测
心跳检测,其实,可以在前端实现,也可以在客户端实现 。但是,前端实现依赖客户端,所以这里还是直接讲客户端实现吧。它的主要思路就是,每隔一段时间去探测一下后台服务是否还在活动。在这里,我们就讲一下本地服务的情况,因为它是最容易挂掉的了。
定时器
首先,就是很简单的一个定时器,写在客户端的Main.js
中,它会随着客户端的退出而关闭,并且监听本地服务端的情况,具体如下:
/main.js
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
setInterval(() => {
const live = checkServerStatus(port);
if (!live){
startServer()
}
}, 10000);
})
在app Ready
之后,就可以触发这个定时器了,因为在CreateWindow
的时候已经去加载了界面了,这个时候服务正常情况下应该已经启动了,不然就会报错。在这里,checkServerStatus
方法是如何实现的呢?别着急,请继续往下看。
判断服务是否启动
如何判断本地服务已经启动了,最直接的方法就是去判断端口是否已经被占用,如果正常访问,端口又被占用了,那说明服务是正常的,反之,服务挂了,需要重新启动。具体实现代码如下:
function portUsed(port){ // 判断端口是否被占用
return new Promise((resolve)=>{
let server = net.createServer().listen(port);
server.on('listening',function(){
server.close();
resolve(port);
});
server.on('error',function(err){
if(err.code == 'EADDRINUSE'){
resolve(err);
}
});
});
}
async function checkServerStatus(port) {
let res = await portUsed(port);
if(res instanceof Error){ // 端口已启动,可以创建应用
return true;
} else{ // 启动本地Node.js服务
return false;
}
}
通过创建一个Node.js
本地服务,监听它的error
事件和listening
事件,如果报错了,说明这个端口已经被占用了,不需要重新启动。否则,需要重新启动,并关掉之前启动的服务。这个代码参考了网上的实现,看起来没有毛病,但是实际跑起来会有问题,listening
方法并不靠谱,有时候已经启动了某端口,再次调用这个方法的时候,还是会触发listening
,导致误以为它没有启动,其实是已经启动了,最后报错。
那么,要如何优化它呢,尝试一番后,发现一个比较绕的方法,就是在Windows系统中去查找对应端口的信息,这样就可以直接判断它是否已经在运行了。具体实现如下:
async function portUsed(port) {
// 相当于执行 netstat -aon
const ls = spawnSync('netstat', [`-aon`], {detached: true});
const stdout = ls.stdout.toString();
// const stdout = iconv.decode(Buffer.from(ls.stdout, "binary"), "GBK");
const list = stdout.split('\n');
for ( let processMessage of list) {
let pms = processMessage.trim().split(/\s+/);
if (process.platform === 'win32') {
let address = pms[1];
if (address && address.includes(`:${port}`)) {
console.log('port is used.');
return true;
}
} else { // mac
// todo
}
}
return false;
}
这里只实现了Windows版本的,Mac版本的其实原理与Windows下的是一样的。通过 执行netstat -aon
命令来列出当前系统中的进程列表,然后,查找出与当前端口号匹配的记录。这个方法,比上一个版本实现的方法可靠多了,并且实战中很有效果。
最后,贴一下RunServer
的代码,仅供参考:
const Koa = require('koa');
const convert = require('koa-convert');
const cors = require('koa-cors'); //跨域
const bodyParser = require('koa-bodyparser'); //请求体JSON解析
const onerror = require('koa-onerror'); //错误处理
const resource = require('koa-static');//静态资源托管
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const path = require('path');
const logger = require('../util/log4js');
const routes = require('./routes/index.js');
const config = require('../../config/index.js');
const app = new Koa();
const env = process.NODE_ENV || 'dev';
onerror(app);
app.use(historyApiFallback({ whiteList: ['/api'] }));
app.use(convert(cors()));
app.use(bodyParser());
app.use(resource(path.join(__dirname, '../../public')));
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
logger.info(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// routes
app.use(routes.routes(), routes.allowedMethods());
app.on('error', (error, ctx) => {
logger.info('other error ' + JSON.stringify(ctx.onerror));
logger.info('server error:' + error);
});
app.listen(config[env].port).on('listening', function () {
logger.info('listening port : ' + config[env].port);
});
它其实就是做了一个简单的Node.js服务做的事情,把前端打包成静态放到 public
里去就可以跑起来了。
总结
本文简单阐述了混合开发Web App的架构和它的一些关键点,实际开发中遇到的问题比这个多的多,但是事情总是需要人去做的,没有解决不了的问题。希望看完本文对你有帮助。
共勉。