在创建多屏应用程序或在多个窗口内并行运行的应用程序时,当所有连接的参与者共享相同数据时,可以节省大量流量。
剧透:我们没有使用 LocalStorage。
1. 简介
我们的演示应用程序非常简洁。它包含一个带有 3 个部件(表格、饼图和条形图)的仪表盘,我们可以将它们分离到单独的浏览器窗口中。
我们使用一个数据 SharedWorker 只为所有连接的窗口加载一次数据,并且我们有一个 "流模式",在该模式下我们每秒拉取 60x 的新数据。
我们还使用了应用程序 SharedWorker,它使我们能够在更换窗口时重复使用相同的组件实例。这将保持状态同步。
一个浏览器窗口内的所有部件
将仪表板部件移到自己的窗口中
2.演示视频
3. 仓库
你可以在这里找到资源库:
演示代码采用 MIT 许可,因此欢迎你随意使用和扩展。自述文件包含在本地运行应用程序所需的步骤。
如果你希望看到部署版本以直接在线测试,请提前通知我们!
4.后台
我们希望后台代码尽可能简单。
import Neo from 'neo.mjs/src/Neo.mjs';
import * as core from 'neo.mjs/src/core/_export.mjs';
import express from 'express';
import Instance from 'neo.mjs/src/manager/Instance.mjs';
import ColorService from './ColorService.mjs';
import { WebSocketServer } from 'ws';
const app = express(),
wsServer = new WebSocketServer({ noServer: true });
wsServer.on('connection', socket => {
socket.on('message', message => {
let parsedMessage = JSON.parse(message),
data = parsedMessage.data,
service = Neo.ns(`Colors.backend.${data.service}`),
replyData = service[data.method](...data.params),
reply;
if (parsedMessage.mId) {
reply = {
mId : parsedMessage.mId,
data: replyData
}
} else {
reply = replyData
}
socket.send(JSON.stringify(reply))
})
});
const server = app.listen(3001);
server.on('upgrade', (request, socket, head) => {
wsServer.handleUpgrade(request, socket, head, socket => {
wsServer.emit('connection', socket, request)
})
})
我们使用 express
和 ws
来创建一个简约的服务器,它可以接收 3001 端口的套接字连接。我们还将 neo.mjs
核心导入到我们的 nodejs 后端代码中,以便使用我们的内部实用功能和类配置系统。
import Base from 'neo.mjs/src/core/Base.mjs';
/**
* @class Colors.backend.ColorService
* @extends Neo.core.Base
* @singleton
*/
class ColorService extends Base {
static config = {
/**
* @member {String} className='Colors.backend.ColorService'
* @protected
*/
className: 'Colors.backend.ColorService',
/**
* @member {Boolean} singleton=true
* @protected
*/
singleton: true
}
// ...
/**
* @param {Object} opts
* @param {Number} opts.amountColors
* @param {Number} opts.amountColumns
* @param {Number} opts.amountRows
* @returns {Object}
*/
read(opts) {
let data = this.generateData(opts);
return {
success: true,
data: {
summaryData: this.generateSummaryData(data, opts),
tableData : data
}
}
}
}
let instance = Neo.setupClass(ColorService);
export default instance;
完整代码在此:ColorService.mjs。
我们的服务将只公开 read()
方法,该方法将根据我们作为 opts 对象内部属性传递的 3 个参数为我们创建随机数据:
- 金额颜色
- 金额列
- 行数
5.RPC 定义文件
{
"namespace": "Colors.backend",
"type" : "websocket",
"url" : "ws://localhost:3001",
"services": {
"ColorService": {
"methods": {
"read": {"params": [{"type": "Object"}]}
}
}
}
}
在现实世界的应用程序中,你将在每个服务内进行 CRUD,并根据复杂程度,动态创建包含可用服务的 JSON 文件。对于特定的用户角色或权限,JSON 文件可能会有所不同。
我们的前端代码将fetch()
该文件,并向我们公开所需的命名空间。
let me = this,
model = me.getModel();
Colors.backend.ColorService.read({
amountColors : model.getData('amountColors'),
amountColumns: model.getData('amountColumns'),
amountRows : model.getData('amountRows')
}).then(response => {
let {data} = response;
me.updateTable(data.tableData);
me.updateCharts(data.summaryData)
})
当然,如果你愿意,也可以使用 async / await
。
这样做的好处在于,你可以在前端代码库中直接将服务方法用作 JavaScript 承诺。
这就是 Promise
的地狱:)
如果我们在应用程序 SharedWorker
(前端代码)中使用 Colors.backend.ColorService.read()
,neo.mjs
将向数据 SharedWorker
发送一条 post 消息。关注点分离。数据 Worker 将检查我们的套接字连接是否已经存在,并在需要时(重新)连接。然后,它将通过套接字发送我们的消息,并在我们的后端代码中执行 ColorService.read()
。特定套接字连接消息的响应将被发回给我们的 App-Worker。在映射到初始调用者后,它将解析我们的 Promise
。
作为开发人员,你无需担心这里发生的神奇事情。
6.前台
我们使用 npx neo-app
自动生成了应用程序外壳。
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>Colors</title>
</head>
<body>
<script src="../../src/MicroLoader.mjs" type="module"></script>
</body>
</html>
neo 应用程序的索引文件将只包含 MicroLoader 模块,它会获取我们应用程序文件夹中的 neo-config.json
文件,并启动 neo 主线程。它将为我们创建 Worker 设置,并在完成后动态加载应用程序文件夹中的 app.mjs
文件。
import Viewport from './view/Viewport.mjs';
export const onStart = () => Neo.app({
mainView: Viewport,
name : 'Colors'
});
我们的视口文件将加载到应用程序的SharedWorker中:
import BaseViewport from '../../../node_modules/neo.mjs/src/container/Viewport.mjs';
import BarChartComponent from './BarChartComponent.mjs';
import HeaderToolbar from './HeaderToolbar.mjs';
import PieChartComponent from './PieChartComponent.mjs';
import TableContainer from './TableContainer.mjs';
import ViewportController from './ViewportController.mjs';
import ViewportModel from './ViewportModel.mjs';
/**
* @class Colors.view.Viewport
* @extends Neo.container.Viewport
*/
class Viewport extends BaseViewport {
static config = {
/**
* @member {String} className='Colors.view.Viewport'
* @protected
*/
className: 'Colors.view.Viewport',
/**
* @member {String[]} cls=['colors-viewport']
*/
cls: ['colors-viewport'],
/**
* @member {Neo.controller.Component} controller=ViewportController
*/
controller: ViewportController,
/**
* @member {Object} layout
*/
layout: {ntype: 'vbox', align: 'stretch'},
/**
* @member {Object[]} items
*/
items: [{
module: HeaderToolbar,
flex : 'none'
}, {
module : TableContainer,
reference: 'table'
}, {
module : PieChartComponent,
reference: 'pie-chart'
}, {
module : BarChartComponent,
reference: 'bar-chart'
}],
/**
* @member {Neo.model.Component} model=ViewportModel
*/
model: ViewportModel
}
}
Neo.setupClass(Viewport);
export default Viewport;
我们将把 3 个部件和一个 HeaderToolbar 直接放入 Viewport 的 items 数组中。我们还导入了视图控制器和视图模型。
import Component from '../../../node_modules/neo.mjs/src/model/Component.mjs';
import ColorsStore from '../store/Colors.mjs';
/**
* @class Colors.view.ViewportModel
* @extends Neo.model.Component
*/
class ViewportModel extends Component {
static config = {
/**
* @member {String} className='Colors.view.ViewportModel'
* @protected
*/
className: 'Colors.view.ViewportModel',
/**
* @member {Object} data
*/
data: {
/**
* @member {Number} data.amountColors=10
*/
amountColors: 10,
/**
* @member {Number} data.amountColumns=10
*/
amountColumns: 10,
/**
* @member {Number} data.amountRows=10
*/
amountRows: 10,
/**
* @member {Boolean} data.isUpdating=false
*/
isUpdating: false,
/**
* @member {Boolean} data.openWidgetsAsPopups=true
*/
openWidgetsAsPopups: true
},
/**
* @member {Object} stores
*/
stores: {
colors: {
module: ColorsStore
}
}
}
}
Neo.setupClass(ViewportModel);
export default ViewportModel;
在 neo 中,视图模型是一个状态提供器(或在其他库/框架中称为 "存储"),它允许子视图绑定到数据属性。我们还可以绑定父链中不同视图模型的多个数据属性。本演示应用程序的重要部分:我们的状态树可以跨浏览器窗口运行。
7.子应用程序的外壳
与主应用程序一样,我们有一个包含 MicroLoader 的索引文件和一个导入 Viewport 的 app.mjs 文件:
import BaseViewport from '../../../../../node_modules/neo.mjs/src/container/Viewport.mjs';
/**
* @class Widget.view.Viewport
* @extends Neo.container.Viewport
*/
class Viewport extends BaseViewport {
static config = {
/**
* @member {String} className='Widget.view.Viewport'
* @protected
*/
className: 'Widget.view.Viewport'
}
}
Neo.setupClass(Viewport);
export default Viewport;
视口是完全空的,因此如果我们在没有加载主应用程序的情况下打开应用程序,它看起来会是这样:
空视口 div
8.在浏览器窗口中移动小工具
import Component from '../../../node_modules/neo.mjs/src/controller/Component.mjs';
/**
* @class Colors.view.ViewportController
* @extends Neo.controller.Component
*/
class ViewportController extends Component {
static config = {
/**
* @member {String} className='Colors.view.ViewportController'
* @protected
*/
className: 'Colors.view.ViewportController'
}
// ...
/**
* @param {Object} data
* @param {String} data.appName
* @param {Number} data.windowId
*/
async onAppConnect(data) {
if (data.appName !== 'Colors') {
let me = this,
app = Neo.apps[data.appName],
mainView = app.mainView,
{windowId} = data,
url = await Neo.Main.getByPath({path: 'document.URL', windowId}),
widgetName = new URL(url).searchParams.get('name'),
widget = me.getReference(widgetName),
widgetParent = widget.up();
me.connectedApps.push(widgetName);
me.getReference(`detach-${widgetName}-button`).disabled = true;
widgetParent.remove(widget, false);
mainView.add(widget)
}
}
/**
*
*/
onConstructed() {
super.onConstructed();
let me = this;
Neo.currentWorker.on({
connect : me.onAppConnect,
disconnect: me.onAppDisconnect,
scope : me
})
}
// ...
}
Neo.setupClass(ViewportController);
export default ViewportController;
在主应用程序 ViewportController 中,我们正在订阅 connect
事件,该事件会在每个连接到应用程序 SharedWorker 的浏览器窗口中触发。
如果连接的窗口不是我们的主应用程序窗口,我们将调用:
widgetParent.remove(widget, false);
我们将从主窗口内的视口中移除所需的部件(表格、饼图或条形图)。第二个参数 false
是一个不销毁组件实例的标志。
mainView.add(widget);
然后,我们将部件添加到主视图 → 不同浏览器窗口的视口中。由于我们重复使用的是同一个组件实例,因此我们可以获得最新的状态。
就是这么简单。
重用组件实例这一主题值得我们专门撰写一篇博文,因为它是降低内存泄漏和运行时性能的一项非常强大的技术。
9.使用 Chrome 开发工具检查我们的应用程序
在 neo 中创建单窗口应用程序时,框架会使用 Dedicated Workers,我们可以直接在主窗口控制台中对其进行检查:
专职 Worker 设置
一旦我们在 "useSharedWorkers": true
文件中写入 neo-config.json
后,框架就会切换到 SharedWorkers,而我们在浏览器窗口控制台中再也看不到 SharedWorkers:
在主窗口控制台中看不到 SharedWorker
Neo.mjs 为 Worker 提供了一个抽象层,因此开发人员使用的 API 完全不变。我们可以在任何时候切换单行配置。要检查 SharedWorkers,你需要打开:
chrome://inspect/#workers
检查应用程序 SharedWorker,并在控制台输入以下代码:
Neo.Main.windowOpen({url:’http://localhost:8080/apps/colors/childapps/widget/index.html?name=bar-chart', windowName:’_blank’})
10.能否进一步提高性能?
虽然演示应用程序感觉非常快,但它尚未进行性能优化。因此,这个问题的答案显然是 "是的!"。我们仍需将 AmCharts 主线程插件升级(重写)到第 5 版。请给我们提个醒,以防这将成为优先事项。
我们还可以使用不同的应用程序设置。例如,直接在应用程序 Worker 内而不是在数据 Worker 内创建 Socket 连接,以减少内部发布消息的数量。
11.更新 neo.mjs 项目
这是我1年半来的第一篇博文。希望你喜欢,也欢迎你提出问题。
在此期间,框架本身也取得了很大进展。最重要的更新是,我们正在制作一个新的产品网站,其中将包括逾期已久的自学学习部分的第一个版本。粗略估计,完成这项工作还需要一个月左右的时间。
我们仍在每周四(中欧标准时间)17:30 举办免费研讨会。如果你想加入我们,请在 Slack 频道内与我们联系。
如果贵公司需要更多帮助,neo.mjs 核心团队现在提供专业讲师指导培训(40 小时,6-12 人参加)和专业服务(例如,帮助你创建下一代应用程序)。
欢迎反馈意见!
致以最诚挚的问候,祝你编码愉快、
托比亚斯