网易卡搭校园在桌面客户端上的探索

阿凡达2018-07-11 17:30
1 序
这篇文章主要整理网易卡搭编程项目组在使用electron框架实现校园客户端、接入scratch工具过程中积累的经验与总结,内容主要包含:客户端需求背景介绍、electron初体验、scratch工具接入、electron与scratch交互、心得体会。

2 需求背景
2.1 自我介绍
首先做个自我介绍,什么是卡搭?
卡搭是网易旗下的创意编程社区,在卡搭,通过拖拽积木块就能设计程序,无需学习编程语法。
我们希望所有使用卡搭的朋友,能够轻松愉快地学习程序设计,学习数学及计算机知识,锻炼创造性思维和逻辑思维,培养团队协作能力等。
那卡搭云教室又是什么?
网易卡搭云教室是为学校、校外机构研发的少儿编程教学管理云平台,专注提供少儿编程教育解决方案, 拥有Scratch在线创作、校园班级创建、学生作品管理、课程资源共享、赛事活动组织等功能,打造高效、有趣的创意编程互动课堂。
本文的主角,卡搭校园客户端就是由卡搭云教室提供,专注于提升课堂效率的一款桌面客户端应用,在卡搭校园官网可以直接下载。
2.2 为什么我们会想要做一个客户端?
首先,我们设想一下这样的场景:
我是一名信息学教师,正在学校带着全班四五十个小宝贝上信息课,这堂课的内容是让孩子用scratch编程做一个自我介绍,传统的创作流程可以分为三步:
  1. 打开浏览器
  2. 输入地址:school.kada.163.com
  3. 找到创作入口进入创作
很简单对吗?但你万万不会想到,对小朋友们来说,这基本操作经常会出现各种各样的问题,要解决这些问题无疑增加了老师的教学成本,降低上课效率,
但有了客户端之后就不一样了,只需要轻轻双击,就可以马上使用到专门为学生端设计的各种功能,缩短了学生操作路径,教学成本也就降低了。
其次,使用过scratch编程的同学都知道,它所包含的角色库,背景库,音乐库等一系列资源,如下图所示,在web端访问时,需要占用较大的带宽,而学校的网速通常不容乐观,使用起来体验就比较差了。
但使用校园客户端之后,这些资源都可以在本地获取到,访问速度将有质的飞跃,单车瞬间变摩托。
另外,校园客户端在产品布局上也非常重要,无疑将成为我们提升品牌竞争力的大杀器。

3 electron初体验
3.1 简介
这里只介绍electron两个最核心的概念,主进程(main process)和渲染进程(renderer process)
主进程 main process
主进程,指 main.js 文件,它是Electron应用的入口文件,集成了NodeAPI,控制着整个 App 的生命周期, 管理着原生元素比如菜单,菜单栏,Dock栏,托盘等,负责创建 APP 的每个渲染进程。每个Electron应用有且只有一个主进程。
渲染进程 renderer process
渲染进程,是应用内的一个浏览器窗口,能够同时存在多个。
在校园客户端主进程中,主要进行了创建窗口、创建托盘、检查版本自动升级、设置应用程序插件、通用消息处理等操作。
3.2 创建一个窗口
在主进程中,可以通过 browserWindow 来创建和控制浏览器窗口。
                    
我们看到了默认样式的菜单栏,而视觉稿中想要的是下面的效果,那么要如何来定制呢?
3.3 自定义菜单栏
首先,我们对定制菜单栏的任务进行拆解,要将菜单栏替换,可以拆分为三个子任务
  1. 隐藏系统菜单栏
  2. 添加一个菜单栏组件在页面上
  3. 为菜单栏按钮添加事件(最小化、最大化、关闭)
  1. 第一步,隐藏系统菜单栏,通过查询 browswerWindow 的选项,我们可以传入 frame: false 来隐藏系统菜单栏
	
        // main.js
	win = new BrowserWindow({
		frame: false
	})
 
隐藏之后,打开应用程序就只有页面内容了
                          
第二步,在页面上引入做好的菜单栏组件,
                           
第三步,为按钮添加事件,我们知道,在一个普通的页面上,并没有直接控制窗口大小的操作,但是在主进程里,提供了像minimize、maximize、show、hide这样的方法,要完成这个功能,得想办法告诉主进程,让主进程来对窗口进行控制,这里就需要用到进程间通信了。
3.4 处理进程间通信
通过ipc(Inter-Process Communication 进程间通信)模块可以在主进程和渲染进程之间发送消息
渲染进程:
	 
	// app.js 直接引入electron会执行其中的index.js,该文件会访问fs,而渲染进程无法直接访问到node原生的模块导致报错
        if (window.require) {
            var ipcRenderer = window.require('electron').ipcRenderer;
        }
	clickBroswerBtn(ev) {
            let scratch = swfobject.getObjectById('scratch');
            if (ipcRenderer) {
                ipcRenderer.send('broswerExtension', ev);
            }
        }
        
主进程:
        // main.js 
	ipcMain.on('broswerExtension', (ev, arg) => {
	    switch(arg) {
	        case 'min':
	            win.minimize();
	            break;
	        case 'max':
	            if (win.isMaximized()) {
	                win.unmaximize();
	            } else {
	                win.maximize();
	            }
	            break;
	        case 'close':
	            win.hide();
	            break;
	    }
	});
 
在我们的场景里,可以用这样一副流程图来表示
                       
3.5 处理应用程序单开
还遇到这样一个问题,当我没做任何处理的情况下,重复打开应用,会启动多个卡搭客户端,而一般的应用程序做法是将之前的应用唤起,如何解决这个问题呢?
electron提供了一个方法:app.makeSingleInstance(callback)
该方法返回一个布尔值,可以检测当前实例是否是该应用程序的第一个实例;当第二个实例被打开时,会执行第一个实例中的回调方法,为了达到这个效果,我们在初始化应用程序之前,可以做一次判断。
              
3.6 自定义协议唤起
有这样一个需求场景,学生在web端创作,可以手动点击按钮唤起本地桌面客户端,electron也提供了这样的能力,通过setAsDefaultProtocolClient方法,
app.setAsDefaultProtocolClient(protocol[, path, args])
• protocol String - 协议的名称, 不包含 ://。 如果您希望应用程序处理 electron:// 的链接, 请将 electron 作为该方法的参数.
• pathString (可选) Windows -默认为 process.execPath
• args String Windows - 默认为空数组
返回 Boolean-是否成功调用。
该方法将自定义协议写入注册表中,完成之后就可以在浏览器上将客户端唤起。
3.7 打包
打包工具我们使用的是 electron-builder。
 
	"build": {
	    "appId": "com.netease.kada.schoolmacos-Dev",
	    "productName": "kada-campus",
	    "compression": "maximum",
	    "files": [
	      "pub"
	    ],
	    "win": {
	      "icon": "./pub/renderer/static/images/app-icon/logo.ico"
	    },
	    "mac": {
	      "category": "your.app.category.type",
	      "target": [
	        "dmg"
	      ]
	    }
	  }
	   
相关的配置都在package.json中的build属性里,
  • asar:这个字段默认值是true,它是一种将多个文件合并成一个文件的类 tar 风格的归档格式,electron无需解压就可以从其中读取任意文件内容,这个参数在做打包优化的时候很有用,我们可以将其设置为false,查看目录结构,以帮助我们删掉无用的文件依赖。
  • compression:压缩的形式,electron-builder提供了store,normal以及maximum三种形式给我们选择,其中,store打包时间最快,适用于测试,而maximum虽然打包速度慢,但是打包出来的体积会比较小。
  • files:明确需要打包的文件目录,在打包时需要注意去掉无用的依赖

4 接入Scratch
4.1 Scratch介绍
首先介绍一下,Scratch是一款由麻省理工学院(MIT)设计开发的少儿编程工具,使用者可以不认识英文单词,也可以不会使用键盘。构成程序的命令和参数通过积木形状的模块来实现,用鼠标拖动模块到程序编辑栏就可以了。
通过Scratch,可以完成各种各样的作品,下图是一位小朋友自我介绍的展示。
下面这些是社区上的精选,都非常棒,非常建议大家来体验体验。
4.2 在页面上接入
从Scratch官网下载下来的离线编辑器是一个桌面应用程序,无法直接接入,我们把Scratch工程克隆到本地。它是一个flash工程,需要配置flash编译环境,
配置好之后进行编译,编译成功会生成一个swf文件,通过swfobject将该文件引入至页面并启动,这时又发现了一个现象,在electron中并不能正确展示Scratch,但在web端却能够正常看到,究其原因,是因为浏览器环境自带flash,electron环境需要额外引入。
4.3 electron引入flash插件
因为浏览器环境自带flash,而electron环境本身没有集成flash插件,需要在electron中运行flash,需要额外引入,
  
	function setPlugin(appRoot) {
	    // 依赖flash插件
	    let pluginName;
	    switch (process.platform) {
	        case 'win32':
	            pluginName = 'pepflashplayer.dll';
	            break;
	        case 'darwin':
	            pluginName = 'PepperFlashPlayer.plugin';
	            break;
	        case 'linux':
	            pluginName = 'libpepflashplayer.so';
	            break;
	    }
	    // 添加flash插件到进程中
	    app.commandLine.appendSwitch('ppapi-flash-path', path.join(appRoot, 'pub/renderer/static/scratch/plugin', pluginName));
	}
	 
不同的系统,windows、osx、linux需要引入不同的flash插件,这些插件可以在官网上下载。
主进程通过 app.commandLine.appendSwitch('ppapi-flash-path', filePath) 将插件集成至electron,这样一来,Scratch在electron环境下就能够正常运行了。
4.4 Scratch与页面交互
ExternalInterface类是用来支持ActionScript和SWF容器之间进行直接通信的应用程序编程接口,下面两个需求场景分别会使用其中的addCallback和call方法。
4.4.1 页面调用Scratch内部方法
场景:点击下载按钮,调用Scratch内部的方法,获取作品数据
方案:flash将内部方法暴露给外部(基于ExternalInterface.addCallback),外部去调用
 
	// scratch.as
	protected function setupExternalInterface(oldWebsitePlayer:Boolean):void {
			addExternalCallback('ASgetSaveNeeded', getSaveNeeded);
			addExternalCallback('ASsetSaveNeeded', setSaveNeeded);
			addExternalCallback('ASclearSaveNeeded', clearSaveNeeded);
			addExternalCallback('ASexportProjectToFile', exportProjectToFile);
			addExternalCallback('ASsaveProjectToLocalConfirm', saveProjectToLocalConfirm);
			addExternalCallback('ASfocusProjectName', stagePart.focusProjectName);
			addExternalCallback('ASGetProjectData', getProjectData);
		}

// app.vueswfobject.getObjectById('scratch').ASGetProjectData();


4.4.2 Scratch调用页面方法
场景:页面打点
方案:调用页面上的trackEvent方法(基于ExternalInterface.call)
  
        // scratch.as
        externalCall('trackEvent', null, desc);
 
4.5 如何对Scratch中的英文文案进行汉化
Scratch内部存在一个翻译器模块,该模块的作用是将内部的英文文案替换为其他语言。
在Scratch官网上可以找到中文简体的汉化包 zh-cn.po,下载并导入,系统会从中匹配关键词来进行文案的替换,假如有文案需要修改完善,修改 zh-cn.po 就可以了。
 
        msgid "Upload from your computer"
        msgstr "从您的计算机中上传"

	msgid "Upload sound from file"
	msgstr "从本地文件中上传声音"
 

5 其他
5.1 windows下打开程序命令行闪动
相信很多人在使用windows的时候,都遇到过这样的情况,打开应用程序会看到黑色的命令行窗口闪现,这个问题对于强迫症来说会比较困扰,深入研究之后找到了问题根源所在:flash进程试图在控制台输出"NOT SANDBOXED”,但是我们的程序并没有控制台,所以它自己创建了一个。
这个问题在electron issue#1779里找到了解决方案,通过编写c++程序编译出新的命令行程序,替换 process.env.COMSPEC,该程序会自动屏蔽NOT SANDBOXED,问题得以解决。
5.2 关于客户端埋点
既然是桌面客户端,总有一些和web端不一样的,比如离线与在线,安装与退出。除了web端的ga打点,同时还需要对安装量和客户端崩溃进行埋点。electron支持客户端在意外退出的时候,在本地生成报告。
关于客户端埋点,我们是这样设计的:客户端未联网的情况下,统计数据都保存到本地,有网络了之后,去本地读取数据并发送至服务端,下面是该设计的时序图。

6 心得与体会
在线下教学这个场景下,我认为桌面客户端是非常有必要的,它使学生提交作业的操作路径变短了,老师的上课效率提高了,更关键的是,优化了由于学校网络差导致资源加载速度慢的问题。
从产品布局来看,这对发展线下课堂非常关键,它有利于产品多元化的发展,提升网易卡搭的品牌竞争力。
从技术角度来看,这一段时间开发下来,确实和之前常规的Web端开发不太一样,它包含了web端的功能,并且内容更多,过程中遇到了许多困难与挑战,好在我们解决了大部分难题,但仍有不足,还需要不断优化来把它做的更好,这对技术能力的提升非常有帮助。
无论是对于线下教学的场景,网易卡搭产品本身,还是对技术经验的积累,这些都是非常有价值有意义的探索。

本文来自网易实践者社区,经作者王歆宇授权发布。