scratch3.0二次开发心得

经过一周多的努力,我研发了一套基本的scratch3.0创作以及微信分享的在线系统,其效果如下:

 

下面记录一下整个历程和关键知识。

技术栈

前端部分基于scratch3.0的开源代码二次开发,要求对react+redux框架比较熟悉,能够理解项目结构与其组件架构思路。

后端部分利用django快速实现原型,实现包括帐号、作品、微信分享等功能。

资源优化

在性能方面带宽和人民币是主要矛盾,因为阿里云主机最便宜的是1Mbps封顶的带宽,相当于下载速度只能达到100KB/S。

为了克服带宽瓶颈,我的解决方案如下:

  • 前端js/css文件:webpack打包文件极大,核心js文件未压缩20M,压缩后6M,所以需要主动推送阿里云OSS,通过CDN完成内容分发,而不能依靠CDN回源,否则体验极差。
  • scratch作品/截图:包含大量图片和音乐的sb3文件可能达到数MB,因此主动推送阿里云OSS,通过CDN完成内容分发。
  • scratch素材:官方提供的音频、造型图片、背景图片默认都是国外地址,需要通过阿里云OSS回源来自动做一波永久缓存。

为了克服人民币瓶颈,我的解决方案如下:

  • scratch作品/截图:按作品ID(即不按内容hash)推送OSS,即同一个作品只有一份数据,同时CDN针对scratch作品目录设置不缓存策略,确保内容分发时效性。

核心功能

前端是主要工作量,后端简单做数据的读写配合。

总结前端工作大概涉及下面几方面,

1、对scratch3.0进行魔改,支持:

  • 登录
  • 作品上传
  • 作品下载

2、学生的个人主页,支持:

  • 作品列表
  • 微信分享
  • 删除作品

3、微信H5分享页,支持:

  • 展示scratch游戏,支持触屏操作
  • 显式手柄按钮,支持基本按键
  • 对接微信js sdk,实现朋友圈/好友分享

关键知识点

HOC

HOC是react里面的一种设计模式,全拼:A higher-order component。

scratch大量使用了该模式,它本身是一个函数,输入是一个react组件,返回值是一个包装了原始react组件的新组件。

大家可以把HOC理解成python中的装饰器,或者是设计模式中的装饰器模式。

我们先说HOC可以干啥,然后再举个具体栗子:

  • 定义一个HOC,AJAX获取登录状态,注入到redux store中,给下层组件使用
  • 定义一个HOC,创建redux store,用<Provider>标签包装下层组件,以便下层组件使用redux

看个具体例子。

我要实现”个人主页”的页面,因此定义了一个My组件,下面是截取了一段不完整的代码片段:

我们注意到,这个组件render时用了一个userinfo的prop,它其实是redux注入进来的:

那么my组件其实就提出了2个前置依赖:

  • 首先需要上层提供redux store的<Provider>包裹
  • 其次需要上层把登录状态state.loginChecker.userinfo更新到redux store里

好,需要上述2个前置依赖的组件可能到处都是,这时候就是HOC出场的机会了。

为了解决Provider问题,我们定义这样一个HOC函数:

这个HOC函数接受一个react组件作为参数,然后创建了redux store,并且Provider包装了参数传进来的组件。

我们完全可以把my组件传进来,这样my组件就可以访问到redux了,这就是HOC的装饰器效果。

但这还不够酷,我们的my组件还需要登录状态,因此我再做一个HOC来在上层搞定这个事情:

这个HOC也是接受一个react组件,但是render的时候只是原样渲染,并没有增加外层包装。

但是我们注意,这个HOC存在的价值是ajax获取登录接口数据,然后dipsatch+reducer更新登录信息到redux store中。

这也是HOC擅长的领域,就是为下层提供状态,这样就不需要下层每个业务组件都去实现一套登录状态的ajax调用了。


看完了HOC实现,我们最后怎么把HOC装饰到my组件上呢?

在my组件中,我们首先为它包装redux容器,然后串联我们的2个HOC函数来完成2层包装得到一个最终的react组件:

compose函数是redux提供的一个便捷方法,其等价于ProviderHoc(WebLogicCheckerHOC(connectedMy)),即逐层包装。

如果把redux connect方法的包装也加进来的话,那么就是:

ProviderHoc(WebLogicCheckerHOC(connect(mapStateToProps, mapDispatchToProps)(My)))

因此我们的My组件被附加了很多能力:

  • 来自connect提供的redux state/dispatch注入
  • 来自WebLoginCheckerHOC的登录state更新
  • 来自ProviderHOC提供的redux store上下文

Redux store/action/dispatch

通过魔改scratch,对store/action/dispatch的关系理解更加透彻了。

其实前端组件化开发现在很像后端开发,我们可以做一个类比:

后端围绕mysql做增删改查,前端围绕redux store做增删改查,因此我们就把store看做一个前端数据库即可。

比如我们在A组件中AJAX拉取了用户的登录状态,那么我们通常会通过redux提供的dispatch方法来触发一次对store的更新操作,而具体要更新的内容就要放在dispatch的参数里,我们叫它为一个action。

为什么要把登录状态更新到store里呢?因为其他组件可能想访问登录状态,这不就是mysql的增删改查吗?其他组件只需要把store里的数据注入到props里即可。

而回到redux dispatch方法自身,实际就是拿着我们提供的action对象,调用所有我们注册在store上的reducer函数,由各个reducer函数自行决定如何更新store。

scratch3.0的架构告诉了我们,reducer只是用来做数据更新的,不要把异步请求之类的逻辑放进去,它是很纯粹的。事件响应、异步任务处理等都应该发生在具体的组件当中,只有当组件希望更新数据时才需要通过dispatch来触发reducer流程。

实际上,scratch3.0压根没用到redux-thunk中间件,我觉得这种设计理念或者说规范非常好。

antd引入

react组件化开发难免要写很多基础效果的组件,比如模态框、错误提示信息等。

因为是个人项目没有UI设计,所以我觉得能省时省力、简洁高效即可,所以引入了antd组件库。

使用antd并不难,最难的还是如何引入antd到webpack编译环境中。

antd自身的css是全局名字,而我们开发项目一般是使用了css module的,为了避免影响到antd的css名字,我们需要分别对待:

上面这段通过include指定了对于依赖的antd模块,没有采用css modules配置项。

而对于我们自己的项目则通过exclude排除掉antd,同时开启css modules:

options里面的东西除了modules需要注意区分,其他选项根据自己项目配置即可。

另外babel-loader里也要求增加一个插件配置,其目的应该是自动加载antd组件依赖的css的意思:

 

process.env.NODE_ENV 环境变量

因为使用了阿里云OSS存储项目,所以我希望开发环境与线上环境能分开存储文件。

前端也是一样的,希望前端AJAX拉取项目文件的时候,能根据所处环境不同从不同的CDN路径拉取。

前端是运行在浏览器中的,怎么能区分出开发环境/线上环境呢?

思路就是webpack编译代码的时候,把编译时刻的环境变量设置到js代码中的全局变量,这样前端在浏览器运行的时候就可以根据全局变量判定出编译的时候到底是指定了开发环境还是生产环境了。

要做到这一点,需要2步:

  • 执行webpack编译之前,先export NODE_ENV=production环境变量,这样webpack整个编译过程中都是可以拿到process.env.NODE_ENV的。
  • 而webpack编译完成后,代码运行在浏览器中,肯定已经没有linux的环境变量信息了,这时候怎么办?

OK,第2步就是我说的webpack编译的时候在输出的JS代码中定义一个全局变量来记录环境变量,这样浏览器运行JS的时候,JS代码还是可以获取到这个信息。

完成这一步,需要我们在webpack配置文件中利用DefinePlugin插件,把linux环境变量生成到JS的全局变量定义中去:

接下来,在JS代码中就可以直接判定了:

移动端长按弹窗问题

因为我给scratch手机端做了游戏手柄,包含了:上、下、左、右、空格 按键,对应5张图片。

当我按住某个键的时候发现浏览器弹出了菜单,让我保存图片或者在新标签页中打开,这个问题让我搞了好半天。

一开始搜到了一种原因,说是需要通过这样的CSS禁用菜单效果:

:global仅仅是用于说明不使用css module的意思,应用后发现chrome效果OK,但是小米浏览器、微信浏览器依旧弹窗。

也一度尝试过对touch系列事件做了preventDefault禁止浏览器默认行为,但是都无果。

最终发现原因:因为我直接使用了<img>标签来显示按钮,因此当我按住按钮的时候浏览器认为我的意图是保存图片。

最简单的解决方案是使用background-img取代img标签,这样浏览器就不会提示你下载图片了。

className库

还是以scratch游戏手柄为例,当某个方向被按下的时候,我会高亮这个按钮,松开则恢复。

因此,我希望当按钮发生touchstart事件的时候附加一个.active的css class,并在css中应用高亮的对应样式。

这会导致我们用React组件传className的时候比较费劲,具体原因就是className如果传多个class需要这样写:className=”cls1 cls2 cls3″,而我们需要根据按钮按下的状态(实际就是redux中的state)来决定哪个class保留,哪个class不保留。

所以我们需要用到一个库来简化这个事情,叫做:

然后就可以通过这样的方式,让classNames方法根据redux状态判定哪些class需要引入:

总是赋予的类是styles.spaceBtn,而当this.props.space=true表示空格按下的时候,我需要额外的附加active类,是不是很方便?

this.xxx和this.props.xxx有什么区别

在改scratch的过程中,我一度在思考一个问题,为什么总要那么麻烦的触发dispatch -> reduce,然后再在组件中state -> props注入状态呢?

为什么不直接修改组件的this.xxx呢?后来试了一下我才恍然大悟,不修改props是无法触发render刷新的,而我们应用大部分时候改变数据是需要重新渲染的。

因此我们也可以知道,如果改变的属性与渲染没有关系的话,完全可以直接改this.xxx而不是走dispatch的流程。

当然,如果我们无脑的全部走dispatch,那么肯定没问题,但应该会导致一些无效的重新VDOM计算。

bindAll

也是scratch中常见的方法,我们做一些onClick事件处理的时候,经常会把组件的方法作为回调函数,因为ES6的类方法不支持this,所以我们以往都是手动this.handleClick.bind(this)。

当有大量回调方法的时候,这样重复的粘贴复制就显得很乱,所以我们只需要用这个库来批量绑定即可:

然后一次性搞定:

关于阿里云OSS+CDN

第一使用阿里云,有几点体会也分享给大家:

  • OSS存储容量收费。
  • OSS上传不收费,下载需要交下行流量费,所以不要把OSS设置为public read,否则被刷就惨了。
  • OSS通过授权CDN,可以实现CDN -> OSS的流量回源,需要交OSS的回源流量费。
  • CDN需要交外网带宽费。

最后

关于scratch3.0二次开发的经验就总结这么多,希望对大家有帮助。

出售源码/服务

最近很多机构和个人找到我的微信,问我是否卖源码或者可以提供服务。

为了确保大家买得放心,买的公平,我在这里统一说明一下:

  • 买源码:基本上是有研发能力的个人/机构,想透过源码了解核心解决方案,该方案采用买断制:提供源码与咨询,价格3万元。
  • 买服务:对技术不了解,但想提升机构形象和信息化的机构,该方案采用年费制:私有化搭建,价格5000元/年,服务器钱自理(每年不超过2000元)。

有意向加微信详聊。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~