前端框架是一个比较重要的话题,我们会从以下方面对框架进行讨论
- 为什么需要框架
- 框架具体需要解决哪些问题
- 对应问题有哪些解决方案
我们会在另外文章针对react和vue进行具体解读,因此本文不会涉及两者过多的细节。
1 为什么需要框架
我们在js的使用过程中,会把一些解决常用问题的方案抽离出来复用,这被称为library,比如jquery和lodash,我们可以用前者操作dom,用后者封装的一些常用函数。
而在我们日常开发做的大多是整个网站,即web aplication,使用框架(framework)可以让我们更容易和高质量的开发web应用,并且做到可预测性(predictability) 和统一性(homogeneity),其中可预测性可以保证即使应用开发再大也可维护。
framework是一种特殊的library。
一个web应用需要引入框架的最大原因是要解决状态和ui的同步问题,如果我们自己实现需要使用大量命令式代码直接创建dom,并根据状态直接修改dom,虽然说了jquery等库后在操作上有了很大提高,但是状态的维护以及对应ui的同步仍比较繁琐且性能低下,具体案例请参考The deepest reason why modern JavaScript frameworks exist。
而vue和react等框架的产生可以将状态和ui的同步由繁琐的命令式操作,改为简明的声明式操作(命令式和声明式等的区别可以参考编程范式)。
而框架本身以及相关的生态也使我们的开发起到了事半功倍的效果。
其他参考:
- Introduction to client-side frameworks
2 框架解决了我们哪些问题
前端框架是包括相关js框架为核心全套解决方案,其中vue包含vue核心、vue-router、vuex等在内的完整方案,因此在官网自称framework。
而react只提供核心库,其他方案由社区提供,因此react自称library。
一个完整web应用的其他功能是由生态中其他library完成的,比如状态管理工具、路由等。
2.1 框架核心库需要解决什么问题
我们已经知道了框架核心解决的是页面构建的问题,现在需要进一步确定具体问题,为了了解框架核心库的细节,我们在这里现场debug一个框架的核心库,即preact,是一个比较受欢迎的类react库,对外的ui和react类似,但是实现较简单。
2.1.1 调试 preact
这里搭建debug环境
- 使用webpack创建一个包含webpack-dev-server的简单preact应用(webpack相关参考这里)
- 下载preact并按照这里下载并配置相关babel
- 创建一个入口文件
//src/index.js
import { h, render } from "preact";
import { useState } from "preact/hooks";
const App = () => {
const [state, setState] = useState(1);
return (
<div>
<div
onClick={() => {
setState((v) => v + 1);
}}
>
点击
</div>
{state}
</div>
);
};
render(<App />, document.body);
- 因为我们import时默认使用的是打包后的版本,因此需要分别修改node_modules目录下,preact/package.json和、preact/hooks/package.json中的module字段为"src/index.js"以便使用开发版本进行调试(package.json文件相关介绍参考这里)。
框架的核心库一般实现了两部分功能,一个是首次渲染,一个是通过交互等修改状态触发重新渲染,我们启动webpack开发环境依次来调试
首次渲染,不讨论状态相关逻辑
- jsx语法
<App/>
会首先调用createElement()
方法,并通过createVNode
返回vnode对象 - 调用
render()
方法,其中第一个参数是前面返回的vnode,第二个是视图挂载的dom节点,这里是body元素,然后设置一个commitQueue,并通过diff元素修改,然后调用commitRoot提交- diff函数会对比出修改前后两次的区别,并进行更新
- 调用
commitRoot()
时参数分别是commitqueue和vnode,会遍历commitqueue,然后调用各自回调执行相关副作用
修改状态
- 在首次渲染初始化组件时调用
const [state, setState] = useState(1)
,实际上调用了useReducer()
,返回一个包含state状态和setState函数的数组,并在页面对应位置显示初始值 - 点击对应元素,触发
setState()
方法,调用Component.prototype.setState()
及enqueueRender
将需要更新的元素加入渲染队列 - 调用
defer()
方法异步调用process()
,将渲染队列中dirty属性为true的组件分别调用renderComponent()
方法进行更新,后者分别调用了diff()
和commitroot()
.
2.1.2 归纳
一个前端框架核心库,需要解决如下问题
- 事件系统,监听各种事件
- 组件设计问题,怎么样的代码组织形式,包含css等
- 变化侦测,判断哪些组件发生了变化
2.2 框架相关库需要解决什么问题
当我们有了框架核心后,一个web应用还需要其生态解决以下问题
- 数据管理问题
- 前端路由问题
- 开发环境问题,即提供开箱即用的脚手架
- 服务端渲染问题
- 代码调试问题
另外网络请求和各种ui库不在本次讨论范围内。
3 解决方案
本章会讨论上一章待解决问题的解决方案和相关概念,参考
- Introduction to client-side frameworks
- 去哪儿网迷你React的研发心得
- 不吹不黑聊聊前端框架 原是尤雨溪的一个live,链接是一个整理笔记
- Presentational and Container Components
- Evan You on Vue.js: Seeking the Balance in Framework Design
3.1 事件系统
浏览器自带的事件系统可以参考这里,当我们开发一个项目时这个事件系统会存在两个问题
- 重新渲染dom就需要在对应dom重新注册和监听事件,造成性能损耗
- 不同浏览器具体实现有所不同,存在兼容性问题
介于以上原因,一个框架一般会为进一步封装自己的事件系统,比如react的合成事件。
3.2 组件设计问题
组件是将部分ui和内部逻辑(甚至只有逻辑)拆分出以提高可维护性和可复用性的方案,类似于html5规范中的 Web Components,前者已经较为成熟。
组件内部可以维护私有的状态,组件之间也可以相互通信,可以进行嵌套,通过继承和组合将整个应用组织成一个树状结构。
3.2.1 组件分类
按编写形式来分,组件可以是一个函数,也可以是一个class,当然两者只是语法糖的区别,并没有什么本质区别。
按起到的作用来区分,可以分为以下几种
- 纯展示组件,输入数据,输出dom,比如react中的presentational components
- 接入型组件,包含和服务端的数据交互,用于向展示组件中传递数据,比如react中的container components
- 交互型组件,指的是主要解决交互问题的组件,且能做到通用,比如elementUI
- 功能型组件,不会输出ui,用来复用逻辑,比如vue中的router-view组件、transition组件
3.2.2 模板和jsx
模板和jsx是前端ui构建中两种主要的代码组织形式。
以react为代表的library使用jsx,可以像使用普通js一样可以自由组织逻辑,因此难以对用户的使用方式进行可控的预判,进而做出高效的优化,jsx的下一步是生成virtual dom进行diff后才能渲染。需要做这么多工作也就意味着在我们的生产代码中需要添加很多库本身的runtime代码。
以svelte为代表的library使用模板,它的使用要遵守特定的规则,这也就意味着编译器可以对有限规则的写法进行处理,在不需要virtual dom的参与下直接修改对应dom,比如
也对应着相关设计思路的library会有更少的runtime代码。
而vue的设计是兼顾了两种写法,既可以使用jsx给予用户书写的自由度,又提供了模板来增大优化空间,不过vue2实际上会把这两种编写方式统一转为virtual dom,并没有充分利用模板的优势,这一点vue3已经做了关于模板的优化。
3.2.3 代码的拆分
和传统按语言来拆分,而引入组件以后,代码的组织要以组件为单位,表示ui和逻辑的html和js基本没异议,对于css的处理还存在以下方案,最终的结果都是保证不同组件中的class名的唯一性。
- 传统的方案,利用BEM等方案创建全局样式
- css module,模块化css
- css-in-js,在react生态中常用
- 单页面组件css,比如在vue中使用scoped
css各种方案详情参考这里。
3.3 变化侦测
这基本是一个框架最核心的功能了,也是决定一个框架性能的关键因素
3.3.1 侦测方案
前面提到,框架会把原来命令式修改dom的写法,改为声明式的写法,而框架要保证状态更改后能够及时刷新首先要做到能够对变化做到高效的侦听,变化侦听主要包括两种
- pull 即框架并不知道数据是否变化,只会知道有相关事件导致可能变了,因此需要对前后做一次比对找出具体变化位置,比对过程对性能有所浪费,比如react和前面提到的preact。
- push 另一种是框架在相关节点做了监测,监测的粒度越细,对变化的位置获取的越精确,但这也意味着相关observabel/watcher会带来内存和依赖追踪的开销,vue的实现是注入中等粒度的监测,并在小范围进行diff,即两种方案的结合。
3.3.2 virtual dom和diff算法
dom是浏览器实现的带有大量属性的对象,因此使用浏览器提供的api直接操作dom时会很消耗性能。
部分前端框架会在内存中维护一个或多个与真实dom节点一一对应的数据结构,这种数据结构被称为virtual dom,即虚拟dom。这种数据结构就是一个普通的js对象,具体到某种特定的virtual dom具体的实现并不尽相同,但每个virtual dom 都包含一些基本的属性,比如type, props, children。
virtual dom和最终渲染成dom是两个独立的步骤,作为一种简单的js对象,除了渲染成dom外,还可以用作输出json,convas等多种形式。
常见的虚拟dom实现有很多,比如最开始的virtual-dom,每种虚拟dom对应一种diff算法,相关演进如图
而虚拟dom的diff算法本身就是一种很耗性能的工作,不断地递归diff,最终才能找到特定需要更新的位置进行重新渲染 在这个前提下,于是出现了两种解决方式,一种是像前面提的svelte一样,直接放弃了virtual dom,同时也放弃了jsx地灵活性,另一种方式是像react这样在性能方面无法继续提高以后,而是在用户的感知上下了功夫,也就是通过引入fiber减少了用户对virtual dom处理时可能带来的卡顿,感觉上变快了。
3.4 数据管理
即状态管理方案,第一个前端的状态管理方案是facebook的flux,后续的状态管理工具沿用了类似的思路。
状态可以看成前端的mv*架构中的model,为了使model和view进行合理的同步,一个状态管理工具应具有以下两个特征:
- 数据流清晰,一个应用中的数据一般是多个组件共用的,状态的改变会影响多个部分,因此需要保证数据流的清晰,一般实现为单向数据流。
- 将状态与各对应组件进行绑定,以实现组件更新
一个状态管理工具通常会设置一个或多个用于数据管理的store。
更多可以参考 该如何通俗易懂的理解 Redux
3.5 前端路由
服务端路由在express文档是这么定义的
每个路由对应着一个请求的响应,该请求包含路径和方法两部分。在传统页面的导航时每次切换页面,客户端都需要向服务端请求新的页面,然后浏览器渲染新的页面,然后将会话历史保存起来方便向前/向后进行导航。
而当代web应用一般为单页面应用(SPA),导航时不需要向服务端请求新的页面,即可以使用切换视图(view)来实现,这种导航方式被称为前端路由,或者客户端路由,也是框架要解决的问题之一。
前端路由的本质是url为代表的相关状态到视图的映射关系,要保证传统路由的向前/向后导航正常使用,一般采取两种策略:hash模式和history api,前者是利用对hashchange
事件的监听,后者是利用history上的pushstate等方法修改会话历史,两者都可以做到无刷新修改url,且能修改会话历史,以实现服务端路由的效果。
3.6 开发环境问题
框架作为开发web应用的完整解决方案,应提供一套对应生态的合适开发环境,比如react的Create React App和vue的vue-cli,两者的打包器都是基于webpack,另外vue还提供了专门用于vue3的开发环境vitejs,打包器基于rollup,打包器相关参考这里。
以上工具的提供,极大降低了框架的使用门槛。
3.7 服务端渲染问题
在前端的spa被广泛使用时,其存在的问题仍然难以解决
- 单页面seo不友好,谷歌等搜索引擎无法对异步加载的页面信息进行收录
- 首屏显示时间,单页面的内容需要进一步解析后调用相关资源才能渲染
针对以上问题目前也有一些其他的解决方案,比如如果只对某些页面的以上问题关注时,可以采用预渲染,webpack的 prerender-spa-plugin插件可以解决这个问题,http/2的server push可以解决首屏显示的问题,但是对seo无能为力。
服务器渲染(SSR)通常是在服务端将virtual dom转化成html字符串,送到客户端时直接显示。vue的Nuxt.js和react的Next.js都提供了该种解决方案的更高层封装。
3.8 代码调试问题
当我们实际开发时,需要对开发中的状态和相关事件进行监控,以掌握开发状况和debug,为此,vue提供了vue-devtools,react提供了react-devtools。
完结撒花
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!