分类 前端 下的文章

还记得开始学习前端,闭包作用域神马的都非常难以理解,遇到面试只能提前去背,其实被一仔细问都很难说出所以然,相信很多新手都有和我曾经一样的问题,所以我们今天来学习一下闭包和函数,一起来了解这一部分吧~

    我们其实每天都在写闭包,每天编码都无时无刻的享受着闭包带来的便利,我们的动画处理,事件回调,包括在一些框架中闭包一直都存在,闭包和作用域规则息息相关,所以我们会连带了解一下作用域相关知识。




 理解闭包

      闭包允许函数访问函数外部的变量,只要变量存在于声明函数的作用域中即可访问。所声明的函数不会因为作用域的消失而消失,而是任何时间都可以调用。

    var value = "我是外部变量";function testFun(){console.log(value);}testFun(); // 执行函数

    这样子的代码我们每天都在写,其实它就是一个闭包,我们可以把代码套入到刚刚的概念中; 首先函数声明和变量都在全局作用域中(符合在同样的作用域概念)也满足任何时间去调用testFun函数,但是我们拿这种简单的例子体现不出闭包的作用。

      // 一个更为直观的闭包例子var value = "shenhao";var later = null;function outsideFun(){  var innerValue = "函数作用域声明的变量";  function insideFun(){    // 由于作用域链    console.log(value); // shenhao    console.log(innervalue); // 函数作用域声明的变量  }  // 把内部函数传递给later  later = insideFun;}outsideFun();later();

    按照道理来说,执行later时候访问变量,value肯定是有值的,因为它存在于全局环境中,那么innerValue真的因为外部作用域的消失而是null么,答案是不可能的,由于闭包原因,我们还能访问innerValue的值。所以我们能得出一个结论: 当在外部函数内部创建一个函数的时候,还额外的创建了一个闭包,这个闭包包含了创建它时此作用域全部的变量,当这个作用域消失的时候,那么闭包中还在,因此我们就可以在作用域消失之后还能访问变量啦。

      那我们可以用一个图来描述一下这个关系(此图借鉴了js忍者秘籍第5章闭包相关内容的气泡图)

      

    闭包就可以使用这个图来解释,那么我们必须要记住的一点就是,通过闭包来访问变量的函数都有一个作用域链,作用域链保存着闭包的信息。

         使用闭包的时候,所有的信息都会存储在内存中,那么这些信息什么时候被销毁,取决于JS引擎认为这些信息不再使用才会被回收或者当页面卸载时。




    用闭包实现私有变量

    开发者如果不止会一种编程语言,那么肯定就知道私有变量这个概念,但是在JS中是不存在的,但是我们可以使用闭包来构建一个“类私有变量”,但是并不是完美真正的私有变量,我们看一下demo


      function Test(){  var count = 0;  this.addCount = function(){    count ++;  }  this.getCount = function(){    return count;  }}
        const private = new Test();private.addCount();private.getCount(); // 1


            Test方法中的count就是我们所说的私有属性,只能通过内置的add和get方法才能对其操作,那么我们也可以构造多个函数,那么当然不同函数中的count都是具有独立性的。我们通过上述代码简单实现了一个私有属性的例子,就是通过闭包,外部通过内部的函数去获取/修改变量而外部不能直接去更改变量。

            



        回调函数callBack

        当我们做WEB应用时候,做一些动画的需求,常见地要求我们同时操作好几个DOM,操作好几个动画状态,那我们在学习闭包之前,都是在全局作用域去定义变量,然后去调用多个函数,对应的函数再去改变对应的变量,这样非常不好,会造成代码过于繁重且控制多个变量会非常麻烦。那么我们可以用闭包解决这个问题,假设我们需要改变2个动画区域,我们可以这么写。

            

          function animated(elementID){  var elem = document.getElementById(elementID);  var tick = 0;  var timer = setInterval(() => {    ...  })}


          如果我们把tick和timer等变量挪到全局作用域中,那么我们控制多个elment的动画,将会产生大量的变量,如果我们通过闭包,计时器中的回调都可以操作对应自己的私有变量,那么我们就可以这样得出结论:

          闭包不是一个创建函数时候的快照,而是一个可以在函数执行的时候能够修改变量的状态封装,只要闭包存在,我们就可以修改变量的值



          执行上下文跟踪代码

          我们都知道JS是单线程的执行模型,我们都知道JS有2种代码类型,一种是全局代码,一种是函数代码,那么执行上下文也分为全局执行上下文和函数执行上下文,全局执行上下文则js程序开始时就创建了,函数执行上下文那么就是函数执行开始创建,那么我们可以看一个例子来了解一下:

            <script>// 全局执行上下文开始var start = "开始";function method(){  // 函数执行文开始}method();</script>

            当一个函数开始执行,则创建一个新的执行上下文,执行完毕则会关闭上下文回到创建时的执行上下文接着执行代码,所以JS为了跟踪这种调用顺序就要使用执行上下文栈,也是大家熟悉的 “调用栈”;


            (PS:有那味了)

            就像一个托盘一样,盘子越垒越高,每一个盘子都是函数执行栈,执行完以后最终都会回归到全局执行栈中

            执行上下文除了可以定位执行的函数之外,在静态环境下也可以准确地定位到具体的标识符噢。




            作用域(词法环境)

            词法环境又称之为作用域,是js引擎内部实现的一套跟踪标识符和变量映射关系的机制,很多文章都没有把词法环境的概念讲清楚,那么表达“映射关系”是最贴切的。

            作用域是和js代码息息相关的,那么在ES6初版的时候没有块级作用域的解决方案,所以从C,Java开发转过来的开发者们就会觉得JS应该也有这样的概念,但是其实从ES6的let const之后才解决了这个问题。我们都知道这种作用域场景最直观的表达就是代码嵌套,那么我们写一个例子:


              var all = "全局的变量"function a(){ var aValue = "a函数里面的变量" function b(){   var bValue = "b函数里面的变量"   for(var c = 0; c<=10; c++){     console.log(all + aValue + bvalue + c);   } }}

              JS引擎是如何跟踪这些代码,分析它们的结构,这就是词法环境的作用。

              执行上下文对应的词法环境,词法环境中包含了上下文中定义标识符映射表,所以for循环具有a和b的标识符映射表,无论何时创建了函数,那么会有一个与之对应的词法环境,那么这个词法环境会存储在[[Enviroment]]这个内部属性上,所以这就是JS引擎分析代码之间关联的重要原因。

              那么现在我们已经了解了基本的标识符查找规则,那么变量类型也是我们需要简单了解的,尽管这比较简单。




              理解JS的变量类型

              像我们现在讨论的这个话题,基本是中小公司面试必问的,烂大街的let

               conat var 有什么区别?这个问题出镜率实在太多了,网上给的答案也太多了,我们就根据刚刚所提到的知识点来总结一下:


              1. 可变性

              2. 词法环境


              const是无法改变的

              let,const 比较var的区别则就是词法环境不一样,let和const解决了JS块级作用域的缺陷。


              社区中很多大佬或者某些公司的代码规范中,出现了强制要求使用const替换var和let的事情,因为非常多代码是因为变量的可变性造成的BUG,那么我们在日常开发中,其实是很少遇到只能通过变量的可变来做的需求,所以我认为这种观念还是蛮正确的,值得提倡

              那么我们来深入const的工作原理,深层次的了解一下:

              我们一般使用const是这样的,第一是不会再次赋值的特殊变量或者是一个固定的值,比如我们在运算的时候,需要定义人数,数量诸如此类,也是为了JS代码的运行稳定为程序优化提供便利。


                const a = "一次赋值";a = 1; // 重新赋一个新值  报错const b = {};b.age = 20; // 不会报错const c = [];c.pus("1"); // 不会报错


                const只允许初始化赋一次值,以后如何有“新值”赋给const变量,则会报错,但是在const变量上对对象,数组的修改是允许的。

                变量的可变性已经描述完了,现在我们可以看看变量的词法环境的区别。


                当我们使用关键字var的时候,该变量的词法环境是最近的函数内部或者全局定义的,那么我们就可以用一个demo表示:

                  function fun(){  for(var i = 0;i<10;i++){    var thisMessage = "和变量i一个作用域"  }}

                  1. 从这个例子就可以看出,变量i是var定义的,所以它属于最近的函数词法环境内,所以变量i和thisMessage是同一个作用域,for循环中的变量i也因此忽略块级作用域


                  所以很多文章教程中,包括我们日常开发的时候,在写for循环中类似的临时变量的时候,需要使用let定义变量,避免在多个循环中(都在一个函数词法环境中)会造成冲突。


                  那么我们使用let改写此demo之后:

                    for(let i = 0; i<10;i++)

                    那么我们可以在一个词法环境中写多个类似的代码,从而不造成冲突,外部词法环境也无法访问到i变量。

                    我们已经了解了标识符的定义和词法环境相关知识,那么我们下来应该进行深入探索在词法环境中如何注册标识符。




                    注册标识符

                    我们如果是从Java或者C转JS的童鞋们,当我们第一次接触到下面的代码,你或许感觉到懵逼。

                      var test = "我是一个测试变量";testFun(test);function testFun(str){  return "Hello" + str;}


                      为什么我的函数还没定义,就可以执行?我们都知道JS是逐行执行代码的,按道理说函数还没执行到声明这一块,是不可以调用testFun。那么JS是如何注册函数的,如何知道testFun的存在的呢?

                      JS在创建新的词法环境的时候,会执行2步。

                      1.  js会访问并注册在当前词法环境注册的变量和函数

                      2. 执行代码:取决于变量类型和词法环境类型

                      那我们要细细分析一下这个注册过程。

                          1. 首先如果是创建了函数词法环境,那么JS先会定义行参和参数默认值

                          2. 如果是全局词法环境,那么将会把函数声明的代码查询到(函数表达式或者是箭头函数都不会被查到)查询到之后将会把值赋给函数同名的标识符中,如果标识符已存在将会被改写,如果是块级作用域则跳过此步骤。

                          3. 变量声明会在函数和全局作用域中,找到当前函数和其他函数中使用var声明的变量以及在其他函数或者代码块之外的let和const变量,在块级作用域中查找let和const变量,如果不存在将赋值为undefined,存在即保留。


                      总结:JS注册标识符将依据当前词法环境进行注册。

                      所以我们就可以解释我们刚刚写的demo出现的疑惑了,是因为JS在第一阶段中加载了全局词法环境,将会把函数声明语句赋给一个新的标识符,因此我们在第二个阶段代码执行时能顺利地取到函数。

                      那么我们在开发中不仅仅会遇到上面这种情况,我们可能还会遇到函数标识符重载的问题:


                        console.log(typeof fun); // functionvar fun = 3;function fun(){}console.log(typeof fun); // number

                        这就涉及到一个概念叫做变量提升,例如函数声明将提升到全局作用域顶部,变量声明将提升到函数作用域顶部

                        那么我们来分析一下上述的代码:

                        1. 首先在第一个阶段,函数声明语句将把函数赋给同名标识符,所以在代码执行的时候fun标识符是有值的而且类型就是函数

                        2. 然后当进行变量处理的时候,发现fun标识符已被注册所以不会被赋值undefind,当代码执行时fun已被再次赋值为3,所以第二个console打印出的类型是number




                        闭包工作原理

                        我们之前了解过闭包可以让我们访问函数创建的作用域的全部变量,也举了几个例子,比如动画处理和回调。

                        我们将利用词法环境和执行上下文重新分析我们之前的demo,这将会对我们理解闭包会更有帮助。

                        1. 计数器私有变量的例子

                          function Test(){var count = 0;this.addCount = function(){count ++;}this.getCount = function(){return count;}}

                          分析:我们通过new关键字调用Test这个构造函数,我们在外部访问addCount函数实际上就是创建了这个闭包,在我们执行Test.addCount这样的函数之前我们的执行上下文是全局执行上下文,因为我们调用函数会创建新的执行上下文那么也会创建新的词法环境,那么addCount这个词法环境包含着创建这个函数的词法环境,当在addCount中找不到count变量那么就会从外部环境去寻找(因为此时对象是活跃的,count变量就在此对象中),就是这么简单...

                          我们理解了执行上下文和词法环境的作用,词法环境主要的作用就是跟踪函数中定义的变量,新的执行环境将创建新的词法环境。

                          请注意,JS没有真正的私有变量,上面的demo我们可以改变它:

                            var testImport = {};var Test = new Test();testImport.getCount = Test.getCountconsole.log(testImport.getCount); // 可以访问

                            2. 动画处理的例子

                              function animated(elementID){var elem = document.getElementById(elementID);var tick = 0;var timer = setInterval(() => {...})}animated("box1");animated("box2");

                              分析:我们调用了animated的2个方法也就创建了2个词法环境,同样的我们也创建了新的执行上下文,每个词法环境都有对应的elem,tick等变量,我们在这个代码中设置了setInterval这个函数,只要有一个函数在访问变量,那么这个闭包就会一直存在,除非计时器被清除,随后回调函数被执行,然后这个闭包会访问创建闭包时候的变量,极大的简化了代码。




                              小结

                              1. 我们彻底理解了闭包的概念,也通过安全气泡的方法来加深我们对于词法环境的理解,保存创建函数时的词法环境我们是存储[[Environment]]属性中的。所以我们当作用域消失还能访问到创建时候的变量

                                  2. JS引擎通过执行上下文栈(调用栈)来跟踪代码的执行,每次调用函数都会有新的执行上下文,并且像我们演示服务生托盘一样推到最顶端

                                  3. JS通过词法环境来跟踪标识符(也就是作用域)

                                  4. 我们可以定义全局,函数,局部作用域级别的变量

                                  5. 使用let const var关键字声明变量时,我们理解了它们的区别:可再次赋值和是否可以局部作用域。

                              前排提示:非推广软文

                              微信公众号: 因卓诶;此文已同步到因卓诶blog

                              Uniapp这两年是Vue开发者很喜欢的跨平台开发框架,作为一个国产开发框架,其实文档和周边工具都对国人非常友好,但是由于框架本身的跨多端所以从诞生以来都被很多开发者诟病“坑太多”,那么这篇文章将结合本人2年Uniapp开发经验,给新手小白一个从0到1的教程&踩坑说明。


                              目录

                              • 历史
                              1. uniapp编译器发展历程
                              • 踩坑经验(框架部分)
                              1. 条件编译
                              2. onLoad和onShow 用合适的生命周期
                              3. 触底加载和下拉加载的实例
                              4. 如何操作DOM
                              5. 上传图片/文件
                              6. websocket
                              7. Nvue介绍
                              8. SubNvue介绍
                              9. 全局通信
                              10. h5+API介绍
                              11. 应用内更新和wgt的介绍和使用
                              12. APP自定义tabbar
                              13. 如何协同原生插件来工作
                              14. manifest.json简介
                              15. uniapp如何请求数据
                              16. uniapp应该用什么方式创建项目
                              • 社区
                              • 插件市场
                              • HbuilderX
                              1. 简介和快速开发的准备工作
                              2. 打包APP前的准备
                              3. uniapp的debug
                              4. APP打包
                              5. 关于IOS
                              • 拓展阅读

                              历史
                              Uniapp从诞生到目前,经历了3次重大变革,首先是最初借鉴了Mpvue的模板语法(非自定义组件模式)这个模式是性能最差且支持Vue语法有很多欠缺的版本,在当时我认为Uniapp开发APP是满足不了企业需要的,倒是做一款小程序开发框架很是不错。

                              在2018-2019这个时间段,也就是uniapp是模板编译模式的时代,网上对于uniapp也是褒贬不一,社区的完善度还有官方的UI支持都是非常落后的,唯一吹一吹的就是QQ群当时很火,官方的解答和处理BUG的速度还很快。我个人认为web前端跨平台框架受到了很多原生安卓&IOS开发者的排挤,拿生成的代码质量和性能说事,说不定自己也没有使用过。

                              在我接触uniapp不久之后(大概几个月)就出了自定义组件模式,这个组件模式相当于革命性的更新,支持用户编写的Vue组件转换成微信小程序的自定义组件,基于这个uniapp在安卓平台上放置了JSCore,从这个时候开始uniapp开始慢慢的被更多的原生开发者接受,因为跨平台开发的好处是在是太大了。

                              那么接下来的一次更新让我瞠目结舌,因为由于19年的年底这一段时间我没有从事uniapp开发,直到今年才继续开发uniapp,发现了uniapp的一次重大更新:“v3引擎”。

                              V3引擎同样也是Dcloud自研,我认为主要的更新在于安卓和IOS端,开始注重了启动速度和包体积大小,我们可以在V3更新说明中看到。

                              到此为止,我认为uniapp到此刻应该是推广的时候,让前端开发人员坐上跨平台开发的小车车。

                              踩坑经验(框架部分)

                              首先,uniapp绝大部分官网描述的API,比如设备信息,内存,蓝牙等等都是原生开发比较常用的,uniapp的优点就体现出来了,不同平台的各种API都会略微有差异,那么我们应该仔细看文档。

                              如果文档已经说明此API存在【平台差异】,那么我们应该注意API下方可能会有这样的【tips】

                              这也是文档的一个小坑,很多新手如果不仔细看文档,在调API的时候不考虑兼容问题,导致在小程序/APP中,比如出现安卓和ios功能不一致的问题,如果看仔细文档,那么百分之99以上的问题都会在tips中说明。

                              注释魔法:条件编译

                              其次uniapp还有一个非常强大的功能【条件编译】,这也是开发中非常常用的功能,如下,我们可以使用像C语言中核心注释,注释中的代码片段将在指定的平台出现,反之亦然;

                              <!-- #ifdef APP-PLUS -->代码片段,APP-PLUS代表着APP端,ifdef包含,ifndef不包含,多个平台可以用空格隔开<!--#endif-->
                              <!--#ifndefAPP-PLUS-NVUEAPP-NVUEMP-->编译多个平台<!--#endif-->
                              

                              条件编译可以存在于任何地方,template css js;同样uniapp支持更强大的条件编译:比如支持在page.json中进行判断,达到不同端不同的分包功能;静态资源也可以进行条件编译,通过static下构建platform目录即可把不同的静态资源编译到不同的平台上去;那么同理也可以把页面进行条件编译,即不同平台不同页面;

                              onLoad&onShow

                              我们已经了解了uniapp极具特色的条件编译之后,我们可以了解一下uniapp必不可少的生命周期,uniapp在目前版本支持vue的所有生命周期以及绝大多数API;比较重要的nextTickcompile不支持,组件选项中比较重要的是render函数不支持,具体更多的支持特性表请移步官网查看(https://uniapp.dcloud.io/use?id=vue%e7%89%b9%e6%80%a7%e6%94%af%e6%8c%81%e8%a1%a8),下面我们将推荐开发中常用的生命周期以及应用场景:

                              onLoad(e){// 组件渲染未完成但是已创建的钩子,与Vue的create同理,这里推荐使用onLoad代替create// 行参e可以获取当前路径的参数,比如当前页面是b,如果a跳转到b页面是如下url:///pages/index/a?type=1 console.log(e.type); // "1"}
                              
                              onShow(){//用于监听:页面在屏幕上显示时触发,从APP应用后台到APP前台也会触发//通常我们可以使用这个钩子做一些非列表页面的数据刷新(比如从上一个页面返回,然后此页面刷新)}
                              

                              触底加载&下拉加载的实践

                              APP中有一个非常常见的场景叫做触底加载,那么针对APP页面级别的滚动触底回调就是onReachBottom

                              onReachBottom(){//在这里进行当前页加1this.listConfig.nowPage ++;// 调用加载列表的方法this.initList();}methods: {async initList(){consttoList=awaitgetALLTodoList({limit:this.listConfig.pageSize,offset:(this.listConfig.nowPage-1)*this.listConfig.pageSize),})}}
                              

                              那么下拉加载这个功能在APP中非常常见,我们需要在page.json中对应的节点style下开启下拉加载

                              "enablePullDownRefresh": true
                              

                              那么对应页面的监听钩子将会触发

                              onPullDownRefresh(){//用户下拉了页面}
                              

                              【注意事项】不管是触底加载和下拉刷新此实例都是针对页面级别,如果你不知道何为页面级,那么请耐心看完uniapp的组件相关内容,在uniapp组件中有一个<scroll-view>,它提供一个滚动视图,在这个滚动视图中有着自己的下拉加载和触底加载方法,因此如果页面中存在此组件,开发者应该妥善处理组件级别的滚动和页面级别的滚动,这是非常重要的。

                              在开发中,我们经常会出现一些列表的场景,那么在uniapp中处理长列表,我们不应该使用scroll-view组件级别的滚动,而应该使用页面级别的滚动,这样性能会更好

                              如何操作DOM?

                              我们通常在开发Vue应用时,很少直接操作DOM,在uniapp中没有暴漏DOM这个概念,但是我们可以通过指定的API去操作DOM,尽管这是非常不建议的,但是在某些业务场景不得不说

                              它是很方便的;

                              lettabInfo=uni.createSelectorQuery().select("#tab");tabInfo.boundingClientRect((data)=>{//目标区域的信息}).exec();
                              

                              文件的选择

                              uniapp在文件的选择上非常丰富,举一个例子比如说图片,我们可以在文档中很清楚的查询到对应的API,通过封装的API我们可以通过配置参数来chooseImage,然后拿到临时路径再继续上传;

                              uni.chooseImage({count: 6, //默认9    sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有    sourceType: ['album'], //从相册选择    success: function (res) {//临时路径 res.tempFilePathsuni.uploadFile({url: '', //仅为示例,非真实的接口地址            filePath: tempFilePaths[0],name: 'file',formData: {'user': 'test'            },success: (uploadFileRes) => {console.log(uploadFileRes.data);            }        });    }});
                              

                              重点来了,我们上传图片固然简单,但是上传文件是非常难的,我们如果在APP端想做一个上传文件的功能,就要尽量使用html的帮助,由html的input type=“file”来做上传,那么首先得简单了解一下利用html如何上传文件。

                              1. 首先我们需要使用h5+API中的webview相关的API

                                let wv = plus.webview.create("", "/hybrid/html/index.html", {'uni-app': 'none', //不加载uni-app渲染层框架,避免样式冲突 top: 0,height: '100%',background: 'transparent' }, { url, header,key: name, ...formData, });wv.loadURL("/hybrid/html/index.html"); // 加载本地的html文件currentWebview.append(wv);//把html追加到当前页面中
                                把本地的HTML加载到APP之上之后,我们在本地编写的HTML文件中引入对应uni.webview文件以及需要的业务JS文件,我们在HTML是这样做的(核心代码)
                                <scripttype="text/javascript"src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script> <scriptsrc="js/h5-uploader.js"type="text/javascript"charset="utf-8"></script>

                              我们在本地编写的业务JS

                              xhr.onreadystatechange=(ev)=>{if(xhr.readyState == 4) {if (xhr.status == 200) {progress.innerText='成功';//从这里可以看出,上传成功之后,我们通过改变html的title把信息返回        title.innerText = `${file.name}|^$%^|${src}|^$%^|${suffix}`;      }else {        progress.innerText = '上传失败了';}      setTimeout(()=>{        tis.style.display = 'none';// 上传成功,关闭了上传webview的窗口        plus.webview.currentWebview().close();},1000);    }  };  xhr.send(formData);
                              

                              当index.html的title变化之后,在vue组件创建webview的代码之后,我们可以在当前webview去监听标题的变化。

                              //wv就是plus.webview.create的返回,上述代码有举例子wv.addEventListener('titleUpdate', ({  title}) => {  console.log(title)}
                              

                              那vue组件拿到回调信息之后,就可以正常拿到图片信息去在页面中渲染。

                              顺便说一句,在文档的后面,会给大家介绍uniapp相关生态,一些业务组件插件都会在相关生态中查询到。

                              websocket

                              oh,又到了这个该死的东西了,今年的一段时间内,在uniapp上使用websocket可是真的太难了,先说一下事情的经过,我司开发的APP中本来使用了聊天相关功能,之后在部分业务中加入了websocket,前端需要根据websocket实时获取一些信息,在测试APP的时候发现,当2个websocket并行连接的时候,发现只有一个连接能有效的发送和接受消息,无奈只好去文档中寻找答案。

                              App平台自定义组件模式下,以及支付宝小程序下,所有 vue 页面只能使用一个 websocket 连接。App下可以使用 plus-websocket
                              插件替代实现多链接。App平台,2.2.6+起支持多个socket链接,数量没有限制。
                              uniapp-websocket文档

                              import socket from'plus-websocket'// #ifdef APP-PLUSObject.assign(uni, socket)// #endif
                              

                              通过把依赖中的socket合并到uni中,把uni中原本封装的socket进行替换;

                              Nvue

                              uniapp支持用户编写.nvue的组件,那么此组件将会使用改进后的weex引擎进行渲染,提供了原生渲染的能力,那么如果你是weex用户如果用nvue写,那么全程和weex的写法无异,如果是普通的vue开发者使用nvue开发,那么将注意,nvue对于css和js的限制有非常多,尤其是css很多常用的简写都将不能支持,所以我们需要额外查询对应的weex框架的文档。

                              那么我们在APP中有必要全部使用nvue原生渲染么,使用普通vue组件进行webview渲染有什么缺点么?那么我建议APP中核心/用户访问次数多/需要更强劲的性能的页面可以使用nvue,其余普通页面我们仍然可以使用vue页面。

                              那么如果是weex用户转uniapp,可以全项目使用nvue,uniapp支持纯原生渲染模式,可以减少APP包体积,也可以减少内存占用,因为此时webview相关依赖是不存在的。使用nvue开发,即保留了原weex开发者的习惯又可以提供强大的API能力,这是非常让人兴奋的开发体验。

                              SubNvue

                              subNvue本质上是一个nvue组件,支持在普通vue页面之上运行,subNvue能覆盖map,video等原生组件,uniapp有很多办法支持覆盖原生组件,subNvue是我认为最好的方法,因为相比<cover-view>组件,不能嵌套,而且写起来和原页面耦合;又和webview相比,需要原生h5+api做技术支撑,nvue相对来说更多vue开发者友好。

                              实例:由于首页有swiper那么悬浮的头部需要遮挡住swiper,在pages.json中注册即可

                              "subNVues":[{"id":"indexTitle",//唯一标识"path":"pages/index/subNVue/indexTitle",//页面路径"style":{"position":"fixed","dock":"top","background":"transparent"}}]
                              

                              uni全局通信

                              在uniapp中,vue,nvue组件中进行信息传递是很难的,尤其是vue和nvue中的信息传递我们可以通过全局通信的方式来做业务逻辑。

                              uni.$emit('update',{msg:'我是首页,用户下滑了'})
                              
                              uni.$on('update',function(data){console.log("我是subnvue头部,你下滑了,那我就改变自身的透明度动画")})
                              

                              适用场景:

                              vue与nvue,nvue与vue间的通讯
                              tabbar页面之间的通讯
                              父页面与多级子页面间的通讯
                              uniapp-emit文档

                              注意事项:

                              1. 只有在页面打开时候才能注册或者监听

                              h5+API

                              其实像uniapp这样的框架(或者其他的跨平台框架)我认为h5+api带动了整个相关技术,我们在没有这样框架的时候,都是使用h5+api完成一个又一个优秀的APP,这都离不开这个非常强大的API

                              在前面的demo中,我们或多或少地看到了h5API的影子,下面我们再复述一遍在uniapp中使用h5+API时的一些注意事项,这非常重要。

                              首先uniapp内置了HTML5+引擎,我们可以直接调用h5+相关规范,但是在小程序,H5端并没有对应的规范拓展,所以在这些平台不会识别“plus”这个变量,所以我们需要写条件编译

                              // #ifdef APP-PLUSconsole.log(plus)//#endif
                              

                              在普通的H5项目中,我们如果要使用H5+API,我们需要进行ready,但是在uniapp不需要ready,直接上去就是一套军体拳就是干。

                              还有我们使用一些监听事件的时候,由于uniapp没有document这个对象,所以需要使用

                              plus.globalEvent.addEventListener("这里写h5+拓展的事件")
                              

                              应用内更新&WGT的使用

                              应用内更新和WGT这一块是每一个APP不可缺少的部分,所以我这里会比较详细的做一些介绍和实践。

                              首先我们来了解一下WGT是干嘛的。wgt是APP资源更新包,通常来讲这个包体积很小,只有1m2m左右,APP在没有拓展原生模块下或者没有增加修改一些原生的插件情况(如果不懂这个概念,后面会讲到)APP是可以使用wgt资源包升级的,整个升级过程用户可以是“无感知的”,我们通常在APP中可以看到所谓的升级提示,这个是有感知的;而有的时候你并没有升级这个APP缺发现ui变化,功能变化,那么这个时候就是无感知升级,技术手端也有很多,比如云端更新代码,在uniapp中我们可以用wgt来实现这样的功能。

                              我们先来APP普通的有感知更新,uniapp的实例。首先请求接口去请求服务器,拿到最新版本的下载地址,在这之前我们需要判断当前APP的版本号,那么这里就有一个小坑,我们千万不要使用下面这个API获取版本号。

                              plus.runtime.version
                              

                              我们如果使用wgt作为资源升级包的话,那么此API获取的版本号不是准确的,它会获取APP内核的应用版本号,我们必须要使用

                              plus.runtime.getProperty(plus.runtime.appid,function(inf){console.log(inf.version)});
                              

                              我们用此API获取到的版本号去数据库比对(前方伪代码):

                              // 比对版本方法, 此方法网友提供,侵权删除const compare = (curV, reqV) => {if (curV && reqV) {//将两个版本号拆成数字var arr1 = curV.split('.'),        arr2 = reqV.split('.');var minLength = Math.min(arr1.length, arr2.length),        position = 0,        diff = 0;//依次比较版本号每一位大小,当对比得出结果后跳出循环(后文有简单介绍)while (position < minLength && ((diff = parseInt(arr1[position]) - parseInt(arr2[position])) == 0)) {        position++;      }      diff = (diff != 0) ? diff : (arr1.length - arr2.length);//若curV大于reqV,则返回truereturn diff >= 0;    } else {//输入为空returnfalse;    }}
                              const downLoadFail=()=>{    uni.hideToast();    uni.showToast({title: "下载新版本失败,请在设置页面检查更新再试",duration: 2000,icon: "none",position: "bottom"    });}
                              
                              
                              // 获取更新列表,取最新的更新包constupdatePackageList=awaitgetUpdatePackageList();if(compare(updatePackageList[0].version,"根据上面的方法获取的版本号")){// 如果存在更新// 整包更新  uni.downloadFile({url: "整包APK的下载地址(非wgt包地址)",success: res => {if (res.statusCode === 200) {//下载之后打开临时路径的文件        plus.runtime.openFile(res.tempFilePath);      }else {        downLoadFail(); // 调用更新失败的方法      }    },fail: error => {      downLoadFail(); // 调用更新失败的方法    }  });}
                              
                              
                              

                              以上是普通的整包升级的伪代码,如果遇到强制更新,非强制更新,即开发者需要自己控制对应的button,这里就不阐述了。

                              对于wgt的更新,相比整包更新有一定区别,因为整包更新非常简单,无非就是下载apk文件,然后下完之后打开,让用户自己安装。wgt的生成需要在hbuilderx中操作(后续hbuilderx篇会讲到),我们需要把wgt包上传在服务器上,前方伪代码:

                              uni.downloadFile({url:"wgt下载地址",success: res => {if (res.statusCode === 200) {// 安装wgt        plus.runtime.install(res.tempFilePath, {  force: false        }, function() {  // wgt安装成功if(silence){            uni.showToast({title: "已更新最新的资源,重启应用获取更佳的用户体验",duration: 4000,icon: "none",position: "bottom"            });          }else {            plus.runtime.restart();// 不是静默升级,就立即重启应用          }        }, function(e) {if(!silence){            downLoadFail();          }        });       }else {        downLoadFail();      }    },fail: error => {      downLoadFail();    }  });
                              

                              细心的同学已经发现了,wgt安装需要用户重启,否则不会生效。所以我们在开发中会有一个silence的选项,如果是true那么会选择静默更新(用户无感知)这个时候我们只需要install安装即可,如果不是无感知,那么需要自动重启。

                              APP自定义tabbar

                              我们在企业级开发中,总会有一些沙雕产品想这种“凸起”或者“奇奇怪怪”的tabbar(希望我司产品不会看到)事实上,这种自定义tabbar对于uniapp开发者来说,是有一定的难度,首先如果是完全定制,那么需要有较强的webview功底或者subnvue功底。

                              由于自定义tabbar代码较多,我们可以借助一些插件来实现,但是值得注意的事情是,如果自定义tabbar在每一个页面都引用,会出现抖动闪烁的问题,所以我们应该在main.js中去draw这个tabbar

                              Vue.prototype.$tabbarView=newTabbarView();
                              

                              然后在每一个tabbar页面去watch对应的路径变化,然后改变当前选中是第几项item

                              onShow(){//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},onHide() {//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},
                              

                              这个watch等方法,是目前我司使用的组件的API,所以具体的定制tabbar思路知道了,我们使用一些第三方插件,可以尽量少踩一些坑。

                              引入原生安卓/IOS插件

                              ios/安卓原生的插件我们可以在【插件市场】(之后会讲到)中找到,我们在manifest.json配置本地插件即可,那么具体的插件的添加办法,如果是在插件市场直接点击添加到APP中。

                              如果我们是云打包(之后会说到这个概念)那么建议大家去社区直接购买然后添加,在配置文件中选中即可。

                              constPluginName=uni.requireNativePlugin(PluginName);
                              

                              此API只存在于APP端,所以需要条件编译,传入插件名称,就会在对应的APP中找已添加的插件是否存在(如果不懂这块,可以看下面一篇说明:《manifest.json:uniapp的半壁江山》),如果插件已被添加则正常使用。

                              我们这边实例选用的是uniapp官方文档中的demo例子,插件是官方提供的原生增强提示框:

                              constdcRichAlert=uni.requireNativePlugin('DCloud-RichAlert')dcRichAlert.show({    position: 'bottom',    title: "提示信息",    titleColor: '#FF0000',    content: "<a href='https://uniapp.dcloud.io/' value='Hello uni-app'>uni-app</a> 是一个使用 Vue.js 开发跨平台应用的前端框架!\n免费的\n免费的\n免费的\n重要的事情说三遍",    contentAlign: 'left',    checkBox: {        title: '不再提示',        isSelected: true    },    buttons: [{        title: '取消'    }, {        title: '否'    }, {        title: '确认',        titleColor: '#3F51B5'    }]}, result => {console.log(result)});
                              

                              说明一下引入原生插件的API,不管是vue还是nvue页面都可以使用。

                              如果要debug,那么就必须重新打一个自定义基座包(下面会讲到这个概念)否则不会生效。

                              manifest.json:uniapp的半壁江山

                              由于这个地方是uniapp配置项重中之重的地方,虽然用hbx直接预览它会帮助我们自动格式化,但是可能我的解释能让这块变得更简单。

                              app的ID是非常重要的,一般建立成功之后就不需要再更改,我们在注册微信开放平台,支付宝或者高德等等,都需要用到APP的ID和包名(packageName),包名设置会在之后介绍到,基本设置中包含了几个重要的信息,一个是应用版本名称一个是应用版本号,我们在做更新的时候需要+1,包括生成wgt包的时候。

                              我们可以在此处可视化的配置APP的模块权限,那么uniapp封装的模块权限真的是傻瓜式的,配置一些平台的key和secret然后再使用uniapp对应的api即可,那么肯定的事是这是增加包体积的。

                              引入原生插件,app如果有对应特殊的业务需求也可以编译原生的插件,具体如何引入本地原生插件,我们之前也提到过。

                              剩余的配置都是针对于小程序/H5的配置,所以根据我们自己的业务可以自己去查询文档。

                              uniapp应该使用哪种请求库呢?axios支持么?

                              uniapp是跨端框架,为了迎合跨端,我们不可以使用axios,因为axios不支持APP原生端,它仅仅支持网页端,所以我们可以使用uniapp提供的API(uni.request)一般这种API就够用,但是如果你想有像axios一样的配置体验的话,强烈建议你使用flyio.js这个库我们在新建项目时候会帮助我们安装好的。

                              uniapp项目是cli创建好还是hbx创建好?

                              创建uniapp项目,可以分为2种模式:

                              1. cli命令行创建
                              2. hbuilderX创建

                              结论:优先使用cli命令行创建,因为对于开发vue
                              web的人来说,cli命令行是最熟悉的,那么uniapp的cli方式创建的项目整体的目录结构类似于普通vue
                              web项目,而且我们直接使用npm快速安装依赖,像普通方式去引入;uniapp在版本支持上也是在cli项目上最先上线,可以使用cli体验到最新的功能,对于初学者来说,标签的学习是一个必须考虑进去的成本,cli方式创建的工程允许使用普通html标签,比如<div><span>它们在编译的时候会转换成uniapp的标签组件(尽管这样写并不推荐

                              hbx创建的uniapp项目,是随着hbx软件的版本升级而升级,cli创建的版本必须执行

                              npm update
                              

                              才能更新到最新版本,所以我建议大家搭建项目使用cli项目,在开发中使用uniapp的标签不要使用html标签。

                              社区

                              uniapp有着非常活跃的社区,这是真的切身体验过,使得整个开发遇到的问题我们都可以在社区找到(https://ask.dcloud.net.cn/

                              我们遇到了所有的bug,首选需要查询是否是自身开发问题,如果自测确认没问题那么就可以在社区发布bug贴,如图

                              发布成功之后我们在文章底部邀请,输入dcloud就可以邀请官方人员解答,如果出现bug,也可以上传代码压缩包(指定官方人员查看)这样可以更快地让官方找到问题帮助你解决问题。

                              插件市场

                              打一个广告先,我的UI库的alpha版本在插件市场发布,尽管现在不开放下载,大概在年底左右会重构给大家带来高质量的组件(名子叫i-uniapp)希望大家关注一下

                              https://ext.dcloud.net.cn/ 插件市场地址

                              插件市场有很多vue组件/原生sdk等等,具体的安装方法我们已经在前面提到了,使用hbx开发uniapp项目可以直接点击导入插件到项目中

                              HbuilderX:开发uniapp神器(本文章重点)

                              介绍Hbx和快速开发指南

                              我可以很负责任的说,虽然内核是eclipse,但是也算是国产的编辑器IDE,它在Vue的支持程度上足以让我震惊,说实话从Vue开发角度来说,HbuilderX比vscode好。但是hbx被吐槽地点也有很多,比如占用内存高,UI不如vscode/sublime等等,但是这是IDE之争不在我们文章的讨论重点之内,所以我们如果要开发uniapp项目,使用hbx开发是上上策

                              我们如果是使用vscode或者sublime,可以在hbx调整快捷键语法

                              插件安装,我目前安装的是这些,为了教程的顺利和以后开发者的开发顺利,请务必装这几个基础插件。

                              打包APP前的准备

                              我们首先来看一下打包APP需要什么

                              我们需要一个安卓证书,我们来生成一下这个安卓证书

                              1. 首先需要安装jdk,如果没有jdk,就去安装,然后在其bin文件夹下运行cmd
                                keytool-genkey-alias这里写证书别名-keyalgRSA-validity36500-keystore这里写证书名称.keystore

                              其中参数-validity为证书有效天数,我们可以写的大写。-alias后面是证书别名
                              输入密码的时候没有显示,就输入就行了。退格,tab等都属于密码内容,这个密码在给.apk文件签名的时候需要。输入这个命令之后会提示您输入秘钥库的口令
                              接着是会提示你输入:姓氏,组织单位名称,组织名称,城市或区域名称,省市,国家、地区代码,密钥口令。确认正确输入y,回车
                              作者:草字头乌君
                              链接:https://www.jianshu.com/p/14add4a02ed6 来源:简书

                              keystore的密码一定要记住,如果忘记需要执行另外一个命令去查询
                              生成keystore成功之后我们可以在其bin目录下发现一个以你设置为名字后缀为keystore文件

                              1. 填写包名
                                这个包名一般标准是以com开始的(java工程中的包名标准),ios不清楚,具体可以询问具体的ios开发,命名包名是一个非常重要的事情,因为包名关系着一个APP的主要信息,在上架应用商店的时候,包名则代表着APP的唯一性,所以一定要设置一个尽量贴合企业/个人的信息相关的名字,比如公司叫“将进酒”:
                                com.qiangjinjiu.andriod
                                或者公司有具体的规定,则按照自身公司进行设置,这里就不阐述了
                              2. 渠道包选项
                                对应的渠道包,可以在对应的平台做一个标识,可以在后台看到每个平台的使用指数,这个根据业务需求可以打开,一般不会选择
                              3. 原生混淆
                                可以对js和nvue文件进行原生混淆,提高安全性
                              4. 广告投放选择
                                uniapp自带的广告插件,根据业务需要自行选择

                              debug:自定义基座/小程序模拟器

                              我们在上一段讲解了如何打包APP,打包APP之前的准备工作,那么细心的同学会发现,打包APP中有这样的一个选项

                              什么是自定义基座,自定义基座是我们开发日常debug的包,这个包没有压缩,所以会比正式包大,正式包记得选择正式包不要选择基座包。自定义基座APP包安装到手机上,可以连接HbuilderX上,进行调试,下面介绍一下如何打自定义基座然后debug调试

                              1. 云打包:发布之前的编译阶段

                              1. 打包成功会返回成功信息,自定义基座打包

                              打包成功之后,我们拿起我们的诺基亚手机,打开usb调试模式(作为开发,如果连这个都不知道咋打开,那就去google吧),调试模式打开之后,一定要选中hbx左边的项目,然后点击运行,运行手机模拟器,如图(重点):

                              基座选择一定要选择自定义基座(如果你没打包自定义基座这里就没有这个选项),然后等待手机,会出现安装基座(app)的提示,点击安装,然后手机会自动打开APP,然后这个时候我们就可以更改代码,ui,然后手机及时更新变化。

                              那么debug自定义基座有什么注意事项呢?

                              1.当我们要调试登录,分享,地图等具有原生权限功能的话,需要配置对应平台的key和secret。那么我们增加,修改了原生插件配置/原生插件的修改,这个时候我们需要重新打包自定义基座查看最新的代码。

                              2.切记不要把自定义基座当成正式包发布

                              3.不要打包打正式包debug,因为云打包正式包一天只有免费20次,我们debug用自定义基座就行了。

                              那么我们觉得,日常的开发除了调试第三方登录,地图等需要用手机去实际测试,那么平时我们更改UI布局,难道也用自定义基座这么麻烦么?

                              答案是否定的,我们要知道,虽然布局UI在不同机型可能会产生偏差,但是我们日常可以使用小程序模拟器进行开发,然后开发完毕,在实体机型上进行初步测试,看看是否Ui或者功能有偏差,再具体更改,因为小程序模拟器是最接近APP的,大部分UI布局功能都是最贴近用户手机的。

                              那么使用小程序模拟器调试,这里也有一些注意事项,我们如果运行项目到微信小程序,如果是第一次,我们需要提前在小程序模拟器中的安全打开RFC调用

                              设置->安全->端口开启

                              然后此时我们可以使用hbx来启动微信小程序进行开发啦。

                              APP打包

                              我们项目模块迭代开发之后,需要打包成APP,那么我们有2种途径打包

                              1. 云打包
                              2. 离线打包

                              以及特殊的wgt资源包(对应之前提到的wgt更新)

                              首先我们来说一下云打包,云打包是dcloud提供的云打包服务器,我们在客户端HBX编译之后就会进入云打包的队列,由dcloud打包服务器打包成功之后返回给hbx控制台临时的下载链接。

                              那么在之前,我们已经了解了自定义基座,那么这个自定义基座的打包和云打包(正式包)只是一个选项的问题,云打包每天有20次的免费机会,所以切记我们debug千万不要生成线上包

                              离线打包指的是我们可以把uniapp工程生成出原生工程,由我们开发者自己去打包,那么我相信如果有原生基础的开发者会很乐意这样做,但是务必提醒一点就是社区很多的问题都是离线打包造成的,那么为了更稳定的APP开发,我建议使用云打包。

                              我们在之前提到的wgt资源包,我们应该这样生成,生成成功的wgt资源包上传到服务器,然后按照前面的demo进行更新

                              关于IOS

                              其实关于IOS,本人并不是了解很多,我可以简单讲一下,可能对于新手很重要。

                              1.uniapp的应用是可以被审核成功的,因为19年年底苹果发了一则通知,大概是性能差劲的套壳APP不会被上架,但是2020年,必须知道uniapp不是套壳APP,它也有原生宣传,webview渲染性能也是非常强劲的,没有网上说的那么不堪。
                              2.如果是windows用户那么我建议你使用mac开发uniapp,因为windows开发打包出的ios包不太方便去上架等等,而mac系统没有这个顾虑。
                              3.windows新手如果没有上架过,可以在windows下载ApplicationLoader, 如果是mac用户那么强烈推荐你下载Transporter App ,老司机就用xcode内置的上传吧~

                              拓展阅读
                              1.uniapp统计功能,免费,安全,统计数据非常详细,支持自定义埋点。

                              https://tongji.dcloud.net.cn/

                              2.uniapp云开发,现在云开发,云数据库这么火,早在几年前小程序就有这样的东西,云开发也是今年uniapp非常大的更新,现在支持腾讯云和阿里云,据说还比较便宜,感兴趣的小伙伴可以去看看。

                              https://unicloud.dcloud.net.cn/login

                              3.uniapp广告平台,非常适用于个人,个人做的APP可以通过这个广告平台变现。

                              https://uniad.dcloud.net.cn/login

                              这篇文章的背景就是在公司的一个项目,使用的技术栈是vue + vuecli3 + antdesign,由于早期的API接口有坑,antdesign的upload组件上传满足不了业务需求就用了iview的组件,所以当时是ant是全量引入,iview是按需引入了一个upload模块,当我们的打包出来的时候发现chunkjs很大,足足有近8m多,导致首屏加载时间很长,所以针对这一个问题通过几个方面来分析和优化。

                              分析模块

                              分析模块使用的是webpack-bundle-analyzer这个插件,按照说明的配置在plugins中即可,由于插件使用过于简单,我推荐的参考文章是
                              打包分析插件webpack-bundle-analyzer简介

                              像我所说的antdesign和iview技术栈混用这样的方式,本身就是不提倡的,所以大家在使用这个分析插件的时候,看看js文件的大小哪个是最大的,比如chunk vendors很大那就要看我们下面的配置说明来进行一步一步优化了

                              webpack4的splitchunks和runtimechunks (剖析被引用的js次数来缓存js)

                              我在看这方面的知识的时候,仅仅凭着以前一点点笔记来实践,总知踩了不少坑的;
                              首先来了解一下splitchunks的默认配置,这些配置讲真话不会用到全部的,很多的配置还存在着一知半解的状态下

                              optimization: {
                                  splitChunks: { // 如果这是一个空对象,那么分割的代码则是按照默认配置进行
                                  chunks: "all", // 只对异步或者同步或者全部的模块引入方式进行分割,all / async / initial
                                  minSize: 30000, // 引入的模块最小体积如果在值之类进行分割,否则不分割,这个是字节,计算/1000是kb
                                  // 同理还有maxSize,如果代码超出,将再次分割
                                  minChunks: 1, // 模块最小引入数量
                                  maxAsyncRequests: 5, // 最大异步并行最大请求数量,用途:控制分割的代码数量(默认是5)轻易不要更改
                                  maxInitialRequests: 3, // 入口并行最大请求数量,轻易不要更改
                                  automaticNameDelimiter: '~', // 分割代码连接字符串
                                  name: true, // 开启分割代码的文件名可定义(filename)
                                  cacheGroups: { // 缓存分组,此分组配置和chunks配置项必须是搭配,在判断引入的模块是异步还是同步之后需要走这个配置项进行分组,可以配置vendors为false
                              // 缓存组:顾名思义,将分割的代码暂时缓存起来,把所有匹配成功的分割代码进行整合打包在一个文件中
                              vendors: {
                                  test: /[\\/]node_modules[\\/]/,
                                  priority: -10, // 优先级:越小优先级越高,如果模块分别匹配条件和default成功,将通过此参数决定具体分配到哪个模块
                                  filename: "ventor.js" // 分割代码分组之后一起打包到哪个文件,这里设置文件名
                              },
                              default: { // 如果满足不了上面的缓存组,将执行下面的配置
                                  minChunks: 2, // 模块引入数量
                                  priority: -20,
                                  reuseExistingChunk: true // 如果开启了此配置,将分割代码中引入的模块(分割过的)直接引入分割后的地址,不再进行分割
                                  }
                              }
                              }
                              }
                              

                              我们可以将工程中的vue,lodash,moment等等js(分析出来的比较大的js)进行分割
                              在缓存组可以这样写

                              vue: {
                                  test: /vue/,
                                  chunks: "initial",
                                  name: "vuw",
                                  enforce: true,
                              }
                              

                              依次类推,我们的lodash和moment将会被提炼出来;

                              微信截图_20200607004425.png
                              那么从分割文件之后,我们的chunks vendors肯定会小,我们也可以加入runtimechunks来进行小文件的优化,将webpack的运行时文件进行打包,那么多个js文件将会共享这个运行时文件 runtimechunks文档

                              图片压缩

                              这一块图片压缩算是一个小小的技巧,也是通过tinypng进行的,这一节我们只讨论压缩,不讨论CDN之类的
                              首先tinypng有一个官网,它可以提供在线压缩免费服务的,一天也是50张压缩图片的额度且单张图片不能超过5m,一般来讲开发过程中也够用了,如果要在web服务中加入tingpng,那么需要获取key,再去npm安装相关tingpng依赖

                              我们来看一下无损压缩图片的压缩率和质量
                              微信截图_20200607002606.png

                              微信截图_20200607002802.png

                              另外说一句tinypng是提供ps插件的,价格也还算便宜,60刀,另外也有额外热心网友提供的tinypng无限制的API和压缩服务破解版tinypng

                              gzip服务

                              作为一个靠谱的前端从业者,gzip压缩技术一定要知晓并且最好会用,我们都知道在前端工程中js,css等资源在webpack的生产环境下会压缩,那么gzip技术会在这个的基础上再压缩至少百分之50以上,对webpack进行一些配置,就可以打包出来如下图的js

                              微信截图_20200607232243.png

                              并不是每个浏览器都支持gzip的,所以服务端会根据浏览器的请求头

                              Accept-Encoding: gzip, deflate;

                              这个是谷歌浏览器的支持程度,如果服务端解析请求体收到了gzip的兼容请求后会返回对应的gzip文件,反之如果不支持将会返回普通的js资源

                              npm i compression-webpack-plugin -d
                              
                              vue.config.js配置代码:
                              plugins: [
                                new CompressionWebpackPlugin({
                                  algorithm: 'gzip',
                                  test: /\.js$|\.html$|\.json$|\.css/,
                                  threshold: 10240,
                                  minRatio: 0.8
                                 })
                              ]

                              后端nginx只需要简单的配置即可开启,开启方法

                              这里有一个小坑,vuecli2是可以支持productionGzip只需要配置true或者false即可,不需要在脚手架配置额外其他内容,但是vuecli3是需要配置的

                              vue路由懒加载

                              曾经在做路由懒加载的时候有一段时间疑惑,既然是懒加载,为什么在打包出全部js后,浏览器还需要加载全部的js呢,如图:

                              微信截图_20200607235625.png

                              那么懒加载是不是就失去了它存在的意义,当我细细研究的时候,发现在加载第一次的时候,那些原本懒加载的js都打不开
                              微信截图_20200607235949.png

                              那么继续往下研究,通过和群友的探讨,他找到了关键性的代码(真心惭愧)

                              1. 首先他找到的是一句追加script的代码

                              微信图片_20200608000935.png

                              1. 我这边找到的是每一句js都会把当前路由写入到webpackJsonp这个数组中

                                (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[12]) //这里的12代表模块ID

                                然后在控制台中打印webpackJsonp这个全局变量会发现

                              微信截图_20200608000752.png

                              对应的数组中不仅仅有模块ID还有对应需要加载的模块,那么如果细细看每个被懒加载的js文件就会发现其中的require函数内部其实都做了一层优化,当第二次访问此模块就从这个数组拿取上次的缓存;

                              __webpack_require__.e和webpackJsonp

                              那么对代码分割的内部异步加载模块的源码解读可以看这篇文章,这也是我在总结这篇文章的时候给我很大帮助的资料

                              这篇文章中出现的CommonsChunkPlugin等等概念是webpack老版本出现,新版本则是spiltchunks

                              IgnorePlugin忽略指定的打包模块

                              使用IgnorePlugin插件可以帮助我们讲不必要的内容不进行打包

                              let webpack = require('webpack');
                              plugins: [
                                // 忽略解析三方包里插件
                                new webpack.IgnorePlugin(/\.\/locale/, /moment/)
                              ]

                              场景: moment中只引入了中文的包,可以通过这个配置进行把非中文的包去掉

                              OSS对象存储

                              这里说一下,OSS也是我近两年知道的,几年前在搭建这个博客的时候,阿里云有一些类似的活动套餐才让我了解这个东西,但是今年做开发的时候却第一次实际用到了OSS作为文件的存储服务,目前我的博客也采用了OSS作为附件资源的存储,价格也非常便宜,一年40G只需要9元

                              因为OSS经过了淘宝双十一的多年考验,同时OSS采用了高可用的架构设计,OSS的多重冗余架构设计,为数据持久存储提供可靠保障。
                              对比自建服务器存储,原始的存储方式基于硬件的影响,可能会出现各种突发问题,且人工数据的恢复有成本

                              安全方面oss为企业提供了很多的基础建设,加密相关

                              在我们这篇博客的主题,从10s到1s有很大一部分的功劳都是归于OSS

                              HTTP2

                              HTTP2在这次实践没有使用到,这块也和大家一起复习,因为在筹备这次的资料的时候,也补充了相关很多东西,在以前的HTTP1时代,前端如果被问到优化相关内容时候,一般通常会说雪碧图,内联样式,合并代码之类的,通过这些手端来达到HTTP的优化

                              HTTP1的一个概念叫做线头阻塞,会同时发起多个TCP请求,而这些请求都是线性流程的,一个资源下载完毕之后才能下载下一个资源,所以我们之前的优化手端“合并代码”才会非常流行,把所有的资源放在一个连接下去请求,但是在HTTP2看来,这些做法都不推荐。

                              微信图片_20200613223716.jpg

                              那么HTTP2主要有以下几个特性关乎到我们的优化

                              1. 多路复用
                                HTTP2允许我们将多个HTTP请求放在一个TCP连接中,避免了HTTP1中的建立多个TCP连接的开销,HTTP将会并行这些HTTP请求
                              2. 头部压缩
                                HTTP2减少了多个HTTP请求的开销,因为每一次的http2的请求都少于没压缩的http1的请求
                              3. 流的优先级
                                HTTP2可以浏览器指定接受资源的顺序,我们可以将较为重要的文件优先安排在前面
                              4. 服务端推送
                                服务端也可以主动推送额外的资源到客户端

                              那么HTTP1的优化观念是需要我们转变的呢?

                              【1】 不需要合并文件
                              不需要合并文件了,通常我们会把css放在一个文件中,js也是如此,webpack打包出来的chunk.js,合并过的css,多个图片合成的雪碧图,这样的作法太老套,如果现在这个时代还有人热衷于雪碧图,我会对此前端开发者的技术打一个大大的问号,必经这个玩意流行的时间是我入行之前都流行的(狗头)

                              微信图片_20200613223720.jpg

                              HTTP2中,合并文件虽然能压缩更多的文件体积,但是却增加了缓存的开销,一个被合并过的js,其中的内容被改变,那么浏览器承担的确是重新缓存整个合并的js,这样代价很大,而http2提倡的是【颗粒化】的传输,将多个文件的缓存利用到极致,这就是比http1好处的地方,还有一个关键的点就是在http1中你的网站可能没有全部使用合并的css和js,这样也无所谓,但是http2中加载这些没有使用过的资源,字节,是会缓慢首次加载的。

                              微信图片_20200613223724.jpg

                              我们可以将经常改动的内容js和不经常改动内容的js做一个区分,比如nodemodules打出来的chunk则是不经常改动的js,我们可以把这一块的资源进行CDN。

                              【2】不需要样式内联

                              在http1中,样式内联比如

                              <body style="color: red"></body>

                              这样单个的css不会产出新的css,浏览器不会去重新请求一个css,而是直接读取html,这样做的确可以达到优化目的在http1

                              微信图片_20200613223727.jpg

                              如图,多个HTML引入了同样的css内联,那么样式发生变化时,还需要请求多个html从服务端,这导致了用户在访问每一个页面都要额外的传输更多的东西。内联同样也会破坏优先级,因为我们在上面提到了http2是可以浏览器指定处理的优先级的,如果内联在html中,那么意味着这些css会和html同样的优先级,会导致http2的【流的优先级】没用,也就是说没按照偏好加载资源,但是在我查到的资料中,提到了

                              可以使用http2的服务端推送,告诉浏览器 “稍等,你刚请求的HTML页面过会渲染时会用到这些图像和CSS文件”,这样其实是和内联一样,还不会被破坏流的优先级,可以单独利用CDN缓存它们

                              【3】不需要细分域名
                              浏览器规定,单个域开通的tcp数量优先,原先为了浏览器能够并行的下载更多的资源,那么就开通多个域名,让浏览器有“并行”效果,但是http2出现了,多个域反而会造成多余的DNS查询和tcp连接,因为http2本来可以共享tcp进行多个http请求,同样的它也会破坏流的优先级,因为浏览器不会通过域不同比较其谁更优先

                              关于域名细分更多的内容都在参考文章中

                              还有很多http1和http2公用的优化手端

                              比如CDN,浏览器缓存

                              那么更重要的一点就是,http2的性能提升还是得看自己的业务,要是http请求很少,那我个人觉得没有必要

                              关于http2的服务端推送这个知识点我没有听说过和使用过,所以大家可以查询更多相关资料了解并且实践它们

                              结语

                              在以后的面试中,webpack,gulp等打包工具,浏览器性能,CDN,优化手端已经成为了高级前端的必考点,所以雪碧图,合并文件等等这些手端就尽量别说了,这篇文章从vue技术栈出发,讲解了企业实战的优化指南,从发现性能问题到解决性能问题提供了一系列的思路,那么这不是最终版,优化的方案远远不止这么多,希望大家一起学习,还有一些手写的图,字很丑多见谅

                              参考文章

                              关于入门了解splitchunks
                              webpack输出文件分析源码
                              oss相关优势-阿里云官网
                              http2性能优化指南

                              基于NPM的包或者库,项目中的package.json是对项目的描述,这个json对象中的script标签就是npm运行脚本,vue.js在这里配置了如下的内容

                              "build": "node build/build.js",
                              "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", (**注意这一块是用逗号分割的**)
                              "build:weex": "npm run build -- weex"
                              

                              vue当然有很多script命令,但是仅仅只有这几种是build的,build:ssr和build:weex其实和build一样,只不过提供了不同的运行参数

                              查找build的入口文件,vue是如何做build源码的?

                              打开对应的build/build.js

                              let builds = require('./config').getAllBuilds()
                              // filter builds via command line arg
                              if (process.argv[2]) {
                                const filters = process.argv[2].split(',') (**分割出来的数组是["web-runtime-cjs","web-server-renderer"]** )
                               // 通过getAllBuild函数返回的config对象对其打包模式进行了rollup的配置,包括output等设置
                               // 而下面这一段代码的b则是返回的config对象,filters这个参数数组使用some来判断builds中output中的file(被resolve函数定义了,这个resolve函数下面会具体讨论)
                                builds = builds.filter(b => {
                                  return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
                                })
                              } else {
                                // filter out weex builds by default
                                builds = builds.filter(b => {
                                  return b.output.file.indexOf('weex') === -1
                                })
                              }
                              

                              这一段代码其实非常简单,作者也在源码中写了注释解释了这段代码的作用

                              通过命令行arg构建过滤器
                              

                              其中引入的config 文件调用了getAllBuilds这个方法,在config.js可以看出

                               exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
                              

                              这一句代码是导出config的关键代码,builds是预定义的一些对象

                               // runtime-only build (Browser)
                                'web-runtime-dev': {
                                  entry: resolve('web/entry-runtime.js'),
                                  dest: resolve('dist/vue.runtime.js'),
                                  format: 'umd',
                                  env: 'development',
                                  banner
                                },
                              

                              entry: 入口文件
                              dest: 输出目标文件
                              format: 构建的格式 umd为umd格式 cjs遵循commonJS规范 es遵循esmodule规范
                              env:开发模式/生产模式
                              banner指的是每个js的页头,比如作者,信息,开源协议等信息

                              还有其他的一些关于rollup(很像webpack的一些设计)别名诸如此类的配置,这边就不阐述了,因为我也忘记的差不多了,文档都很好查,想知道具体的意思也很容易

                              resolve函数的定义

                              这里的resolve函数比较简短,很容易理清

                              //假定一个config中使用resolve这个函数,它传递的字符串是这样的
                              'web-runtime-cjs': {
                                  entry: resolve('web/entry-runtime.js')
                                }
                              
                              const resolve = p => {
                                const base = p.split('/')[0]
                                if (aliases[base]) {
                                  return path.resolve(aliases[base], p.slice(base.length + 1))
                                } else {
                                  return path.resolve(__dirname, '../', p)
                                }
                              }
                              

                              首先base则是 “web”这个字符串,
                              这个base并不是真实的路径,而这个web则指向了aliases的配置

                              module.exports = {
                                web: resolve('src/platforms/web'),
                              }
                              

                              这里的web指向的路径就是src/platforms/web
                              那么resolve函数返return的就是path.resolve,其中第一个参数就是web,第二个参数则是entry-runtime
                              所以由此得知,通过这样的一个过程找到了build的入口文件然后经过rollup的打包就会在dist目录下生成web/entry-runtime.js

                              拓展阅读

                              Runtime Only VS Runtime + Compiler 推荐阅读

                              在初级和中级前端面试笔试中,函数的防抖和节流要求候选人有能力去精通或者熟悉里面的机制,不仅仅要知道基本版的防抖和节流,还要知道更完美进阶的节流防抖,比如一些lodash中的实现等等,所以这个系列也是从基础版开始,我会和大家一起补充这方面的知识


                              配套代码地址在线调试和编辑
                              参考文章

                              防抖

                              【背景故事】
                              某商业楼每天早上上班高峰期的电梯非常堵,通常生活来讲,一个电梯肯定是要进去很多人的直到人满为止,这对于自身和电梯都是一种节约资源的表现,不可能出现每个人一人一个电梯的情况,所以对于电梯和我们,这是一种防抖函数的表现。

                              电梯每当停到一个楼层,会等我们进去,人没有塞满或者没有人它才会到达指定楼层。
                              在web端最容易遇到的就是input搜索,用户输入一个字符,会在指定的时间中监听是否还有字符进入,如果没有,那么就执行对应的函数,这样就解决了用户频繁输入字符而前端发起请求给服务器造成压力的问题

                              // 防抖函数
                              function debounce(fn, time = 300) {
                                let timer = null;
                                return function() {
                                  clearTimeout(timer);
                                  timer = setTimeout(() => {
                                    fn.apply(this, arguments);
                                  }, time);
                                };
                              }
                              

                              基础版的代码梳理,通过这个函数将fn传递,就能实现防抖的效果,当用户输入的第一个字符,会把timer函数赋予一个延时器,这个延时器执行了对应的fn,this是为了绑定执行的this指向,arguments则是这个函数的参数类数组对象,当用户在延时还未触发的时候再次输入字符,那么clearTimeout把上一个timer清除掉了,因为闭包的作用,timer不会被销毁,这样的一个函数就表示了,只要在延时间隔中(此时timer被赋值了),如果谁再调用此函数,会把上一个timer清空掉,重新延时,依次类推...

                              节流

                              【背景故事】
                              在0几年的时候包括可能现在都有,很多家庭知道水管把阀门开到最小,让水一滴一滴的掉下,是不会走水表的(不计费的),所以在我很小的时候就知道这个事情,虽然现在没有了,但是还是觉得很羞愧,节流函数就是起到的作用就是,把原本频繁的调用变得有秩序有间隔;

                              以监听scroll滚动为例:

                              // 节流函数
                              function throttle(fn, time = 300) {
                                let canRun = true;
                                return function() {
                                  if (!canRun) return;
                                  canRun = false;
                                  setTimeout(() => {
                                    fn.apply(this, arguments);
                                    canRun = true;
                                  }, time);
                                };
                              }
                              
                              $(window).on(
                                "scroll",
                                throttle(() => {
                                  console.log("触发了scroll");
                                }, 300)
                              );
                              

                              在这个函数中,canRun这个变量变化了3次,判断了一次如果为false就return掉

                              1. 初始化时设置为true
                              2. 判断canRun是否为false后设置了canRun为false
                              3. 最终事件执行完毕之后设置canRun为true,否则就进入不了下一次的节流

                              其实这个函数从开始到结束的canRun形成了一次循环,这个循环唯一保证的就是只有事件结束才可以开始下一个事件

                              总结

                              节流:保证回调能在指定的时间间隔中依次有规律的执行
                              防抖:保证回调能在指定的时间间隔中等待是否有再次执行回调,如果没有则执行,如果有则重置继续等待;