Scratch二次开发7:Scratch3.0作品的生命周期(各类状态)分析讲解

写在前面:

为公共事业做贡献,做了个开源版本:scratch.lite

开源版本带MySQL后台服务器,功能:注册、登录、保存作品、分享、修改作品名称、保存作品缩略图。

有兴趣的朋友可以去下载参考:lite: 一个轻量级的Scratch编程分享平台:注册登录、作品创作、作品管理、素材管理、用户管理,作品点赞、收藏、分享。

Scratch二次开发的纯技术交流QQ群:115224892/914159821

今天的内容有点硬核,不太熟悉Scratch源代码的朋友或是对REACT不太熟悉的朋友,可能会略感不适!!!

一、Scratch作品生命状态图

所有的平台、技术都是围绕数据内容做操作的。Scratch也不例外:它的一切过程,最终都会成为一个作品!!!

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!!!

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值