写在前面:

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

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

有兴趣的朋友可以去下载参考:https://gitee.com/scratch-cn/lite

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

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

一、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 nothingif (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 projectif (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 projectif (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 showif ([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 fineif ([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 notif (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及作者IDreturn {...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,//作品IDtitle:'',//作品名称authorId: 0,//作者IDstate:0,// 0:未发布;// 1:已发布;// 2:已开源;(开源的必须发布)//作业部分=====================================================homeworkProjectId:0,//>0时,作品为一个课程的作业student_id:0,//>0时,表示作品可以设置为作业,且其值就是学生报班是student表中的IDclass_id:0,//班级Id
};
const reducer = function (state, action) {if (typeof state === 'undefined') state = initialState;switch (action.type) {case SET_PROJECT_ID://设置准备加载的作品IDif (state.loadingState === LoadingState.NOT_LOADED || state.loadingState === LoadingState.SHOWING_WITH_ID) {// 作品ID没变:直接返回原状态,阻止重新加载projectif (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_IDif (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统一为0projectId: action.projectData.id==defaultProjectId?0:action.projectData.id,projectData: action.projectData.src,//作品JSON源代码authorId: action.projectData.authorid,//作者IDstate: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_IDif (state.loadingState === LoadingState.LOADING_VM_WITH_ID) {return {...state, loadingState: LoadingState.SHOWING_WITH_ID};}case COPY_PROJECT://复制作品:修改作品ID=0、作品作者ID=0if (state.loadingState === LoadingState.SHOWING_WITH_ID) {return Object.assign({}, state, {projectChanged: true,//自动设置改变标签projectId: 0,//作品IDauthorId: 0,//作者IDstate: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及作者IDreturn {...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,//作品IDauthorId: 0,//作者IDstate: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 showif ([LoadingState.FETCHING_WITH_ID,LoadingState.LOADING_VM_WITH_ID].includes(state.loadingState)) {return initialState}// non-fatal errors: can keep showing editor state fineif ([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!!!

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

  1. .NET Core 3.0 即将结束生命周期,建议迁移 3.1

    .NET Core 官方发布博客,说明 .NET Core 3.0 即将结束生命周期,建议开发者迁移到 3.1 版本. .NET Core 3.0 于 2019 年 12 月 3 日发布,这是一个 C ...

  2. atitit.提升开发效率---使用server控件生命周期 asp.net 11个阶段 java jsf 的6个阶段比較...

    atitit.提升开发效率---使用server控件生命周期  asp.net 11个阶段  java jsf 的6个阶段比較 例如以下列举了server控件生命周期所要经历的11个阶段. (1)初始 ...

  3. (二)Java线程与系统线程,生命周期

    本专栏多线程目录: (一)线程是什么 (二)Java线程与系统线程和生命周期 (三)Java线程创建方式 (四)为什么要使用线程池 (五)四种线程池底层详解 (六)ThreadPoolExecutor ...

  4. 【Android】8.0活动的生命周期(一)——理论知识、活动的启动方式

    1.0 Android是使用任务(Task)来管理活动的,活动就像栈一样堆放着在一起. 每个活动的生命周期最多可能会有四种状态: 1.1 运行状态 位于栈顶 1.2 暂停状态 不在栈顶但在界面上仍处于 ...

  5. Scratch二次开发0:少儿编程平台功能设计及各框架应用

    自打自己进入少儿编程这行,对这个行业慢慢的有所了解.以前基本上什么编程语言都用过,反正是需要开发的应用是合适用什么编程语言,就去使用,还好,对于编程选择的语言工具,对本人没障碍. 下面是自己的所思所想 ...

  6. scratch二次开发(一)

    一.scratch模块 ## scratch-vm 虚拟机解析加载序列化项目文件.扩展功能实现.根据相应事件渲染舞台### scratch-audio 声音引擎解析.播放声音### scratch-b ...

  7. 【五一创作】使用Scala二次开发Spark3.3.0实现对MySQL的upsert操作

    使用Scala二次开发Spark实现对MySQL的upsert操作 背景 在我们的数仓升级项目中,遇到了这样的场景:古人开发的任务是使用DataStage运算后,按照主键[或者多个字段拼接的唯一键]来 ...

  8. wap六感程序二次开发_Cscms v4.0 二次开发y2002音乐网站程序

    Cscms v4.0 二次开发y2002音乐网站程序 源码简介: 修复了多处问题,比网上流传的版本要完整很多. 程序包括pc+wap,页面功能和原y2002基本一样. 程序比较完整了,但还是会有bug ...

  9. E:大疆M300二次开发PSDKV2.1.0。无法识别无人机型号。一直出现 aircraft type 0

    连接好设备后(M300无人机,开发套件2.0,选用树莓派4B),可以运行示例程序,但是一直打印 [35.664][module_core]-[Info]-[PsdkCore_Init:134]PSDK ...

  10. Revit二次开发实现BIM盈利(以橄榄山快模为例讲解) 视频讲座下载

    应笔墨闲谈群的邀请, 在10月11号晚8:30分在其群做了一次关于BIM二次开发的讲座. 由于参与者基本上都是从设计院和施工单位来的,所以对Revit二次开发做了纵览性的讲解, 以非程序员能听懂的方式 ...

最新文章

  1. Javascript history pushState onpopstate方法做AJAX SEO
  2. 想做一个整合开源安全代码扫描工具的代码安全分析平台 - Android方向调研
  3. qt 实现拖动矩形角度_手机上如何使用CAD角度标注功能?
  4. 牛客网(剑指offer) 第九题 变态跳台阶
  5. IntelliJ IDEA for Mac工件包(artifact)中 Web facet resources 的模块名称有误,如何修改?
  6. Java代码质量改进之:使用ThreadLocal维护线程内部变量
  7. C/C++程序之根据有向图、无向图求通路、回路、可达矩阵
  8. ubuntu下载android11源码
  9. android程序图标透明,怎么把android手机软件图标变透明
  10. Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date'
  11. oauth2生成jwt令牌
  12. 软工网络15个人阅读作业2 201521123023 网络1511 戴建钊
  13. 携程 最短路径的代价
  14. linux 查看mmc分区_Linux MMC介绍
  15. 科技作者吴军:不用低效率的算法做事情
  16. 一个sql server2005分页的存储过程
  17. 2019java 开发工程师 最新面试官 问的问题
  18. 并购Opera,360之蜜糖,猎豹之砒霜
  19. 中台战略下的保险订单销售模式设计
  20. NeurIPS 2022 | Stable Diffusion采样速度翻倍!清华提出扩散模型高效求解器

热门文章

  1. token实现单点登录原理
  2. linux 怎么格式化u盘写保护,u盘怎样去掉写保护状态手机怎么加密软件
  3. 计算机网络-自顶向下方法 第五章课后习题答案(第七版)
  4. layui修改头像功能
  5. 计算机应用技术三级学科,三个计算机专业的区别是什么?
  6. C#上位机与施耐德PLC通讯
  7. 硬盘安装助手安装苹果Mac系统镜像Change partition type to AF: not a HFS partition的解决方法
  8. JAVA与GO语言之间应该选择学习哪个?
  9. word文档打印表格时预览时看的到表格打印出来的表格没有上下两根横线?
  10. charles安卓抓包步骤详解