前言
之前有小伙伴问我,经常在各种前端框架的文档中看到的“前端状态管理”究竟是什么意思,他始终弄不懂。我想可能初学前端的同学多少都会对这个概念比较懵,状态管理又是现代的前端框架中非常核心的内容,不理解这个就很难完整的理解React/Vue这类框架, 那我就写一篇文章来解释一下这个概念吧。
前端状态
首先说说什么叫“前端状态"。所有程序都有“状态”,状态表现在代码中其实就是各种类型的变量,其实程序运行的过程就可以理解为是程序内部的“状态“发生改变的过程,而我们编写的程序就是在控制这些“状态”如何发生改变。
前端状态的概念主要是应用在单页应用SPA(Single Page Application)中的,是在React/Vue等现代化的前端框架流行起来之后才有的一个提法,之前的jQuery时代是没有这种概念的。
SPA
那么SPA和之前的网页有什么区别呢?其实最本质的区别就是"服务端渲染“和”前端渲染“的区别。
所谓服务端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面是完整的,浏览器直接展示这个HTML内容再加上CSS的样式即可。JS主要做一些辅助性的特效或其他工作。
而前端渲染,是指浏览器请求一个URL地址,服务端返回的HTML页面并不包含具体内容,而是会通过 script标签引入一个JavaScript文件。浏览器只有通过解析和执行JS代码页面上才会展示出内容,否则页面就是空白的。而且后续的页面更新也都是在JS中完成,而不是通过跳转到另一个URL地址来完成(这也是单页应用名字的由来)。当然这里并不是指URL地址就一定不会变化,而是说页面不会“刷新”。
SPA的出现使得Web前端成为真正的"客户端程序",可以独立完成渲染(DOM API)、网络请求(XHR,fetch)等任务,也就是所谓的"前端渲染",而不是只是一个“网页展示器”。
声明式编程和命令式编程
前端状态管理之所以流行的另一个原因是,现代的前端框架包括React/Vue都是声明式的编程方式,而之前的jQuery是命令式的。所谓声明式,就是说你在代码中不会直接去操作UI,而是通过操作数据,来间接地改变UI内容。而命令式,是直接操作UI的,这就导致数据层和展现层通常是不作区分的。
声明式的编码方式天然的会把数据状态和页面代码分离,所以更需要一套独立的状态管理系统。
Todo App示例
上面是从概念上解释,接下来我们举一个实例来解释一下这个概念。这里举一个很简单的Todo App:
上面两张图分别是列表页和详情编辑页。列表页中的内容是在打开首页的时候从服务端通过请求来的。点击列表页中的一个标题,会进入到详情编辑页,要求在编辑页中显示的标题要和列表页的标题对应。在编辑页可以编辑标题和内容,点击Save按钮会返回到列表页,这里页要求列表页的标题内容更新为新的标题。
产品需求介绍完了,我们先来看一下如果是服务端渲染的方式,是如何工作的。
首先,当你在浏览器中输入https://todo.com/网址的时候,浏览器向你的服务端发起HTTP请求,你的服务端返回给浏览器HTML文件。这个HTML文件中包含了完整的Todo List的内容。浏览器只需要解析HTML代码以及CSS然后展示即可。
列表中的每一项都是一个<a href="/:id">标签,比如当你点击id为1的一个标题的时候,浏览器会“跳转”到https://todo.com/1的地址。这时候会向你的服务器发送另一个HTTP请求,URL就是https://todo.com/1。你的服务器又会针对这个地址返回对应的HTML文件,同样的,这个HTML文件也是有完整内容的,浏览器要做的仅仅是展示HTML内容。
这里的"Save"按钮会是一个form Submit按钮,它会使浏览器向你的服务端发送一个POST请求,并带上你在页面中新输入的标题和内容。你的服务器收到请求之后,会把新输入的内容保存数据库,然后返回一个"Redirect",使你的浏览器重新跳转到列表页。
然后我们看一下如果是前端渲染的方式:
你在浏览器中输入了网址:https://todo.com/,浏览器向你的服务端发起HTTP请求,然后你的服务器返回HTML文件。这一步是没有区别的,区别就在于服务端返回的那个HTML文件的内容。在服务端渲染的情况下,这个文件是由服务端“拼装”出来的,而在前端渲染的情况下,这个文件不包含任何内容,而只是引入了一个JS文件。比如下面这样的:
<html>
<head>
<script src="/app.js"></script>
<head>
<body>
</body>
</html>
当这个文件到达浏览器,因为HTML的body部分是空的,所以在执行JS之前页面会是空白的。接下来浏览器会下载和执行JS代码。
在JS代码中,你会使用XHR的方式向服务器发起一个“API请求“,这类请求是浏览器在后台发起,不会引起地址栏和页面刷新。和页面请求不同,服务器对这种“API请求”的响应内容通常会是JSON格式的(服务器通常是根据不同的请求路径判断是页面请求还是API请求,比如/api/*开头的请求都算作API请求)。比如,请求列表的URL是:GET /api/todos,它的响应是:
[
{
"id": 1,
"title": "title1",
"content": "This is list 1 content",
}
,...
]
收到响应之后,JS代码会被执行,之后会由JS执行DOM操作来改变页面的展示内容,也就是页面内容被“渲染”出来了。
这个列表页面中的每一条会是一个简单的<div>标签,而不是一个会发生浏览器跳转的<a>标签。当你点击其中一个标题的时候,同样会由JS代码接管。由于在请求列表的时候前端已经拥有了所有数据,所以这次不用再请求服务器了,JS代码直接重新操作DOM,来"渲染"出编辑页面即可。
当在编辑页面中点击Save按钮,同样触发JS代码,JS代码会向服务器发起一个保存修改的请求。如果服务器响应成功,那么JS会更新自身的数据,修改为新的标题和内容。
在这个例子中,所谓的前端“状态”,可能就是一个全局的JS对象,比如:
{
"todos": [
{
"id": 1,
"title": "title1",
"content": "This is list 1 content",
}
,...
]
}
而所谓的"状态管理",其实就是对这个全局对象的一系列增删改查的操作。
通过上面的例子,我们可以看一下前端状态管理的意义有哪些:
第一,数据管理逻辑和页面渲染逻辑分离,使得代码更容易维护。这其实有点类似于MVC的设计模式,操作数据的地方不会关心页面如何展示,展示页面的地方不会关心数据从哪里来的。
第二,可以保证数据有一份“唯一可信数据源“。比如上面例子中的“title"字段,在列表页面和编辑页面都会展示,如果不对这个状态做统一管理,很难保证数据的统一:比如在修改页面修改了之后要保证列表页面同步修改。在实际的项目中,同一份数据可能在N多个地方展示、修改,如果不做好状态管理,代码会写成什么样简直不敢想。
总结
看到这里,我不知道你会不会觉得奇怪,“不是要讲前端状态管理吗,为什么长篇大论的讨论服务端渲染和前端渲染?“。其实我是觉得因为有了单页应用的需求(为了解决浏览器刷新的用户体验问题),所以需要前端渲染;而因为有了前端渲染,所以前端不得不需要自己管理状态。理解了前端渲染的整个流程,对于理解前端状态管理是个前提,也有很大帮助。
不知道你现在对前端状态管理的概念有没有清晰一些。这篇文章主要介绍概念,并没有涉及具体框架的描述。其实理解了概念再去看redux/vuex这些框架,会容易理解很多。
如果你对前端技术感兴趣请关注我的公众号<前端时光机>,所有文章都会在那里首发,谢谢。