写在前面:
为公共事业做贡献,做了个开源版本:scratch.lite
开源版本带MySQL后台服务器,功能:注册、登录、保存作品、分享、修改作品名称、保存作品缩略图。
有兴趣的朋友可以去下载参考:lite: 一个轻量级的Scratch编程分享平台:注册登录、作品创作、作品管理、素材管理、用户管理,作品点赞、收藏、分享。
Scratch二次开发的纯技术交流QQ群:115224892/914159821
今天的内容有点硬核,不太熟悉Scratch源代码的朋友或是对REACT不太熟悉的朋友,可能会略感不适!!!
一、Scratch作品生命状态图
所有的平台、技术都是围绕数据内容做操作的。Scratch也不例外:它的一切过程,最终都会成为一个作品!!!
Scratch二次开发过程中,也会绕不开作品的加载、修改及保存(既:增、删、改、存)。
本人通过Scratch作品的加载方式,分为三类作品,再来看这些作品的不同的生命周期。
先上一张图:
本文就是根据上面Scartch作品的三种不同加载方式,把作品分类,来讨论Scartch作品的加载、保存的。
二、Scratch作品的三种不同加载方式
1、从本机加载;
2、Scratch默认自带;
3、从服务器加载。
三、Scratch作品状态源代码
这三类作品的状态,主要体现在Scratch源代码中的一个:project-state.js(源文件在reducers目录下),其主要内容为(篇幅有限,就不全部贴出来了,只贴重要部分):
const LoadingState = keyMirror({
NOT_LOADED: null,
ERROR: null,
AUTO_UPDATING: null,
CREATING_COPY: null,
CREATING_NEW: null,
FETCHING_NEW_DEFAULT: null,
FETCHING_WITH_ID: null,
LOADING_VM_FILE_UPLOAD: null,
LOADING_VM_NEW_DEFAULT: null,
LOADING_VM_WITH_ID: null,
MANUAL_UPDATING: null,
REMIXING: null,
SHOWING_WITH_ID: null,
SHOWING_WITHOUT_ID: null,
UPDATING_BEFORE_COPY: null,
UPDATING_BEFORE_NEW: null
});
const initialState = {
error: null,
projectData: null,
title: "Scratch作品",
projectId: null,
loadingState: LoadingState.NOT_LOADED
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case DONE_CREATING_NEW:
// We need to set project id since we just created new project on the server.
// No need to load, we should have data already in vm.
if (state.loadingState === LoadingState.CREATING_NEW) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
projectId: action.projectId
});
}
return state;
case DONE_FETCHING_WITH_ID:
if (state.loadingState === LoadingState.FETCHING_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.LOADING_VM_WITH_ID,
projectData: action.projectData
});
}
return state;
case DONE_FETCHING_DEFAULT:
if (state.loadingState === LoadingState.FETCHING_NEW_DEFAULT) {
return Object.assign({}, state, {
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT,
projectData: action.projectData
});
}
return state;
case DONE_LOADING_VM_WITHOUT_ID:
if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD ||
state.loadingState === LoadingState.LOADING_VM_NEW_DEFAULT) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITHOUT_ID,
projectId: defaultProjectId
});
}
return state;
case DONE_LOADING_VM_WITH_ID:
if (state.loadingState === LoadingState.LOADING_VM_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID
});
}
return state;
case DONE_LOADING_VM_TO_SAVE:
if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD) {
return Object.assign({}, state, {
loadingState: LoadingState.AUTO_UPDATING
});
}
return state;
case DONE_REMIXING:
// We need to set project id since we just created new project on the server.
// No need to load, we should have data already in vm.
if (state.loadingState === LoadingState.REMIXING) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
projectId: action.projectId
});
}
return state;
case DONE_CREATING_COPY:
// We need to set project id since we just created new project on the server.
// No need to load, we should have data already in vm.
if (state.loadingState === LoadingState.CREATING_COPY) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
projectId: action.projectId
});
}
return state;
case DONE_UPDATING:
if (state.loadingState === LoadingState.AUTO_UPDATING ||
state.loadingState === LoadingState.MANUAL_UPDATING) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID
});
}
return state;
case DONE_UPDATING_BEFORE_COPY:
if (state.loadingState === LoadingState.UPDATING_BEFORE_COPY) {
return Object.assign({}, state, {
loadingState: LoadingState.CREATING_COPY
});
}
return state;
case DONE_UPDATING_BEFORE_NEW:
if (state.loadingState === LoadingState.UPDATING_BEFORE_NEW) {
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_NEW_DEFAULT,
projectId: defaultProjectId
});
}
return state;
case RETURN_TO_SHOWING:
if (state.projectId === null || state.projectId === defaultProjectId) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITHOUT_ID,
projectId: defaultProjectId
});
}
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID
});
case SET_PROJECT_ID:
// if the projectId hasn't actually changed do nothing
if (state.projectId === action.projectId) {
return state;
}
// if we were already showing a project, and a different projectId is set, only fetch that project if
// projectId has changed. This prevents re-fetching projects unnecessarily.
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
// if setting the default project id, specifically fetch that project
if (action.projectId === defaultProjectId || action.projectId === null) {
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_NEW_DEFAULT,
projectId: defaultProjectId
});
}
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_WITH_ID,
projectId: action.projectId
});
} else if (state.loadingState === LoadingState.SHOWING_WITHOUT_ID) {
// if we were showing a project already, don't transition to default project.
if (action.projectId !== defaultProjectId && action.projectId !== null) {
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_WITH_ID,
projectId: action.projectId
});
}
} else { // allow any other states to transition to fetching project
// if setting the default project id, specifically fetch that project
if (action.projectId === defaultProjectId || action.projectId === null) {
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_NEW_DEFAULT,
projectId: defaultProjectId
});
}
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_WITH_ID,
projectId: action.projectId
});
}
return state;
case START_AUTO_UPDATING:
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.AUTO_UPDATING
});
}
return state;
case START_CREATING_NEW:
if (state.loadingState === LoadingState.SHOWING_WITHOUT_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.CREATING_NEW
});
}
return state;
case START_FETCHING_NEW:
if ([
LoadingState.SHOWING_WITH_ID,
LoadingState.SHOWING_WITHOUT_ID
].includes(state.loadingState)) {
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_NEW_DEFAULT,
projectId: defaultProjectId
});
}
return state;
case START_LOADING_VM_FILE_UPLOAD:
if ([
LoadingState.NOT_LOADED,
LoadingState.SHOWING_WITH_ID,
LoadingState.SHOWING_WITHOUT_ID
].includes(state.loadingState)) {
return Object.assign({}, state, {
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
});
}
return state;
case START_MANUAL_UPDATING:
//console.log("已点击保存,当着作品状态:"+state.loadingState);
if (state.loadingState === LoadingState.SHOWING_WITH_ID||state.loadingState === LoadingState.SHOWING_WITHOUT_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.MANUAL_UPDATING
});
}
return state;
case START_REMIXING:
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.REMIXING
});
}
return state;
case START_UPDATING_BEFORE_CREATING_COPY:
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.UPDATING_BEFORE_COPY
});
}
return state;
case START_UPDATING_BEFORE_CREATING_NEW:
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return Object.assign({}, state, {
loadingState: LoadingState.UPDATING_BEFORE_NEW
});
}
return state;
case START_ERROR:
// fatal errors: there's no correct editor state for us to show
if ([
LoadingState.FETCHING_NEW_DEFAULT,
LoadingState.FETCHING_WITH_ID,
LoadingState.LOADING_VM_NEW_DEFAULT,
LoadingState.LOADING_VM_WITH_ID
].includes(state.loadingState)) {
return Object.assign({}, state, {
loadingState: LoadingState.ERROR,
error: action.error
});
}
// non-fatal errors: can keep showing editor state fine
if ([
LoadingState.AUTO_UPDATING,
LoadingState.CREATING_COPY,
LoadingState.MANUAL_UPDATING,
LoadingState.REMIXING,
LoadingState.UPDATING_BEFORE_COPY,
LoadingState.UPDATING_BEFORE_NEW
].includes(state.loadingState)) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
error: action.error
});
}
// non-fatal error; state to show depends on whether project we're showing
// has an id or not
if (state.loadingState === LoadingState.CREATING_NEW) {
if (state.projectId === defaultProjectId || state.projectId === null) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITHOUT_ID,
error: action.error
});
}
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
error: action.error
});
}
return state;
case SET_PROJECT_NEW_ID://设置刚被新建保存到服务器的作品id及作者ID
return {...state, projectId: action.projectId};
case SET_PROJECT_TITLE://修改作品名称
return {...state, title: action.title};
default:
return state;
}
};
四、Scratch作品状态讲解
如果看不懂各作品的状态之间的关系,想要自己做点啥都会有点傻眼,您会发现下不了口。
本人特意把各类状态按加载显示阶段的顺序排列,然后再一一加以说明。
Scratch最初的状态:
NOT_LOADED:没有加载任何作品时的状态。此状态为Scratch初次加载时的一个必经状态,此时Scratch没有加载任何作品数据,也不知道要从哪里加载作品。
Scratch加载作品的状态:
FETCHING_NEW_DEFAULT:从Scratch内部加载default-project目录下的那个默认作品。(作品ID=0,我们也可当他没有作品ID)。
FETCHING_WITH_ID:根据给出的作品ID,从服务器加载一个作品。
CREATING_NEW:创建一个新的作品。(注:会判断前一个作品是否需要保存,并加载Scratch的默认作品,即src/lib/default-project目录下的那个作品,也就是我们一打开就会看到的好个小猫猫的空白作品)。
CREATING_COPY:复制当前作品为一个新的作品(没有作品ID)。
REMIXING:改编当前作品,即复制当前作品(新作品同样没有作品ID)。
Scratch作品加载到VM中的状态:
LOADING_VM_FILE_UPLOAD:从本机直接加载一个作品到VM中。(这个最好理解,此时也没有作品ID)。
LOADING_VM_NEW_DEFAULT:把已经fetch成功的默认作品加载到VM中。
LOADING_VM_WITH_ID:把已经fetch成功的服务器作品加载到VM中。
Scratch作品的正常显示状态:
SHOWING_WITH_ID:当前状态表示作品已有ID,即在服务器上有了,可以做一些有作品ID时的动作,比例:自动保存。
SHOWING_WITHOUT_ID:当前状态表示作品是一个默认作品或是从本机加载的作品。
Scratch保存作品的状态:
AUTO_UPDATING:自动保存当前作品到服务器。(进入这个状态的一个前提:当前作品是从服务器加载的,通俗的讲,就是这个作品是用ID的。顺便聊一句:从本机加载或是新建的作品,都是没有作品ID的,所以无法自动保存,毕竟服务器上的作品,都是需要有作品ID,才知道谁是谁。)
MANUAL_UPDATING:手动保存作品。(即用户主动点击保存按钮时触发)
UPDATING_BEFORE_NEW:新建作品前,保存当前作品。
UPDATING_BEFORE_COPY:复制作品前,保存当前作品。
Scratch作品的其他状态:
ERROR:加载作品过程中出错的状态,用于报警与回到一个正确的状态。
五、综述
根据上面的各种状态的描述及配合最上面的图,基本上可以明白Scratch作品的生命周期。
个人也是看了一天的时间,才把这里面的弯弯绕看明白。
觉得这个Scratch的各种状态非常复杂,也没明白老外为什么这么设计,感觉他们有点傻傻的(玩笑话,当真你就输了:))。
于是自己动手,丰衣足食:把Scratch作品状态从原来的 16 种简化到只有 7 种。
把整个Scratch作品的生命周期给简化了。然后Scratch系统也自然而然的给简化了。
简化后大大减少的状态的种类,并且功能更加多了。
在此简单的贴一下本人简化后的状态源代码(仅供参考):
// 作品的各种状态
const LoadingState = keyMirror({
ERROR: null,
NOT_LOADED: null,
FETCHING_WITH_ID: null,
LOADING_VM_WITH_ID: null,
SHOWING_WITH_ID: null,//显示的正常作品:用户自己的作品或是别人的作品
MANUAL_UPDATING: null,//开始手动更新作品
LOADING_VM_FILE_UPLOAD: null,//从本机加载作品时的状态
});
const defaultProjectId = 0;
const initialState = {
loadingState: LoadingState.NOT_LOADED,//当前作品状态
error: null,
//设置projectChanged==true的条件:
// 1、作品源代码被更改
// 2、新建、复制、改编、上传的作品(同时,设置作品ID=0、作品作者ID=0)
projectChanged:false,//作品是否已改变,即为是否需要保存
//作品部分=====================================================
projectData: {},//整个作品的JSON格式的源代码
projectId: 0,//作品ID
title:'',//作品名称
authorId: 0,//作者ID
state:0,// 0:未发布;// 1:已发布;// 2:已开源;(开源的必须发布)
//作业部分=====================================================
homeworkProjectId:0,//>0时,作品为一个课程的作业
student_id:0,//>0时,表示作品可以设置为作业,且其值就是学生报班是student表中的ID
class_id:0,//班级Id
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_PROJECT_ID://设置准备加载的作品ID
if (state.loadingState === LoadingState.NOT_LOADED || state.loadingState === LoadingState.SHOWING_WITH_ID) {
// 作品ID没变:直接返回原状态,阻止重新加载project
if (state.projectId>0 && state.projectId === action.projectId) {return state;}
// 作品ID变了:设置状态为FETCHING_WITH_ID,即通知对应组件请求新的project数据
return Object.assign({}, state, {
loadingState: LoadingState.FETCHING_WITH_ID,
projectId: action.projectId
});
}
case DONE_FETCHING_WITH_ID://对已经下载好了作品,做数据配置
if (state.loadingState === LoadingState.FETCHING_WITH_ID) {
// 已经获取到作品数据,开始加载到VM中:LOADING_VM_WITH_ID
if (action.projectData.student_id){
state.student_id = action.projectData.student_id;
if (action.projectData.homeworkProjectId){
state.homeworkProjectId = action.projectData.homeworkProjectId;
}
if (action.projectData.class_id){
state.class_id = action.projectData.class_id;
}
}
if (action.projectData.lesson_id){
state.lesson_id = action.projectData.lesson_id;
}
if (action.projectData.card_count){
state.card_count = action.projectData.card_count;
}
return Object.assign({}, state, {
loadingState: LoadingState.LOADING_VM_WITH_ID,
projectChanged: action.projectData.id==defaultProjectId,//自动设置改变标签
//作品ID:如果是默认作品,则Id也设置为0,与复制、改编、上传的作品被保存前的Id统一为0
projectId: action.projectData.id==defaultProjectId?0:action.projectData.id,
projectData: action.projectData.src,//作品JSON源代码
authorId: action.projectData.authorid,//作者ID
state:action.projectData.state,//分享:是否分享
title:action.projectData.title,//作品名称
//作业相关部分
class_id:state.class_id,
homeworkProjectId:state.homeworkProjectId,
student_id:state.student_id
});
}
case DONE_LOADING_VM_WITH_ID://已经加载到VM,设置作品状态为SHOWING_WITH_ID
if (state.loadingState === LoadingState.LOADING_VM_WITH_ID) {
return {...state, loadingState: LoadingState.SHOWING_WITH_ID};
}
case COPY_PROJECT://复制作品:修改作品ID=0、作品作者ID=0
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return Object.assign({}, state, {
projectChanged: true,//自动设置改变标签
projectId: 0,//作品ID
authorId: 0,//作者ID
state:0,//分享:是否分享
});
}
case START_UPDATING://开始更新
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return {...state, loadingState: LoadingState.MANUAL_UPDATING};
}
case DONE_UPDATING://上传更新成功
if (state.loadingState === LoadingState.MANUAL_UPDATING) {
return {...state, loadingState: LoadingState.SHOWING_WITH_ID};
}
case SET_PROJECT_NEW_ID://设置刚被新建保存到服务器的作品id及作者ID
return {...state, authorId:action.authorId, projectId: action.projectId};
case START_LOADING_VM_FILE_UPLOAD://加载本地作品
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
return {...state, loadingState: LoadingState.LOADING_VM_FILE_UPLOAD};
}
case DONE_LOADING_VM_FILE_UPLOAD://加载本地作品完毕
if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
projectChanged: true,//自动设置改变标签
projectId: 0,//作品ID
authorId: 0,//作者ID
state:0,//分享:是否分享
title: action.title,//作品名称
error: null,
projectData: {}//整个作品的JSON格式的源代码
});
}
case RETURN_TO_SHOWING:
return {...state, loadingState: LoadingState.SHOWING_WITH_ID};
case SET_PROJECT_TITLE://修改作品名称
return {...state, title: action.title};
case SET_PROJECT_SHARE://修改作品分享状态
return {...state, state:1};
case SET_PROJECT_CHANGED://设置作品是否需要被保存
//console.warn("BLJ:设置作品是否需要被保存"+action.changed);
return {...state, projectChanged: action.changed};
case SET_HOMEWORK_PROJECT://设置作品为作业
state.homeworkProjectId = action.workId;//用户在新建、打开其他作品时,作业ID应该一直有效
return {...state, homeworkProjectId: action.workId};
case START_ERROR:
// fatal errors: there's no correct editor state for us to show
if ([
LoadingState.FETCHING_WITH_ID,
LoadingState.LOADING_VM_WITH_ID
].includes(state.loadingState)) {
return initialState
}
// non-fatal errors: can keep showing editor state fine
if ([
LoadingState.MANUAL_UPDATING
].includes(state.loadingState)) {
return Object.assign({}, state, {
loadingState: LoadingState.SHOWING_WITH_ID,
error: action.error
});
}
// default: return state;
}
return state;
};
写在后面:
如果本文章对您有帮助,请不吝点个赞再走(点赞不要钱,只管拼命赞)!!!
您的支持,就是本人继续分享的源动力,后续内容更加硬核+精彩,请 收藏+关注 ,方便您及时看到更新的内容!!!
Bailee 了个Bye!!!