chenfei-hnu / Blog

个人整理的跟前端相关的文档 ( This is some front-end development related documentation )
9 stars 2 forks source link

前端入门知识 #27

Open chenfei-hnu opened 4 years ago

chenfei-hnu commented 4 years ago

概览:

  1. HTML/CSS/JS职责,浏览器如何解析
  2. 浏览器是怎么渲染页面的
  3. Vue这类框架都做了什么(前端模版引擎简介)
  4. Vue与传统jQuery区别(理解数据驱动与事件驱动)
  5. 前端工程化的简单介绍

前端页面解析

前端项目代码组成

我们打开一个前端项目,经常会看到很多不同后缀的文件,例如一个页面可能包括a.htmla.cssa.js,用了 Vue 还有a.vue,再加上 Typescript 可能还有a.ts 其实最终跑在浏览器中的代码,主要包括三种:HTML、CSS、Javascript,然后就是一些图片等静态资源

基础HTML

<html>
    <head></head>
    <body>
        <h1>我的第一个标题</h1>
        <p>我的第一个段落。</p>
    </body>
</html>

这里面包括两个子模快

<head>常包括控制样式的<link>标签、控制浏览器特殊逻辑的<meta>标签
<body>包括展示在页面的内容各个标签元素,控制代码执行逻辑的<script>代码块

通常来说,一段 HTML 代码,最终在浏览器中会生成一堆 DOM 节点树,例如:

<div>
    <a>123</a>
    <p>456<span>789</span></p>
</div>

这段代码在浏览器中渲染时,会先渲染外层数据节点,再逐层渲染内部节点

CSS

CSS 主要是给我们的 HTML 元素添加样式,可以通过几个方式匹配:

- 标签匹配:如`p{color: red}`会让所有p元素的文字都变成红色   
- class 匹配:类匹配,如`.color-red{color: red}`会让所有class="color-red"元素的文字都变成红色   
- id 匹配:id匹配,如`#color-red{color: red}`会让 id="color-red"的元素的文字都变成红色
(若页面内有多个相同的 id,则只有第一个生效)  

Javascript

HTML 是简单的网页静态信息,而 JavaScript 可以在网页上实现复杂的功能。

我们常常使用 Javascript 来做以下事情:

- 处理事件(点击、输入等)  
- 改变 HTML 内容、位置和样式  
- 处理 Http 请求、各种业务逻辑的执行  

Javascript 是单线程的,更多是因为对页面交互的同步处理。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作 DOM,若是多线程会导致严重的同步问题。

页面渲染

浏览器的渲染机制

我们现在知道一个页面的代码里,主要包括了 HTML、CSS、Javascript 三大块内容,那么浏览器是怎么解析和加载这些内容的呢?

一次浏览器的页面渲染过程中,浏览器会解析三种文件:

解析 HTML/SVG/XHTML,会生成一个 DOM 结构树
解析 CSS,会生成一个 CSS 规则树
解析 JS,可通过 DOM API 和 CSS API 来操作 DOM 结构树和 CSS 规则树

CSS 规则树与 DOM 结构树结合,最终生成一个 Render 树(即最终呈现的页面,例如其中会移除 DOM 结构树中匹配到 CSS 里面display:none的 DOM 节点),一般来说浏览器绘制页面的过程是:

1.创建Document对象并解析HTML生成DOM树,将解析到的节点添加到文档
2.加载CSS资源并解析生成CSS规则树,结合DOM树生成render树,进行页面渲染
3.遇到<script>标签就会停下来,等到下载执行完再继续向下渲染,如果是异步脚本defer aysnc则进行异步下载,并继续渲染
4.渲染过程中aysnc下载完毕则会立即执行该脚本,执行完毕后继续渲染
5.文档渲染完毕后,等待资源文件下载完成后,载入所有资源及并执行defer异步脚本

页面的局部刷新

我们的页面更多的不只是静态的页面,还会包括点击、拖拽等事件操作,以及接口请求、数据渲染到页面等动态的交互逻辑,这时候我们会需要更新页面的信息。

我们的业务代码中情况会复杂得多,除了插入内容,还包括内容更新、删除元素节点等。不管是、哪种情况,目前来说前端一般分为两种方式:

  1. 绑定映射关系
  2. 直接替换内容

1. 绑定映射关系

通过document提供的一些方法将DOM元素映射成JS对象,并使用这些JS对象通过JS内置API去操作DOM元素

var a = document.getElementById('a');

这里拿到了的这样一个元素映射,我们在更新内容、处理节点的时候就可以用这个映射来直接操作,如:

// 1. 更改元素里面内容
a.innerHTML = '<p>测试</p>'
// 2. 插入一个<a>元素
a.appendChild(document.createElement(`a`))
// 3. 删除第一个元素,在这里是前面的<p>测试</p>
a.removeChild(a.firstChild)

如果页面需要绑定变量的元素很多,那每次要更新某块的页面数据,需要保存很多元素映射的JS对象,同时需要调用很多的createElement()/appendChild()/removeChild()这类的原生方法 这种情况下,我们可以使用直接替换内容的方式

2. 直接替换内容

我们每次更新页面数据和状态,还可以通过innerHTML使用字符串拼接的方式来用新的HTML String替换旧的 例如,上面的几次更新 a 元素节点,可以调整成这样实现:

// 1. 更改元素里面内容
a.innerHTML = '<p>测试</p>'
// 2. 插入一个<a>元素
a.innerHTML = '<p>测试</p><a></a>'
// 3. 删除第一个元素,在这里是前面的<p>测试</p>
a.innerHTML = '<a></a>'

大量的元素更新不管是使用绑定映射关系还是使用直接替换内容,都会导致大量浏览器计算,很容易出现性能问题,导致页面卡顿等现象

页面重排、重绘

我们更新页面元素时,通常会触发浏览器的两种操作:Repaint(重绘) 和 Reflow(重排):
Repaint:页面部分重画,通常不涉及尺寸的改变,常见于颜色的变化 Reflow:意味着节点需要重新计算和绘制,常见于尺寸的改变 在 Reflow 的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成构造后,浏览器会重新绘制受影响的部分到屏幕中,该过程为 Repaint

重排的性能消耗跟render tree有多少节点需要重新构建有关系,单次操作使用innerHTML更改的节点数量更多会导致更多的开销 如果将多次操作合并为一次innerHTML,减少DOM操作及计算可能相较于绑定映射关系的方式效率更优,所以到底是使用绑定映射表方式,还是使用直接替换内容方式,都是需要按照实际场景具体问题具体分析的

编码方式的改变

事件驱动

事件驱动其实是前端开发中最容易理解的编码方式,例如我们写一个提交表单的页面,用事件驱动的方式来写的话,会是这样一个流程:

1.编写静态页面

<form>
    Name: <p id="name-value"></p>
    <input type="text" name="name" id="name-input" />
    Email: <p id="email-value"></p>
    <input type="email" name="email" id="email-input" />
    <input type="submit" />
</form>

2.给对应的元素绑定对应的事件,例如给 input 输入框绑定输入事件:

var nameInputEl = document.getElementById('name-input');
var emailInputEl = document.getElementById('email-input');
// 监听输入事件,此时 updateValue 函数未定义
nameInputEl.addEventListener('input', updateNameValue);
emailInputEl.addEventListener('input', updateEmailValue);

3.事件触发时,更新页面内容:

var nameValueEl = document.getElementById('name-value');
var emailValueEl = document.getElementById('email-value');
// 定义 updateValue 函数,用来更新页面内容
function updateNameValue(e){
    nameValueEl.innerText = e.srcElement.value;
}
function updateEmailValue(e){
    emailValueEl.innerText = e.srcElement.value;
}

以上这个流程,是很常见的前端编码思维,称之为事件驱动模式。

前端思维转变

前端思维转变,将事件驱动的思维模式是过去的常用模式,如今的前端开发过程中,多了很多的新框架、新工具,还有了工程化,带来了很多思维模式的变化

前端框架的出现

最初 AngularJS 占领了比较多的地位,后面 React 迎面赶上,Vue 再结合各种框架的优势,以及非常容易入门的文档,目前被更多人接受,下面以Vue来介绍这些新框架的概念

数据绑定

Vue 文本插值

在 Vue 中,最基础的模板语法是数据绑定,例如:

<div>{{ data }}</div>

这里绑定了一个 data的变量,开发者在 Vue 实例 data 中绑定该变量:

new Vue({
  data: {
    data: '测试文本'
  }
})

最终页面展示内容为

测试文本

数据绑定的实现

这种DOM中的值与JS中的变量进行绑定的方式,我们称之为数据绑定,数据绑定的过程如下:

1. 解析语法生成 AST(抽象语法树)
2. 根据 AST (抽象语法树)结果生成 DOM
3. 将数据绑定更新至模板

模板引擎完成上述过程,

{{ data }}
框架解析获得这样一个 AST(抽象语法树) 对象便于浏览器渲染:

thisDiv = {
    dom: {
        type: 'dom', ele: 'div', nodeIndex: 0, children: [
            {type: 'text', value: ''}
        ]
    },
    binding: [
        {type: 'dom', nodeIndex: 0, valueName: 'data'}
    ]
}

这样,我们在生成一个 DOM 的时候,同时添加对data的监听,数据更新时我们会找到对应的nodeIndex,并进行数值更新

虚拟 DOM

因为一个 DOM 节点包括了太多太多的属性、元素和事件对象,通常包括节点内容、元素位置、样式、节点的添加删除等方法,但是我们并不是全部都会用到 而我们通过用 JS 对象表示 DOM 元素的方式,大大降低了比较差异的计算量,所以数据变更时框架先使用虚拟DOM的JS对象进行对比更新 然后再按虚拟DOM进行浏览器渲染,以JS对象的比较更新替代频繁的DOM操作,将多次DOM操作合并成单次更新来提升性能

虚拟 DOM 大概是这么个过程:

1. 用 JS 对象模拟 DOM 树,得到一棵虚拟 DOM 树  
2. 当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异  
3. 把差异应用到真正的 DOM 树上 

XSS 漏洞

模板引擎还可以协助预防下 XSS 相关漏洞,XSS 的整个攻击过程大概为:

1. 通常页面中包含的用户输入内容都在固定的容器或者属性内,以文本的形式展示 
2. 攻击者利用页面的用户输入片段,拼接特殊格式的字符串,突破安全限制形成可执行的代码片段  
3. 攻击者通过在目标网站上注入脚本,使之在用户的浏览器上运行,从而引发潜在风险  

避免 XSS 的方法之一主要是将用户所提供的内容进行编码过滤,而大多数模板引擎会自带 HTML 转义功能 在 Vue 中,默认的数据绑定方式(双大括号、v-bind等)会进行 HTML 转义,将数据解释为普通文本,而非 HTML 代码

当然,如果你一定要输出 HTML 代码,也可以使用v-html指令输出,页面上渲染任意的HTML 可能会非常危险, 因为它很容易导致 XSS 攻击,请只对可信内容使用 HTML 插值,绝不要对用户自定义的输入内容使用插值

Vue简介

Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。

通过虚拟DOM再进行浏览器渲染的过程中我们可以实现一些功能:

- 排除无效 DOM 元素,并在构建过程可进行报错  
- 可方便地实现数据绑定、事件绑定等,具备自动更新页面的功能  
- 为虚拟 DOM Diff 过程打下铺垫  
- HTML 转义(预防 XSS 漏洞)  

使用框架自带的上述这些必要的功能,可以提升我们的开发效率,节省很多的工作量

对比其他框架,Vue易于上手这块,是大多数人都比较认可的,框架的性能也不错,这也是技术选型中比较重要的一些考虑,其他对比信息见 https://cn.vuejs.org/v2/guide/comparison.html https://github.com/introfei/Blog/issues/28

数据驱动

在 jQuery 年代,我们通常是使用事件驱动的模式去进行开发,使用了 Vue 之后,我们使用数据驱动的方式来进行编码,同样的们写一个提交表单的页面的流程:

1.设计与视图显示数据绑定的数据结构

// 包括一个 name 和 一个 email 的值
export default {
  data() {
    return {
      name: '',
      email: ''
    }
  }
}

2.把数据绑定到页面中需要展示的地方

<form>
    Name: <p>{{ name }}</p>
    <input type="text" name="name" v-bind:value="name" />
    Email: <p>{{ email }}</p>
    <input type="email" name="email" v-bind:value="email" />
    <input type="submit"  @click="onSubmit"/>
</form>

3.事件触发时,更新相应的显示数据

export default {
  data() {
    return {
      name: '',
      email: ''
    }
  },
  methods: {
    // 绑定 input 事件,获取到输入值,设置到对应的数据中
    onSubmit(){
      XXX
    }
  }
}

具体使用DOM操作的过程由框架自动完成

所以事件驱动和数据驱动一个很重要的区别在于,我们是从每个事件的触发开始设计我们的代码,还是以数据为中心,接收事件触发和更新数据状态的方式来编码。

页面抽象

使用数据驱动进行编码,我们经常需要对数据和视图进行抽象关联,例如我们现在要写一个列表,数据从后台获取到之后,展示到页面中

1.当我们需要渲染成列表时:

1. 事件驱动,直接将现有列表以DOM元素进行拼接输出
<ul id="ul"></ul>
<script>
const dom = $('#ul');
list.forEach(item => {
    dom.append(`<li data-id="${item.id}"><a href="">${item.href}</a></li>`)
});
</script>

2. 数据驱动,将列表数据与一个抽象的元素模板进行绑定,模板渲染由框架处理
<ul>
    <li v-for="item in list" :key="item.id"><a :href="">{{item.href}}</a></li>
</ul>

2.当我们需要更新一个列表中某个 id 的其中一个数据时:

// 1). jQuery + 事件驱动
const dom = $('#ul');
const id = 3;
dom.find(`li[data-id="${id}"] span`).text('newName3');

// 2). vue + 数据驱动
const id = 3;
list.find(item => item.id == 3).name == 'newName3';

在使用数据驱动的时候,模板渲染的事情会交给框架去完成,我们需要做的就是数据处理而已 当我们页面抽象成数据来表示之后,我们就可以将所有的页面元素、组件、展示内容和接口配置等都变成以配置数据的方式进行管理,使得代码变得更加清晰简洁

前端工程化

现在的主流构建工具是 Webpack,包括我们使用 Vue 官方脚手架生成的代码,构建工具也是 Webpack

我们在代码中会使用到很多的资源,图片、样式、代码,还有各式各样的依赖包,而打包的时候怎么实现按需加载、处理依赖关系、不包含多余的文件或内容, 同时提供开发和生产环境,包括本地调试自动更新到浏览器这些能力,都是由 Webpack 进行整合的

npm 依赖包

要实现工程化,前端开发离不开 nodejs 的包管理器 npm,在搭建本地开发服务以及打包编译前端代码等都会用到 在前端开发过程中,经常用到npm install来安装所需的依赖

我们在写代码的时候,为了可以将注意力集中到业务开发中,会需要使用别人开源的很多工具、框架和代码包 在很早以前,我们是一个个的下载,或是通过的方式去引用

当网站依赖的代码越来越多,管理依赖和版本是一件很麻烦的事情,我们就使用npm来对这些依赖进行集中管理

同时,我们可以在本地构建的时候,忽略依赖包里没用用到的部分,减小代码包的大小等

在安装 Node.js 的时候,npm 会一起安装,确认环境变量设置生效后,我们就可以在cmd或者VScode的终端栏中, 进入package.json文件所在的目录使用npm install 安装所有的依赖或使用npm install xxx安装某个依赖同时加入到配置文件中,具体组件使用时,在通过import的方式进行引入,针对实际应用场景在网上搜索对应实现的依赖包即可

脚手架

脚手架可以让你快速地生成示例代码,也可以更新依赖的版本等,避免开发者自行调整开发环境、打包逻辑等配置,帮我们完成前端工程化的处理并提供基础的项目编码结构

Vue CLI 致力于将 Vue 生态中的工具基础标准化,它确保各种构建工具能够基于智能的默认配置平稳衔接,让开发者专注于应用编码

使用方式很简单:

// 安装脚手架
npm install -g @vue/cli
// 脚手架生成 vue 项目,同时会自动安装依赖
vue create vue-cli-demo

启动项目

一般来说,每个项目根目录下都会有一个 README.md 文件,你能看到一些简单的项目说明及操作流程,例如:

# 安装项目依赖
yarn install

# 启动热加载服务器进行本地编码
yarn run serve

# 将项目的框架代码编译成发布要用到的js,css,html等Web基础文件
yarn run build

# 执行测试用例
yarn run test

yarn 跟 npm 都是包管理器,区别在于 yarn 在安装依赖时由于并行,离线等优化,速度更快,以及版本统一管理的比较好 yarn需要单独安装,同时也需要在确认环境变量生效后才能正常使用 部分公司可能有自己的包管理器,比如tnmp,来加载一些公司内部使用的依赖文件

部分项目 README缺失,可以查看或添加package.json文件的scripts部分的指令:

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "start": "vue-cli-service lint"
  }
}

执行上述提供的命令即可,如果是start命令,直接执行npm start即可,其他命令需要添加run关键字,如使用npm run server,执行 "vue-cli-service serve"

从jQuery到Vue React过渡时间出现的一些新知识

在 2016 年学 JavaScript 是一种什么样的体验 https://zhuanlan.zhihu.com/p/22782487