现代前端技术解析读书笔记

思维导图链接:http://v3.processon.com/view/link/5f7ec592762131119546c899

取材自《现代前端技术解析》

本文只是个人读书笔记,更多详细内容请查看原书。

 

前端技术解析

 

  • Web前端技术基础
      • 用户界面包括浏览器可见的地址输入框、浏览器前进返回按钮、打开书签、打开历史记录等用户可操作的功能选项。
      • 浏览器引擎可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分间相互通信的核心。
      • 浏览器渲染引擎(排版引擎)的功能是解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面。
      • 网络功能模块是浏览器开启网络线程发送请求或下载资源文件的模块。
      • UI后端用于绘制基本的浏览器窗口内控件,如组合选择框、按钮等。
      • JS引擎是浏览器解释和执行JS脚本的部分,如V8引擎。
      • 浏览器数据持久化存储涉及cookie、localStorage等一些客户端存储技术,可以通过浏览器引擎提供的API进行调用。
    • 解析HTML构建DOM树时渲染引擎会先将HTML元素标签解析成由多个DOM元素对象节点组成的且具有节点父子关系的DOM树结构,然后根据DOM树结构的每个节点顺序提取计算使用的CSS规则并重新计算DOM树结构的样式数据,生成一个带样式描述DOM渲染树对象。DOM渲染树生成结束后,进入渲染树的布局阶段,即根据每个渲染树节点在页面中的大小和位置,将节点固定到页面的对应位置上,这个阶段主要是元素的布局属性(如position、float、margin)生效,即在浏览器中绘制页面上元素节点的位置。接下来就是绘制阶段,将渲染树节点的背景、颜色、文本等样式信息应用到每个节点上,这个阶段主要是元素的内部显示样式(如color、background、text-shadow)生效,最终完成整个DOM在页面上的绘制显示。
    • 浏览器数据持久化存储技术
      • HTTP文件缓存
        • HTTP文件缓存是基于HTTP协议的浏览器端文件级缓存机制。在文件重复请求的情况下,浏览器可以根据HTTP响应的协议头信息判断是从服务器端请求文件还是从本地读取文件。
      • LocalStorage
      • SessionStorage
      • indexDB
        • IndexDB是一个可在客户端存储大量结构化数据并且能在这些数据上使用索引进行高性能检索的一套API。
      • Web SQL
      • Cookie
        • Cookie指网站为了辨别用户身份或Session跟踪而储存在用户浏览器端的数据。Cookie信息一般会通过HTTP请求发送到服务器端。一条Cookie记录主要由键、值、域、过期时间和大小组成,一般用于保存用户的网站认证信息。
        • Cookie设置中有个HTTPOnly参数,前端浏览器使用document.cookie是读取不到HTTPOnly类型Cookie的,被设置为HttpOnly的Cookie记录只能通过HTTP请求头发送到服务器端进行读写操作,这样就避免了服务器端的Cookie记录被前端JS修改,保证了服务器端验证Cookie的安全性。
      • CacheStorage
        • CacheStorage是在ServiceWorker规范中定义的,可用于保存每个ServiceWorker声明的Cache对象,是未来可能用来代替Application Cache的离线方案。
        • CacheStorage在浏览器端未windows下的全局内置对象caches:caches.has();       // 检查如果包含Cache对象,则返回一个promise对象caches.open();     // 打开一个Cache对象,并返回一个promise对象caches.delete();   // 删除Cache对象,成功则返回一个promise对象,否则返回falsecaches.keys();     // 含有keys中字符串的任意一个,则返回一个promise对象caches.match();   // 匹配key中含有该字符串的cache对象,返回一个promise对象
      • Application Cache
        • Application Cache是一种允许浏览器通过mainfest配置文件在本地有选择性地存储JS、CSS、图片等静态资源的文件级缓存机制。当页面不是首次打开时,通过一个特定的mainfest文件配置描述来选择读取本地Application Cache里面的文件。
        • 优势
          • 1. 离线浏览
          • 2. 快速加载
          • 3. 服务器负载小
            • 只有在文件资源更新时,浏览器才会从服务器端下载,这样就减小了服务器资源请求的压力。
        • 问题
          • 1. Application Cache已经开始被标准弃用,渐渐将会由ServiceWorkers来代替,所以现在不建议使用Application Cache来实现离线应用,仅作为一种技术了解即可。
          • 2. Application Cache仍不能兼容目前全部主流的浏览器环境,即使是在移动端。
          • 3. Application Cache为站点离线存储提供的容量限制是5MB,现在来说显然不适用。
          • 4. 如果mainfest文件或内部列表中的某一个文件不能正常下载,整个更新过程将被视为失败,浏览器将继续使用旧的缓存。
          • 5. 引用mainfest的HTML、缓存列表的静态资源必须与mainfest文件同源,即保持在同一个域下。
          • 6. 站点中的其他页面即使没有设置mainfest属性,请求的资源也会从缓存中访问。
          • 7. 当mainfest文件发生改变时,资源请求本身也会触发更新。
      • Flash缓存
  • 前端与协议
    • web安全
      • XSS(Cross Site Script,跨站脚本攻击)
        • XSS通常是由带有页面可解析内容的数据未经处理直接插入到页面上解析导致的。
        • 根据攻击脚本的引入位置分类
          • 存储型XSS
            • 存储型XSS的攻击脚本常是由前端提交的数据未经处理直接存储到数据库然后从数据库中读取出现后又直接插入到页面中所导致的。
            • <div>{{ content }}</div>
          • 反射型XSS
            • 反射型XSS可能是在网页URL参数中注入了可解析内容的数据而导致的,如果直接获取URL中不合法的并插入页面中则可能出现页面上XSS攻击
            • let name = req.query['name'];this.body = `<div>${name}></div>`;
          • DOM XSS
            • MXSS则是在渲染DOM属性时将攻击脚本插入DOM属性中被解析而导致的。
            • <p class="class-a {{b}}"></p>
        • XSS主要的防范方法是验证输入到页面上所有内容来源是否安全,如果可能含有脚本标签等内容则需要进行必要的转义。
      • SQL注入
      • CSRF(Cross-site Request Forgery,跨站请求伪造)
        • CSRF是指非源站点按照源站点的数据请求格式提交非法数据给源站点服务器的一种攻击方法。非源站点在取到用户登录验证信息的情况下,可以直接对源站点的某个数据接口进行提交,如果源站点对该提交请求的数据来源未经验证,该请求可能被成功执行。通常比较安全的是通过页面Token提供验证的方式来验证请求是否为源站点页面提交的,来阻止跨站伪请求的发生。
    • 网络劫持
      • 网络劫持一般指网络资源请求在请求过程中因为人为的攻击导致没有加载到预期的资源内容。
      • DNS劫持
        • DNS劫持是指攻击者劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致用户对该域名地址的访问由原IP地址转入到修改后的指定IP地址的现象,其结果就是让正确的网址不能解析或被解析指向另一网站IP,实现获取用户资料或破坏原有网站正常服务的目的。
        • DNS劫持一般通过篡改DNS服务器上的域名解析记录,来返回给用户一个错误的DNS查询结果实现。
      • HTTP劫持
        • HTTP劫持指在用户浏览器与访问的目的服务器之间所建立的网络数据传输通道中从网关或防火墙层上监视特定数据信息,当满足一定的条件时,就会在正常的数据包中插入或修改成为攻击者设计的网络数据包,目的是让用户浏览器解释“错误”的数据,或以弹出新窗口的形式在使用者浏览器界面上展示宣传性广告或直接显示某块其他的内容。
      • 请求劫持唯一可行的预防方法就是尽量使用HTTPS协议来访问目标网站。
  • 前端三层结构与应用
    • 桌面浏览器器端推荐使用JS直接实现动画的方式或SVG动画的实现方式,移动端则可以考虑使用CSS3 transition、CSS3 animation、canvas或requestAnimationFrame。
    • 1rem的计算
      • 1rem=屏幕宽度*屏幕分辨率/10=屏幕宽度的10%
      • 1rem=屏幕宽度/设计稿屏幕宽度*10
  • 现代前端交互框架
    • 页面路由实现思路:让URL地址内容匹配对应的字符串然后进行相应的操作。
    • ```
    • const router = {
    •     get(match, fn) {
    •         let url = location.href,
    •             routeReg = new RegExp(match, 'g');
    •         if (routeReg.test(url)) {
    •             fn();
    •         }
    •         return this;
    •     }
    • };
    •  
    • router.get('#index', function () {
    •     _loadIndex(); // 注册hash含有#index的路由执行对应的操作
    • }).get('#detail', function () {
    •     _loadDetail(); // 注册hash含有#detail的路由执行对应的操作
    • });
    • ```
    • 可以使用html5的pushState来实现路由:history.pushState(state,title,url)可以改变当前页面的url而不发生跳转,并将不同的state数据和对应的url对应起来。如果页面显示的内容是根据不同的数据状态来自动完成的,这样根据state的内容来加载不同的组件就很有用了。history.pushState({page:'A'}, 'page A','a.html'};
    • 主流MVC框架的组件定义
      • ```
      •  
      •  
      • // 可能有一个公用的Component基类
      • let component = new Component();
      •  
      • let A = component.extend({
      •     $el: document.getElementById('A'),
      •     model: {
      •         text: 'ViewA渲染完成',
      •     },
      •     view(data) {
      •         let template = '{{text}}';
      •         // 调用模板渲染数据获取HTML片段
      •         let html = render(template, data);
      •         this.$el.innerHTML = html;
      •     },
      •     controller() {
      •         let self = this;
      •         // 调用model数据传入view中渲染内容
      •         self.view(self.model);
      •         // 用户操作一般通过Hash来触发Controller改变Model和View
      •         $('window').on('hashchange', function () {
      •             self.model.text = location.hash;
      •             self.view(self.model);
      •         });
      •  
      •         // 点击事件可以直接触发Model改变并重新渲染View
      •         self.event['change'] = function () {
      •             self.model.text = '新的ViewA渲染完成';
      •             self.view(self.model);
      •         };
      •     }
      • });
      • ```
    • Presenter作为中间部分连接Model和View的通信交互完成所有的逻辑操作,但这样Presenter层的内容就可能变得很重了。另外用户在View上的操作会反馈到Presenter中进行Model修改,并更新其他对应部分的View内容。
    • ```
    •  
    •  
    • // 可能有一个公用的Component基类
    • let component = new Component();
    •  
    • let A = component.extend({
    •     $el: document.getElementById('A'),
    •     model: {
    •         text: 'ViewA渲染完成',
    •     },
    •     view: '{{text}}',
    •     presenter() {
    •         let self = this;
    •  
    •         // 调用模板渲染数据获取HTML片段
    •         let html = render(self.view, self.model);
    •         self.$el.innerHTML = html;
    •  
    •         // View上的改变将通知Presenter改变Model和其他的View
    •         $('#input').on('change', function () {
    •             self.model.text = this.value;
    •             html = render('{{text}}', self.model);
    •             $('#showText').html(html);
    •         });
    •  
    •         // 点击事件可以直接触发Model改变并重新渲染View
    •         self.event['change'] = function () {
    •             self.model.text = '新的ViewA渲染完成';
    •             html = render('{{text}}', self.model);
    •             $('#showText').html(html);
    •         };
    •     }
    • });
    • ```
    • MVVM设计的一个很大的好处是将MVP中Presenter的工作拆分成多个小的指令步骤,然后绑定到相对应的元素中,根据相对应的数据变化来驱动触发,自动管理交互操作,同时也免去了查看Presenter中事件列表的工作,而且一般ViewModel初始化时会自动进行数据绑定,并将页面中所有的同类操作复用,大大节省了我们自己进行内容渲染和事件绑定的代码量。
    • ```
    •  
    •     
    •  
    •  
    • let viewModel = new VM({
    •     $el: document.getElementById('A'),
    •     data: {
    •         text: 'ViewA渲染完成'
    •     },
    •     method: {
    •         change() {
    •             this.text = '新的ViewA渲染完成';
    •         }
    •     }
    • });
    • ```
    • 实现数据变更检测的方法主要有手动触发绑定、脏数据检测、对象劫持、Proxy等。
      • 手动触发绑定主要思路是通过在数据对象上定义get()方法和set()方法(也可以使用其他命名方法),调用时手动触发get()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的重新渲染功能。
      • ```
      •  
      •  
      •  
      •     
      •     手动触发绑定
      •  
      •  
      •  
      •  
      •  
      •     let elems = [document.getElementById('el'), document.getElementById('input')];
      •     let data = {
      •         value: 'hello'
      •     };
      •     // 定义Directive
      •     let directive = {
      •         text: function (text) {
      •             this.innerHTML = text;
      •         },
      •         value: function (value) {
      •             this.setAttribute('value', value);
      •         }
      •     };
      •  
      •     // 数据绑定监听
      •     if (document.addEventListener) {
      •         elems[1].addEventListener('keyup', function (e) {
      •             ViewModelSet('value', e.target.value);
      •         }, false);
      •     } else {
      •         elems[1].attachEvent('onkeyup', function (e) {
      •             ViewModelSet('value', e.target.value);
      •         }, false);
      •     }
      •  
      •     // 开始扫描节点
      •     scan();
      •     // 设置页面2秒后自动改变数据更新视图
      •     setTimeout(function () {
      •         ViewModelSet('value', 'hello ouvenzhang');
      •     }, 1000);
      •  
      •     function scan() {
      •         // 扫描带指令的节点属性
      •         for (let elem of elems) {
      •             elem.directive = [];
      •             for (let attr of elem.attributes) {
      •                 if (attr.nodeName.indexOf('q-') >= 0) {
      •                     // 调用属性指令
      •                     directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      •                     elem.directive.push(attr.nodeName.slice(2));
      •                 }
      •             }
      •         }
      •     }
      •  
      •     // 设置数据改变后扫描节点
      •     function ViewModelSet(key, value) {
      •         data[key] = value;
      •         scan();
      •     }
      •  
      •  
      •  
      • ```
      • 脏数据检测的基本原理是在ViewModel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后再比较数据变化,如果变化则进行Directive指令调用,对这个元素进行重新扫描渲染。脏数据检测只针对可能修改的元素进行扫描,提高了ViewModel内容变化后扫描视图渲染的效率。
      • ```
      •  
      •  
      •  
      •     
      •     data-binding-drity-check
      •  
      •  
      •  
      •  
      •  
      •     let elems = [document.getElementById('el'), document.getElementById('input')];
      •     let data = {
      •         value: 'hello'
      •     };
      •     // 定义Directive
      •     let directive = {
      •         text: function (text) {
      •             this.innerHTML = text;
      •         },
      •         value: function (value) {
      •             this.setAttribute('value', value);
      •         }
      •     };
      •  
      •     // 初始化扫描节点
      •     scan(elems);
      •     $digest('value');
      •  
      •     // 输入框数据绑定监听
      •     if (document.addEventListener) {
      •         elems[1].addEventListener('keyup', function (e) {
      •             data.value = e.target.value;
      •             $digest(e.target.getAttribute('q-bind'));
      •         }, false);
      •     } else {
      •         elems[1].attachEvent('onkeyup', function (e) {
      •             data.value = e.target.value;
      •             $digest(e.target.getAttribute('q-bind'));
      •         }, false);
      •     }
      •  
      •     // 设置页面2秒后自动改变数据更新视图
      •     setTimeout(function () {
      •         data.value = 'hello ouvenzhang';
      •         // 执行$digest方法来启动脏检测
      •         $digest('value');
      •     }, 1000);
      •  
      •     function scan(elems) {
      •         // 扫描带指令的节点属性
      •         for (let elem of elems) {
      •             elem.directive = [];
      •         }
      •     }
      •  
      •     // 可以理解为数据劫持监听
      •     function $digest(value) {
      •         let list = document.querySelectorAll('[q-bind=' + value + ']');
      •         digest(list);
      •     }
      •  
      •     // 脏数据循环检测
      •     function digest(elems) {
      •         // 扫描带指令的节点属性
      •         for (let elem of elems) {
      •             for (let attr of elem.attributes) {
      •                 if (attr.nodeName.indexOf('q-event') >= 0) {
      •                     // 调用属性指令
      •                     let dataKey = elem.getAttribute('q-bind') || undefined;
      •                     // 进行脏数据检测,如果数据改变,则重新执行指令,否则跳过
      •                     if (elem.directive[attr.nodeValue] !== data[dataKey]) {
      •                         directive[attr.nodeValue].call(elem, data[dataKey]);
      •                         elem.directive[attr.nodeValue] = data[dataKey];
      •                     }
      •                 }
      •             }
      •         }
      •     }
      •  
      •  
      •  
      •  
      • ```
      • 数据劫持基本思路是使用Object.defineProperty和Object.defineProperties对ViewModel数据对象进行属性get()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就可以了。
      • ```
      •  
      •  
      •  
      •     
      •     data-binding-hijacking
      •  
      •  
      •  
      •  
      •  
      •     let elems = [document.getElementById('el'), document.getElementById('input')];
      •     let data = {
      •         value: 'hello'
      •     };
      •     // 定义Directive
      •     let directive = {
      •         text: function (text) {
      •             this.innerHTML = text;
      •         },
      •         value: function (value) {
      •             this.setAttribute('value', value);
      •         }
      •     };
      •  
      •     let bValue;
      •     // 开始扫描节点
      •     scan();
      •  
      •     // 可以理解为数据劫持监听
      •     defineGetAndSet(data, 'value');
      •  
      •     // 数据绑定监听
      •     if (document.addEventListener) {
      •         elems[1].addEventListener('keyup', function (e) {
      •             data.value = e.target.value;
      •         }, false);
      •     } else {
      •         elems[1].attachEvent('onkeyup', function (e) {
      •             data.value = e.target.value;
      •         }, false);
      •     }
      •  
      •  
      •     // 设置页面2秒后自动改变数据更新视图
      •     setTimeout(function () {
      •         data.value = 'hello ouvenzhang';
      •     }, 1000);
      •  
      •     function scan() {
      •         // 扫描带指令的节点属性
      •         for (let elem of elems) {
      •             elem.directive = [];
      •             for (let attr of elem.attributes) {
      •                 if (attr.nodeName.indexOf('q-') >= 0) {
      •                     // 调用属性指令
      •                     directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      •                     elem.directive.push(attr.nodeName.slice(2));
      •                 }
      •             }
      •         }
      •     }
      •  
      •     // 定义对象属性设置劫持
      •     function defineGetAndSet(obj, propName) {
      •         Object.defineProperty(obj, propName, {
      •             get: function () {
      •                 return bValue;
      •             },
      •             set: function (newValue) {
      •                 bValue = newValue;
      •                 scan();
      •             },
      •             enumerable: true,
      •             configurable: true
      •         });
      •     }
      •  
      •  
      •  
      • ```
      • Proxy
      • ```
      •  
      •  
      •  
      •     
      •     data-binding-proxy
      •  
      •  
      •  
      •  
      •  
      •     let elems = [document.getElementById('el'), document.getElementById('input')];
      •  
      •     // 定义Directive
      •     let directive = {
      •         text: function (text) {
      •             this.innerHTML = text;
      •         },
      •         value: function (value) {
      •             this.setAttribute('value', value);
      •         }
      •     };
      •  
      •     let data = new Proxy({}, {
      •         get: function (target, key, receiver) {
      •             return target.value;
      •         },
      •         set: function (target, key, value, receiver) {
      •             target.value = value;
      •             scan();
      •             return target.value;
      •         }
      •     });
      •  
      •     data['value'] = 'hello';
      •  
      •     // 数据绑定监听
      •     if (document.addEventListener) {
      •         elems[1].addEventListener('keyup', function (e) {
      •             data.value = e.target.value;
      •         }, false);
      •     } else {
      •         elems[1].attachEvent('onkeyup', function (e) {
      •             data.value = e.target.value;
      •         }, false);
      •     }
      •  
      •  
      •     // 设置页面2秒后自动改变数据更新视图
      •     setTimeout(function () {
      •         data.value = 'hello ouvenzhang';
      •     }, 1000);
      •  
      •     function scan() {
      •         // 扫描带指令的节点属性
      •         for (let elem of elems) {
      •             elem.directive = [];
      •             for (let attr of elem.attributes) {
      •                 if (attr.nodeName.indexOf('q-') >= 0) {
      •                     // 调用属性指令
      •                     directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
      •                     elem.directive.push(attr.nodeName.slice(2));
      •                 }
      •             }
      •         }
      •     }
      •  
      •  
      •  
      • ```
    • Virtual DOM交互模式
      • Virtual DOM设计理念
        • MVVM的前端交互模式大大提高了编程效率,自动双向数据绑定让我们可以将页面逻辑实现的核心转移到数据层的修改操作上,而不再是在页面中直接操作DOM。但实际上,尽管MVVM改变了前端开发的逻辑方式,但是最终数据层反应到页面上View层的渲染和改变仍是通过对应的指令进行DOM操作来完成的,而且通常一次ViewModel的变化可能会触发页面上多个指令操作DOM的变化,带来大量的页面结构层DOM操作或渲染。
        • Virtual DOM是一个能够直接描述一段HTML DOM结构的JS对象,浏览器可以根据它的结构按照一定规则创建出确定唯一的HTML DOM结构。整体来看,Virtual DOM的交互模式减少了MVVM或其他框架中对DOM的扫描或操作次数,并且在数据发生改变后只在合适的地方根据JS对象来进行最小化的页面DOM操作,避免大量重新渲染。
      • VIrtual DOM核心实现
        • 使用VM模式来控制页面DOM结构更新的过程:创建原始页面或组件的VM结构,用户操作后需要进行DOM更新时,生成用户操作后页面或组件的VM结构并与之前的结构进行对比,找到最小变化VM的差异化描述对象,最后把差异化的VM根据特定的规则渲染到页面上。
        • 步骤
          • 1. 创建Virtual DOM
            • 创建VIrtual DOM即把一段HTML字符串文本解析成一个能够描述它的JS对象。
            • 通过浏览器提供的DOM API扫描这段DOM的节点,遍历它的属性,然后添加到JS对象上即可。这样创建Virtual DOM会直接失去VIrtual DOM的优势,它是为了避免直接进行DOM操作而设计的。我们不能通过浏览器DOM API扫描去生成JS对象,因为扫描过程本身使用到DOM的读取操作,这个过程很慢。
            • 逐个分析HTML字符串中的字符,根据词法分析内容,将标签名存为tagName,属性存入attributes,子标签内容存入children。这样,就通过JS直接分析HTML字符串文本来生成VIrtual DOM,比DOM API操作要快。
            • 根据HTML字符串解析创建VIrtual DOM的过程相当于实现了一个HTML文本解析器,但是没有生成DOM对象树,只是生成了一个操作效率更高的JS对象,因此通常不会直接将HTML交给浏览器去解析,因为浏览器的DOM解析很慢,这也是VIrtual DOM交互模式和普通DOM编程最本质的区别。
            • 完成之后再通过VIrtual DOM进行渲染生成一个真实的DOM操作就比较简单了。
            • ```
            • const render = function (virtualDOM) {
            •     let element = document.createElement(virtualDOM.tagName);
            •     let attributes = virtualDOM.attributes;
            •  
            •     // 设置节点的DOM属性
            •     for (let key in attributes) {
            •         element.setAttribute(key, attributes[key])
            •     }
            •  
            •     let children = virtualDOM.children || [];
            •  
            •     for (let child of children) {
            •         // 如果是字符串则直接插入字符串,否则构建子节点
            •         let childNode = (typeof children === 'string') ? document.createTextNode(child) : render(child);
            •         element.appendChild(childNode)
            •     }
            •     return element;
            • };
            • ```
          • 2. 对比两个Virtual DOM生成差异化Virtual DOM
            • Virtual DOM的对比算法实际上是对于多叉树结构的遍历算法。
            • 可以对VIrtual DOM中的每个节点添加一个唯一的字母id,那么两个VIrtual DOM的节点顺序分别使用深度优先遍历算法表示为ABEFCGHDIJ和AKLMBEFCGHDIJ,这样我们很容易分析出需要在A和B之间进行插入操作KLM节点,再根据KLM的关系,可以知道只需要插入完整的K节点即可。使用广度优先算法遍历的思路也是类似的,遍历出两个VIrtual DOM节点属性为ABCDEFGHIJ和AKBCDLMEFGHIJ,不过稍微不同的是,这种情况下检测到有两处插入,需要进一步判断来合并操作,优化差异树的结构。
            • 在VIrtual DOM的对比过程中,除了节点改变的内容,还需要继续记录发生差异化改变的类型和位置,例如是针对具体哪一个元素的增加、替换、删除操作等。
          • 3. 将差异化Virtual DOM渲染到页面上
      • 与以前交互模式相比,VIrtual DOM最本质的区别在于减少了对DOM对象的操作,通过JS对象来代替DOM对象树,并且在页面结构改变时进行最小代价的DOM渲染操作,提高了交互的性能和效率。这就是VM交互模式的优势,也是提高前端交互性能的根本原因。
      •  
      • MNV*框架端的主要任务是解析Model、ViewModel或VIrtual DOM组成JSBridge协议串并发送,而Native端的实现将会比较复杂,需要处理不同的标签元素解析,还可能需要处理事件的绑定等,即将JS的事件通过Native事件来实现。整体上像是使用移动端原生的方式来解析HTML上需要实现的应用功能。
      • MNV*的基本原理是将JSBridge和DOM编程的方式进行结合,让前端能够快速构建开发原生界面的应用,从而脱离DOM的交互模式。
  • 前端项目与技术实践
    • 前端规范
      • 前端页面开发应做到结构层(HTML)、表现层(CSS)、行为层(JS)分离 ,保证它们之间的最小耦合。移动端开发可以适当地进行CSS样式、图片资源、JS内联,内联的资源大小标准一般为2KB以内,否则可能会导致HTML文件过大,页面首次加载时间过长。
      • head中必须定义title、keyword、description,保证基本的SEO页面关键字和内容描述。移动端页面head要添加viewpoint控制页面不缩放,有利于提高页面渲染性能。
      • CSS样式书写顺序遵循先布局后内容的规则。
    • 设计一个高效的组件化规范应该解决的问题
      • 组件之间独立、松耦合。组件之间的HTML、JS、CSS之间相互独立,尽量不重复,相同部分通过父级或基础组件来实现,最大限度减少重复代码。
      • 组件间嵌套使用。组件可以嵌套使用,但嵌套后仍然是独立、送耦合的。
      • 组件间通信。主要指组件之间的函数调用或通信,例如A组件完成某个操作后希望B组件执行某个行为,这种情况就可以使用监听或观察者模式在B组件中注册该行为的事件监听或加入观察者,然后选择合适的时机在A组件中触发这个事件监听或通知观察者来触发B组件中的行为操作,而不是在A组件中直接拿到B组件的引用并直接进行操作,因为这样组件之间的行为就会产生耦合。
      • 组件公用部分设计。组件的公用部分应该被抽离出来形成基础库,用来增加代码的复用性。
      • 组件的构建打包。构建工具能够自动解析和打包组件内容。
      • 异步组件的加载模式。在移动端,通常考虑到页面首屏,异步的场景应用非常广泛,所有异步组件不能和同步组件一起处理。这时可以将异步组件区别于普通组件的目录存放,并在打包构建时进行异步打包处理。
      • 组件继承与复用性。对于类似的组件要做到基础组件复用来减少重复编码。
      • 私有组件的统一管理。为了提高协作效率,可以通过搭建私有源的方式来统一管理组件库,例如使用包管理工具等。但这点即使在大的团队里面也很难实施,因为业务组件的实现常常需要定制化而且经常变更,这样维护组件库成本反而更大,目前可以做的是将公用的组件模块使用私有源管理起来。
      • 根据特定场景进行扩展或自定义。如果当前的组件框架不能满足需求,我们应该能够很便捷地拓展新的框架和样式,这样就能适应更多的场景需求。如在通过目录管理组件的方案下,既可以使用MVVM框架进行开发,也可以使用VIrtual DOM框架进行开发,但要保持基本的规范结构不变。
    • 前端性能测试
      • Performance Timing API
        • Performance Timing API是一个支持IE9以上版本及WebKit内核浏览器中用于记录页面加载和解析过程中关键时间点的机制,它可以详细记录每个页面资源从开始加载到解析完成这一过程中具体操作发生的时间点,这样根据开始和结束时间戳就可以计算出这个过程所花的时间了。
        • 浏览器中加载和解析一个HTML文件的详细过程先后经历unload、redirect、App Cache、DNS、TCP、Request、Response、Processing、onload几个阶段,每个过程开始和结束的关键时间戳浏览器已经使用performance.timing来记录了,所以根据这个记录并结合简单的计算,我们就可以得到页面中每个过程所消耗的时间。
        • ```
        • function performanceTest() {
        •     let timing = performance.timing,
        •         readyStart = timing.fetchStart - timing.navigationStart,
        •         redirectTime = timing.redirectEnd - timing.redirectStart,
        •         appcacheTime = timing.domainLookupStart - timing.fetchStart,
        •         unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart,
        •         lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart,
        •         connectTime = timing.connectEnd - timing.connectStart,
        •         requestTime = timing.responseEnd - timing.requestStart,
        •         initDomTreeTime = timing.domInteractive - timing.responseEnd,
        •         domReadyTime = timing.domComplete - timing.domInteractive,
        •         loadEventTime = timing.loadEventEnd - timing.loadEventStart,
        •         loadTime = timing.loadEventEnd - timing.navigationStart;
        •  
        •     console.log('准备新页面时间耗时:' + readyStart);
        •     console.log('redirect重定向耗时:' + redirectTime);
        •     console.log('Appcache耗时:' + appcacheTime);
        •     console.log('unload前文档耗时:' + unloadEventTime);
        •     console.log('DNS查询耗时:' + lookupDomainTime);
        •     console.log('TCP连接耗时:' + connectTime);
        •     console.log('request请求耗时:' + requestTime);
        •     console.log('请求完毕至DOM加载:' + initDomTreeTime);
        •     console.log('解析DOM树耗时:' + domReadyTime);
        •     console.log('load事件耗时:' + loadEventTime);
        •     console.log('加载时间耗时:' + loadTime);
        • }
        • ```
        • performance.memory                // 内存占用的具体数据performance.now()                    // 返回当前网页自performance.timing到现在的时间,可以精确到微妙,用于更加精确的计数。performance,getEntries()           // 获取页面所有加载资源的performance timing情况。浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等)发出一个HTTP请求。此方法以数组形式返回所有请求的时间统计信息。performance.navigation             // 提供用户行为信息,如网络请求的类型和重定向次数等performance.navigation.redirectCount  // 记录当前网页重定向跳转的次数
      • Profile工具
        • Performance Timing API描述了页面资源从加载到解析各个阶段的执行关键点时间记录,但是无法统计JS执行过程中系统资源的占用情况。Profile是Chrome和Firefox等标准浏览器提供的一种用于测试页面脚本运行时系统内存和CPU资源占用情况的API。
        • 可实现功能
          • 1. 分析页面脚本执行过程中最耗资源的操作
          • 2. 记录页面脚本执行过程中JS对象消耗的内存与堆栈的使用情况
          • 3. 检测页面脚本执行过程中CPU占用情况
        • 使用console.profile()和console.profileEnd()就可以分析中间一段代码执行时系统的内存或CPU资源的消耗情况,然后配置浏览器的Profile查看比较消耗系统内存或CPU资源的操作,这样就可以有针对性地进行优化了。
      • 页面埋点计时
        • 使用Profile可以在一定程度上帮助我们分析页面的性能,但缺点是不够灵活。实际项目中,我们不会过多关注页面内存或CPU资源的消耗情况,因为JS有自动内存回收机制。我们更多关注的是页面脚本逻辑执行的时间。除了Performance Timing的关键过程耗时计算,我们还希望检测代码的具体解析或执行时间,这就不能写很多的console.profile()和console.profileEnd()来逐段实现,为了更加简单地处理这种情况,往往选择通过脚本埋点计时的方式来统计每部分代码的运行时间。
        • 页面JS埋点计时的实现:记录JS代码开始执行的时间戳,在需要记录的地方埋点记录结束时的时间戳,最后通过差值来计算一段HTML解析或JS解析执行的时间。可以将某个操作开始和结束的时间戳记录到一个数组中,然后分析数组之间的间隔就得到每个步骤的执行时间。
        • ```
        • let timeList = [];
        •  
        • function addTime(tag) {
        •     timeList.push({"tag": tag, "time": +new Date()});
        • }
        •  
        • addTime("loading");
        • timeList.push({"tag": "load", "time": +new Date()});
        • // TODO, load加载时的操作
        • timeList.push({"tag": "load", "time": +new Date()});
        •  
        • timeList.push({"tag": "process", "time": +new Date()});
        • // TODO,process处理时的操作
        • timeList.push({"tag": "process", "time": +new Date()});
        •  
        • // 输出{load: 时间毫秒数, process: 时间毫秒数}
        • parseTime(timeList);
        •  
        • function parseTime(time) {
        •     let timeStep = {},
        •         endTime;
        •     for (let i = 0, len = time.length; i = 0 && endTime.tag) {
        •             timeStep[endTime.tag] = endTime.time - time[i].time;
        •         }
        •     }
        •     return timeStep;
        • }
        • ```
      • 资源加载时序图分析
        • 该方法可以粗粒度地宏观分析浏览器的所有资源文件请求耗时和文件加载顺序情况,如保证CSS和数据请求等关键资源优先加载,JS文件和页面中非关键性图片等内容延后加载。
    • 移动端浏览器前端优化策略
      • 网络加载类
        • 1. 首屏数据请求提前,避免JS文件加载后才请求数据
        • 2. 首屏加载和按需加载,非首屏内容滚屏加载,保证首屏内容最小化
          • 一般推荐移动端页面首屏数据展示延时最长不超过3秒。
        • 3. 模块化资源并行下载
        • 4. Inline首屏必备的CSS和JS
        • 5. meta dns prefetch设置DNS预解析
          • 设置文件资源的DNS预解析,让浏览器提前解析获取静态资源的主机IP,避免等到请求时才发起DNS解析请求。
          • <!--cdn域名预解析--><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//cdn.domain.com">
        • 6. 资源预加载
        • 7. 合理利用MTU策略
          • 通常情况下,TCP网络传输的最大传输单元MTU为1500B,即一个RTT(网络请求往返时间)内可以传输的数据量最大为1500字节。因此,在前后端分离的开发模式中,尽量保证页面的HTML内容在1KB以内,这样整个HTML的内容请求就可以在一个RTT内请求完成,最大限度地提高HTML载入速度。
      • 缓存类
        • 1. 合理利用浏览器缓存
        • 2. 静态资源离线方案
        • 3. 尝试使用AMP HTML
      • 图片类
        • 1. 图片压缩处理
        • 2. 使用较小的图片,合理使用base64内嵌图片
          • 一般图片大小超过2KB就不推荐使用base64嵌入显示了
        • 3. 使用更高压缩比格式的图片
        • 4. 图片懒加载
        • 5. 使用Media Query或srcset根据不同屏幕加载不同大小图片
        • 6. 使用iconfont代替图片图标
          • @font-face {   font-family: iconfont;   src: url("./iconfont.eot");   src: url("./iconfont.eot?#iefix") format("eot"),         url("./iconfont.woff") format("woff"),         url("./iconfont.ttf") format("truetype");}
        • 7. 定义图片大小限制
          • 加载的单张图片一般建议不超过30KB,推荐在10KB以内。
      • 脚本类
        • 1. 尽量使用id选择器
        • 2. 合理缓存DOM对象
        • 3. 页面元素尽量使用事件代理,避免直接事件绑定
          • 使用事件代理可以避免对每个元素都进行绑定,并且可以避免出现内存泄露及需要动态添加元素的事件绑定问题,所以尽量不要直接使用事件绑定。
          • $('body').on('click','.btn', function(e){  console.log(this);}
        • 4. 使用touchstart代替click
        • 5. 避免touchmove、scroll连续事件处理
          • 需要对touchmove、scroll这类可能连续触发回调的事件设置事件节流,如设置每隔16ms(60帧的帧间隔为16.7ms,因此可以合理地设置为16ms)才进行一次事件处理,避免频繁的事件调用导致移动端页面卡顿。
        • 6. 避免使用eval、with,使用join代替连接符+,推荐使用ES6的字符串模板
        • 7. 尽量使用ES6+的特性来编程
      • 渲染类
        • 1. 使用Viewpoint固定屏幕渲染,可以加速页面渲染内容
          • 在移动端设置Viewpoint可以加速页面的渲染,也可以避免缩放导致页面重排重绘。
          • <!--设置viewport不缩放--><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        • 2. 避免各种形式重排重绘
        • 3. 使用CSS3动画,开启GPU加速
          • 使用CSS3动画时可以设置transform:translateZ(0)来开启移动设备浏览器的GPU图形处理加速,让动画过程更加流畅。
        • 4. 合理使用Canvas和requestAnimationFrame来实现动画
        • 5. SVG代替图片
        • 6. 不滥用float
          • 推荐使用固定布局或flex-box弹性布局的方式来实现页面元素布局
        • 7. 不滥用web字体或过多font-size声明
          • 过多的font-size声明会增加字体的大小计算
      • 架构协议类
        • 1. 尝试使用SPDY和HTTP2
          • 在条件允许的情况下可以考虑使用SPDY协议来进行文件资源传输,利用连接复用加快传输过程,缩短资源加载时间。
        • 2. 使用后端数据渲染
        • 3. 使用Native View代替DOM的性能劣势
    • 前端用户数据分析
      • 用户访问统计
        • PV(Page View)
          • PV一般指在一天时间之内页面被所有用户访问的总次数,即每一次页面刷新都会增加一次PV
          • PV作为单个页面的统计量参数,通常用来统计获取关键入口页面或临时推广性页面的访问量或推广效果。
        • UV(Unique Visitor)
          • UV指在一天时间内访问页面的不同用户个数
          • UV可以认为是前端页面统计中一个最有价值的统计指标,因为其直接反应页面的访问用户数。
          • 目前有较多站点的UV是按照一天之内访问目标页面的IP数来计算的,因此我们也可以根据UV来统计站点的周活跃用户量和月活跃用户量。
          • 除了根据IP,还需要结合其他的辅助信息来识别统计不同用户的UV
            • 根据浏览器Cookie和IP统计。问题:Cookie手动被清除,页面被重新访问时就只能算第二次。
            • 结合用户浏览器标识userAgent和IP统计。
          • UV一般情况下是无法用于精确统计的,所以通常需要结合PV、UV来一起分析网站被用户访问的情况。
          • 我们还可以对站点一天的新访问数、新访客比率等进行统计,计算第一次访问网站的新用户数和比例,这对判断网站用户增长也是很有意义的。
        • VV(Visit View)
          • VV是统计网站被用户访问次数的参考数据,通常用户从进入网站到最终离开该网站的整个过程只算一次VV
        • IP(访问站点的不同IP数)
      • 用户行为分析
        • 相对于访问量的统计,用户行为分析才是更加直接反映网页内容是否受用户喜欢或满足用户需求的一个重要标准。
        • 如果我们能知道用户浏览目标页面时所有的行为操作,一定程度上就可以知道用户对页面的哪些内容感兴趣,对哪些内容不感兴趣,这对产品内容的调整和改进是很有意义的。
        • 参数指标
          • 页面点击量
            • 页面点击量用来统计用户对于页面某个可点击或可操作区域的点击或操作次数。
          • 用户点击流
            • 点击流用来统计用户在页面中发生点击或操作动作的顺序,可以反映用户在页面上的操作行为。所以统计上报时需要在浏览器上线保存记录用户的操作顺序,如在关键的按钮中埋点,点击时向localStorage中记录点击或操作行为的唯一id,在用户一次VV结束或在下一次VV开始时进行点击流上报,然后通过后台归并统计分析。
          • 用户访问路径
            • 用户访问路径针对每个页面埋点记录用户访问不同页面的路径。通常是在一次VV结束或下一次VV开始时上报用户的访问路径。
          • 用户点击热力图
            • 用户点击热力图是为了统计用户的点击或操作发生在整个页面哪些区域位置的一种分析方法,一般是统计用户操作习惯和页面某些区域内容是否受用户关注的一种方式。
            • 获取上报点的方式主要是捕获鼠标事件在屏幕中的坐标位置进行上报,然后在服务端进行计算归类分析并绘图。
          • 用户转化率与导流转化率
            • 对用户转化率的分析一般在一些临时推广页面或拉取新用户宣传页面上较常用。
            • 用户转化率=通过该页面注册的用户数/页面PV
            • 导流是将某个页面的用户访问流量引导到另一个页面中
            • 导流转化率=通过源页面导入的页面访问PV/源页面PV
          • 用户访问时长、访问内容分析
            • 用户访问时长和内容分析是统计分析用户在某些关键内容页面的停留时间,来判断用户对该页面的内容是否感兴趣,从而分析出用户对网站可能感兴趣的内容,方便以后精确地向该用户推荐他们感兴趣的内容。
      • 前端日志上报
        • 获取错误日志
          • 浏览器提供了try...catch和window.onerror两种机制来帮助我们获取用户页面的脚本错误信息。
          • 一般来说,使用try...catch可以捕捉前端JS的运行时错误,同时拿到出错的信息,如错误信息描述、堆栈、行号、列号、具体的出错文件信息等。我们也可以在这个阶段将用户浏览器信息等静态内容一起记录下来,快速地定位问题发生的原因。需要注意的是,try...catch无法捕捉到语法错误,只能在单一的作用域内有效捕获错误信息,如果是异步函数里面的内容,就需要把function函数块内容全部加入到try...catch中执行。
          • window.onerror可以在任何执行上下文中执行,如果给window对象增加一个错误处理函数,既能处理捕获错误又能保存代码的优雅性。window.onerror一般用于捕捉脚本语法错误和执行时错误,可以获得出错的文件信息,如出错信息、出错文件、行号等,当前页面执行的所有JS脚本出错都会被捕捉到。
          • ```
          • window.onerror = function(msg, url, line) {
          •   // 可以捕获异步函数中的错误信息并进行处理,提示Script error
          •   console.log(msg):  // 获取错误信息
          •   console.log(url);  // 获取出错的文件路径
          •   console.log(line): // 获取错误出错的行数
          • };
          •  
          • setTimeout(function() {
          •   console.log(obj):  // 可以被捕获到,并在onerror处理
          • }, 200);
          • ```
          • 使用onerror要注意,在不同的浏览器中实现函数处理返回的异常对象是不相同的,而且如果报错的JS和HTML不在同一个域名下,错误时window.onerror中的errorMsg全部为script error而不是具体的错误描述信息,此时需要添加JS脚本的跨域设置。<script src="//www.domain.com/main.js" crossorigin></script>
          • 我们可以对前端脚本中常用的异步方法入口函数或模块引用的入口方法统一使用try...catch进行一层封装,这样就可以使用try...catch捕获每个引用模块作用域下的主要错误信息了。
          • ```
          • // 对setTimeout实现函数进行包装
          • window.setTimeoutTry = function (fn, time) {
          •     let args = arguments;
          •     let _fn = function () {
          •         try {
          •             // 将函数参数用try...catch包裹
          •             return fn.apply(this, args);
          •         } catch (e) {
          •             console.log(e);
          •         }
          •     };
          •     return window['setTimeout'](_fn, time);
          • };
          •  
          • try {
          •     setTimeoutTry(function () {
          •         obj //获取错误信息 ReferenceError: obj is not defined
          •     }, 300);
          • } catch (e) {
          •     console.log(e);
          • }
          • ```
        • 将错误信息上传到服务器
          • 注意:页面的访问量可能很大,如果到达百万级、千万级,那么就需要按照一定的条件上报,如根据一定的概率进行上报,否则大量的错误信息上报请求会占用日志收集服务器的很多资源和流量。
        • 通过高效的方式来找到问题
          • 为了方便查看收集到的这些信息,我们通常可以建立一个简单的内容管理系统(CMS)来管理查看错误日志,对同一类型的错误做归并统计,也可以建立错误量实时统计来查看错误量的即时变化情况。当某个版本发布后,如果收到的错误量明显增加,就需要格外注意。
        • 文件加载失败监控
          • 可以对<img>或<script>的readyChange进行是否加载成功的判断,但只有部分IE浏览器支持<img>或<script>的readyState,因此一般还需要结合其他方式,如onload,针对不同浏览器分开处理。
          • ```
          • // 页面需要加载的三个script脚本资源
          • let scripts = [script1, script2, script3];
          •  
          • // 三个script加载的初始状态
          • let loaded = {
          •     [script1]: false,
          •     [script2]: false,
          •     [script3]: false
          • };
          •  
          • for (let script of scripts) {
          •     // IE浏览器的情况设置readyState来判断
          •     if (script.readyState) {
          •         script.onreadystatechange = function () {
          •             let state = this.readyState;
          •             if (state === 'loaded' || state === 'complete') {
          •                 callback();//脚本加载成功回调
          •                 // 表示该脚本加载成功
          •                 loaded[script] = true;
          •             }
          •         }
          •     } else {
          •         // 其他浏览器,如Firefox、Safari、Chrome或Opera,结合onload
          •         script.onload = function () {
          •             callback();//脚本加载成功回调
          •             // 表示该脚本加载成功
          •             loaded[script] = true;
          •         }
          •     }
          • }
          •  
          • setTimeout(function () {
          •     //  如15秒后执行页面脚本加载情况的上报进行统计
          •     report(loaded);
          • }, 15000);
          • ```
          • 通过这种方式仅仅是判断了文件或脚本加载成功的情况,我们还需要指定一个文件加载的列表,在一段时间后将页面文件加载的结果对象上报给服务器端来统计不同文件的具体加载情况。
    • 前端项目开发流程设计
      • 1. 前端框架选型
      • 2. 模块化方案
      • 3. 代码规范化
      • 4. 构建自动化
      • 5. 组件化目录设计
      • 6. 代码优化处理
      • 7. 数据统计
      • 8. 同构项目结构设计
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值