十年下载自动微信抢红包生涯一瞬间,下载自动微信抢红包即人生!
  • 大多的时候,都是生活选择你,而不是你去选择生活,尽管如此不情愿,可是必须去面对,这就是人生。我只是站在原地,慢慢承受着,学会隐忍,我会努力,我会回归最初的淡漠,再见,自以为是的青春,再见,想象中的美好,当现实赤裸裸的告诉你必须去面对的时候,除了微笑,只能微笑。
  • 在人生的牌局中,发牌的是上帝,打牌的却是我们自己。我们不能左右天气,但是可以改变心情;我们不能预见明天,但是可以珍惜今天;我们不能改变别人,但是可以改变自己。不要委屈自己,既然改变不了现实,那就好好把握自己的人生。世界上没有绝望的人生,只有对人生处境产生绝望的人。
  • 自信是成功的前提,勤奋是成功的催化剂。
公告
悬壶济世更多
  • 发布会总结:VR游戏最前线,Oculus 的新动向都在这里了
    ? 距离E3游戏展开幕还有不到一周时间,Oculus(简称OC)特地挑在了这个时点发布了今年最新的动向。? 准确地说,今晚OC并没有发布头盔新品,而是向数万名开发者们更清晰地描述了未来的游戏是怎样的。Oculus官方更是在会后豪言,“E3?Begins?Today.”? 跟?WWDC?和?Google?IO?不同,OC?发布会从一开始就选择了游戏视频站Twitch直播(而非传统大家喜好的YouTube)。当然,后者的关注规模没法跟前两者相比,OC在发布会前仅有3万多的全球观众,峰值也只有6-7万人。而在国内主流的VR圈内讨论活跃,但暂时还没有主流科技媒体(除雷锋网外)投入很大的关注,更多的还有VR行业媒体(比如VRerse和87870)在第一时间报道。? 回到这次发布会的主体,大致可以分成3个部分:头盔更新、输入设备、游戏demo。? 1.Oculus?Rift?消费者版更多细节? ? Oculus?CEO?Brendan?Iribe?开场迅速切入了主题(因为OC还没有太多往年数字需要吹水):? 初代?Oculus?Rift?消费者版(Consumer?Version?1,CV1)会是一个单手就能轻松拿起的头盔,分辨率为2160X1200。? 相比第二代开发者版(Developer?Kit?2,DK2),CV1?将具有更广阔的视野和更可靠的头部追踪系统,新的追踪系统除了头盔本身的追踪点优化以外,还会有一个升级版桌面摄像头与之配套。? ? CV1?自带耳机,提供360度的VR音效,画面和声音完美同步,同时支持戴着眼镜佩戴。而且如果你嫌原装耳机不够烧,可以摘下替换成你自己的。? 另外在物理特性上,CV1瞳距可一个按键人肉调整,整个头盔用布料包裹,手感更佳,佩戴也更舒适,不压迫脸部。? 总之,就是轻量、新版追踪系统、360度音效、瞳距可调。? 2.输入设备:Xbox手柄+Oculus?Touch控制器? ? OC宣布与Xbox的合作被此前很多的国内开发者们猜到。? 发布会上,OC公布的第一款输入外设就是Xbox手柄。Xbox的负责人Phil?Spence讲Win?10会原生支持Oculus?Rift,Rift也很有诚意地支持Xbox上所有的2D游戏。? 不过,这些都不是重点。手柄是早期教育和培养用户很好的一类成熟设备,但对VR这种强调沉浸感的体验,终将变成“过渡设备”。? ? 然后是Oculus?Touch,OC特地在讲完众多游戏demo之后在众人濒临尿点时压轴。? Oculus?Touch?开发代号叫“半月”,物如其名,Touch呈半月状,顶部是按钮和操纵杆。? Touch?周身是跟头盔一样的光学追踪点,应该也是配合新版的桌面摄像头使用,能跟踪玩家在空间里水平和垂直的位置。? ?? 看看实物图(这几款设备在E3上都能体验)。? ? ? 3.游戏展示,越多越好? 发布会上大约一半的时间在讲各种现有的Demo,官方表示E3上还会有更多。? 举栗:? 座舱对战游戏Valkryie,在E3上会展示,明年发布? ? Gunfire?Games的RPG游戏Chronos? ? ? INsomniac?games的第三人称动作冒险游戏Edge?of?Nowhere? ? Oculus现在正在合作的一些游戏开发商。? ? 此外Oculus?Share平台会重新打造,给VR开发者们服务,同时官方宣布要至少拿出1000万美金支持独立开发者。? 最后,CV1的发货时间仍然是明年Q1,当然没有更具体的时间节点,恐怕我们还是得等9月份的?Oculus?Connect。Oculus?Touch的发货时间也是明年Q1。? 各位,几天后的?E3?再见,届时?Oculus?将会公布消费者版Rift头盔的详细配置。? ? ? ? ?
    ? ? ?
    ? ??
    ?

    转载请注明:前端录?发布会总结:VR游戏最前线,Oculus?的新动向都在这里了

    ? ?
  • HTML5结合ajax实现文件上传以及进度显示


    ????????

    基于原生html5实现,不需要falsh支持,进度可以自定义显示,控制灵活,?本来打算使用jquery插件进行异步文件上传,比如uploadfy但是需要额外的支持,也有人用iframe模仿异步上传机制,感觉都比较别扭。因为项目不考虑低版本浏览器,所以决定用html5实现。下面只是一个简单的demo,具体样式需要自己去做。?


    后台基于strut2进行文件处理,具体因项目而定。只是要注意设置文件大小的限制。??这个配置根据具体情况设定,超过此值会报404.?


    首先是上传页面,比较简单,附带了文件上者这个参数。?


    upload.jsp?


    <%@page?language="java"?pageEncoding="UTF-8"?contentType="text/html;?charset=UTF-8"%>?<%String?path?=?request.getContextPath();?%>????使用XMLHttpRequest上传文件????????var?xhr?=?new?XMLHttpRequest();????????//监听选择文件信息????function?fileSelected()?{?????//HTML5文件API操作???????var?file?=?document.getElementById('fileName').files[0];???????if?(file)?{?????????var?fileSize?=?0;?????????if?(file.size?>?1024?*?1024)???????????fileSize?=?(Math.round(file.size?*?100?/?(1024?*?1024))?/?100).toString()?+?'MB';?????????else???????????fileSize?=?(Math.round(file.size?*?100?/?1024)?/?100).toString()?+?'KB';


    ?????????document.getElementById('fileName').innerHTML?=?'Name:?'?+?file.name;?????????document.getElementById('fileSize').innerHTML?=?'Size:?'?+?fileSize;?????????document.getElementById('fileType').innerHTML?=?'Type:?'?+?file.type;???????}?????}????????//上传文件????function?uploadFile()?{???????var?fd?=?new?FormData();???????//关联表单数据,可以是自定义参数???????fd.append("name",?document.getElementById('name').value);???????fd.append("fileName",?document.getElementById('fileName').files[0]);


    ???????//监听事件???????xhr.upload.addEventListener("progress",?uploadProgress,?false);???????xhr.addEventListener("load",?uploadComplete,?false);???????xhr.addEventListener("error",?uploadFailed,?false);???????xhr.addEventListener("abort",?uploadCanceled,?false);???????//发送文件和表单自定义参数???????xhr.open("POST",?"<%=path%>/user/uploadifyTest_doUpload");???????xhr.send(fd);?????}????//取消上传?function?cancleUploadFile(){??xhr.abort();?}????????//上传进度????function?uploadProgress(evt)?{???????if?(evt.lengthComputable)?{?????????var?percentComplete?=?Math.round(evt.loaded?*?100?/?evt.total);?????????document.getElementById('progressNumber').innerHTML?=?percentComplete.toString()?+?'%';???????}???????else?{?????????document.getElementById('progressNumber').innerHTML?=?'unable?to?compute';???????}????}


    ????//上传成功响应????function?uploadComplete(evt)?{?????//服务断接收完文件返回的结果?????alert(evt.target.responseText);????}??????//上传失败????function?uploadFailed(evt)?{??????alert("上传失败");????}?//取消上传????function?uploadCanceled(evt)?{?????alert("您取消了本次上传.");????}??????????选择文件????

  • 上传者:????


    fd.append("name",?document.getElementById('name').value);fd.append("fileName",?document.getElementById('fileName').files[0]);


    这两句是把数据绑定到表单。因为html5支持多文件上传,所以?document.getElementById('fileName').files返回的是数组。这里只有一个文件所以取下标0的元素。?xhr.upload.addEventListener("progress",?uploadProgress,?false);?xhr.addEventListener("load",?uploadComplete,?false);?xhr.addEventListener("error",?uploadFailed,?false);?xhr.addEventListener("abort",?uploadCanceled,?false);这里绑定进度、上传、错误、中断的事件,提供一些交互。文件进度显示就是在progress回调中进行显示的。?


    然后贴上后台下载自动微信抢红包和action配置,UploadifyTestAction.java?







    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36


    package?com.bjhit.eranges.actions.test;
    import?java.io.File;
    import?com.opensymphony.xwork2.ActionSupport;
    public?class?UploadifyTestAction?extends?ActionSupport?{
    ?private?static?final?long?serialVersionUID?=?837481714629791752L;
    ?private?File?fileName;
    ?private?String?name;
    ?private?String?responseInfo;
    ?public?String?doUpload()?throws?Exception?{
    ??System.out.println(name);
    ??File?myFile?=?fileName;
    ??System.out.println(myFile.getName());
    ??responseInfo?=?"上传成功!";
    ??return?"doUpload";
    ?}
    ?public?String?getName()?{
    ??return?name;
    ?}
    ?public?void?setName(String?name)?{
    ??this.name?=?name;
    ?}
    ?public?File?getFileName()?{
    ??return?fileName;
    ?}
    ?
    ?public?void?setFileName(File?fileName)?{
    ??this.fileName?=?fileName;
    ?}
    ?
    ?public?String?getResponseInfo()?{
    ??return?responseInfo;
    ?}
    ?public?void?setResponseInfo(String?responseInfo)?{
    ??this.responseInfo?=?responseInfo;
    ?}
    }action配置?






    1
    2
    3


    private?ImageView?image;????
    image?=?(ImageView)?findViewById(R.id.xx_id);????
    image.setAlpha(0);?这样基本的上传功能了。

    ?


    ?????
    ????
    ????
  • inline如果大家关心html5的话,你应该知道在HTML5中也包含了几个新的元素,例如,section,article等等,但是仍旧遵循这里我们介绍的显示类型。inline类型的元素包括:?img,span,a等,用来定义文字或者数据,通常显示方式是“同一行显示”。更具体的说就是,如果很多的inline类型的元素在同一行的时候,它们会显示在同一行,直到宽度不够显示了,再转到下一行。例如,如下下载自动微信抢红包:1
    <a?href="http://gbtags.com">gbtags.com</a>?is?website?for?<span>geeks</span>
    查看在线调试相?反block类型的元素,例如,div,p或者HTML5中新的元素section,article和article的显示方式和inline类型都不一?样。它们都是典型的结构化的元素,可以包含inline类型的元素。浏览器处理block类型的元素,会在元素前后添加换行,这样你看到它们都是独立成行?显示的。当然,如果你修改它的缺省显示类型为inline,它就会按照inline元素的显示方式显示。样式CSS通常我们都是使用CSS来控制元素的显示:1
    2
    3
    sometag{???
    ??display:inline;?/*当然你也可以设置为block,none等等支持的属性*/
    }
    虽然上面的属性中我们指定了显示类型,这同时也意味这其它相关的样式,例如,你可以指定显示类型为block的元素的宽和高,但是?inline类型的无法指定。padding(内边距)和margin(外边距)可以被应用到inline显示的元素,但是不会影响包含的元素。看看如下?例子:查看在线调试另外一些显示方式除了inline和block类型的显示,这里还有一个inline-block的显示方式。如下图:它显示的方式类似于inline,但是它相关的属性,例如,宽度,高度还有padding/margin等等遵循于block显示类型的规则。inline-block可以帮助我们实现类似float元素的效果,但是也有自己的问题。其它的属性例如,list-item,顾名思义,显示的方式和列表元素类似。1
    2
    3
    4
    <ul>???
    ??<li>元素1</li>???
    ??<li>元素2</li>?
    </ul>
    最后还有一个元素需要提一下就是”none”,这个属性可以让元素不显示,并且不占聚任何的document空间。?注意和“hidden”这个属性区别一下。正常Document的加载流程那?么什么是浏览器正常的加载流程呢?基本上浏览器按照它解析的顺序来显示内容,顶端的先加载,然后加载下面的内容。当大家开始做web设计的时候,可能都不?关心正常的document加载过程,而只醉心于各种不同的绚丽花哨的技巧,如果你能够正确的理解document加载,对于更好的帮助你理解web设计?绝对有利无弊。做一个练习吧!在这里我们将做一个简单的联系帮助你巩固你的学习,这里我们使用HTML5?Shiv来帮助我们支持HTML5标签,使用placekitten这个图片占位应用来生成图片。HTML框架下载自动微信抢红包如下:1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ????<div>
    ????????<h1>An?Intro?to?Normal?Document?Flow.</h1>
    ????????

    ????????????<img?src="http://placekitten.com/g/100/200"?alt=""?/>
    ????????????<img?src="http://placekitten.com/g/160/200"?alt=""?/>
    ????????????<img?src="http://placekitten.com/g/220/200"?alt=""?/>
    ????????????<img?src="http://placekitten.com/g/180/200"?alt=""?/>
    ????????????<img?src="http://placekitten.com/g/240/200"?alt=""?/>
    ????????????<img?src="http://placekitten.com/g/130/200"?alt=""?/>
    ????????</figure>
    ????????

    ????????????<h2>?欢迎访问我的喵星人图片画廊</h2>
    ????????????<p>如果你也喜欢猫咪的话,来一起看看这些可爱的小生命吧!</p>
    ????????</article>
    ????</div>
    ????

    ??????
    欢迎访问<a?href="http://gbtags.com">gbtags.com</a></section>
    ????</footer>
    CSS布局下载自动微信抢红包:1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    body{
    ??font-size:12px;
    ??font-family:?Arial;
    }
    ?
    .container?{
    ????width:?85%;
    ????margin:0?auto;
    ????background:?#f2f2f2;
    }
    ?
    figure?img?{
    ????padding-left:?10px;
    }
    ?
    h1?{
    ????font-family:?Verdana,?Geneva,?Arial,?Helvetica,?sans-serif;
    ????color:?#707070;
    ????padding:?10px;
    ????font-size:14px;
    }
    ?
    article?{
    ????background:?#505050;
    ????color:?#f2f2f2;
    ????padding:?10px;
    }
    ?
    footer{
    ??width:?85%;
    ??margin:0?auto;
    }
    ?
    section{
    ??padding:?20px?0;
    }
    查看在线调试总结希望通过这篇文章的学习,大家能够更好的了解document的加载和布局,如果你有任何问题或者建议,请给我们留言,谢谢!
    ?????
    ????
    ????
  • jQuery中的常见数据存储及队列处理函数


    ?????????1、data(name)返回元素上储存的相应名字的数据,可以用data(name,?value)来设定。如果jQuery集合指向多个元素,那将只返回第一个元素的对应数据。这个函数可以用于在一个元素上存取数据而避免了循环引用的风险。jQuery.data是1.2.3版的新功能。可以在很多地方使用这个函数,另外jQuery?UI里经常使用这个函数。返回值?Any参数????name?(String)?:存储的数据名?示例:$(function()?{????//在一个div上存取数据????$("div").data("blah");?//?undefined????alert($("div").data("blah"));????$("div").data("blah",?"hello?world");?//?blah设置为hello?world????$("div").data("blah");?//?hello????$("div").data("blah",?123);?//?设置为123????$("div").data("blah");?//?123????alert($("div").data("blah"));????$("div").removeData("blah");?//移除blah????$("div").data("blah");?//?undefined?????//在一个div上存取名/值对数据?????$("div").data("test",?{?first:?456,?last:?"jeff?wong!"?});????alert($("div").data("test").first);?//456;????alert($("div").data("test").last);?//jeff?wong?})
    2、removeData(name)在元素上移除存放的数据与$(...).data(name,?value)函数作用相反返回值?jQuery参数????name?(String)?:存储的数据名?示例:$(function()?{????$("div").data("test",?{?first:?456,?last:?"jeff?wong!"?});????alert($("div").data("test").first);?//456;????alert($("div").data("test").last);?//jeff?wong????$("div").removeData("test");?//移除test????alert($("div").data("test"));?//undefined;})
    3、queue([name])返回指向第一个匹配元素的队列(将是一个函数数组)返回值?Array参数????name?(String)?:队列名,默认为fx?示例://通过设定队列数组来删除动画队列$("#btnShow").click(function()?{????var?n?=?$("div").queue("fx");????$("span").text("Queue?length?is:?"?+?n.length);});function?runIt()?{????$("div").show("slow");????$("div").animate({?left:?'+=200'?},?2000);????$("div").slideToggle(1000);????$("div").slideToggle("fast");????$("div").animate({?left:?'-=200'?},?1500);????$("div").hide("slow");????$("div").show(1200);????$("div").slideUp("normal",?runIt);}runIt();文档片段:<html?xmlns="http://www.w3.org/1999/xhtml"><head?runat="server">????<title>title>????<style>????????div????????{????????????margin:?3px;????????????width:?40px;????????????height:?40px;????????????position:?absolute;????????????left:?0px;????????????top:?30px;????????????background:?green;????????????display:?none;????????}????????div.newcolor????????{????????????background:?blue;????????}????????span????????{????????????color:?red;????????}????style>????<script?src="js/jquery-1.3.1.js"?type="text/javascript">script>head><body>????<form?id="form1"?runat="server">???????<button?id="btnShow"?type="button">????????Show?Length?of?Queuebutton>????<span?style="color:Red">span>????form>????<script?src="js/jQTest.js"?type="text/javascript">script>body>html>4、queue([name],callback)在匹配的元素的队列最后添加一个函数返回值?jQuery参数????name?(String)?:队列名,默认为fx?callback?(Function)?:?要添加进队列的函数示例://插入一个自定义函数?如果函数执行后要继续队列,则执行?jQuery(this).dequeue();jQuery(document).ready(function()?{????$(document.body).click(function()?{????????$("div").show("slow");????????$("div").animate({?left:?'+=200'?},?2000);????????$("div").queue(function()?{????????????$(this).addClass("newcolor");????????????$(this).dequeue();????????});????????$("div").animate({?left:?'-=200'?},?500);????????$("div").queue(function()?{????????????$(this).removeClass("newcolor");????????????$(this).dequeue();????????});????????$("div").slideUp();????});});文档片段:<html><head>??<style>??div?{?margin:3px;?width:40px;?height:40px;????????position:absolute;?left:0px;?top:30px;?????????background:green;?display:none;?}??div.newcolor?{?background:blue;?}style>head><body>??Click?here??<div>div>body>html>5、queue([name],queue)将匹配元素的队列用新的一个队列来代替(函数数组).返回值?jQuery参数????name?(String)?:队列名,默认为fx?queue?(Array)?:?用于替换的队列。所有函数都有同一个参数,这个值与queue(callback)相同示例://通过设定队列数组来删除动画队列?jQuery(document).ready(function()?{????$("#start").click(function()?{????????$("div").show("slow");????????$("div").animate({?left:?'+=200'?},?5000);????????$("div").queue(function()?{????????????$(this).addClass("newcolor");????????????$(this).dequeue();????????});????????$("div").animate({?left:?'-=200'?},?1500);????????$("div").queue(function()?{????????????$(this).removeClass("newcolor");????????????$(this).dequeue();????????});????????$("div").slideUp();????});????$("#stop").click(function()?{????????$("div").queue("fx",?[]);?//通过设定队列数组来删除动画队列?????????$("div").stop();????});});文档片段:<html><head>??<style>??div?{?margin:3px;?width:40px;?height:40px;????????position:absolute;?left:0px;?top:30px;?????????background:green;?display:none;?}??div.newcolor?{?background:blue;?}??</style></head><body>??<button?id="start">Start</button>??<button?id="stop">Stop</button>??<div></div></body></html>6、dequeue([name])从队列最前端移除一个队列函数,并执行它。返回值?jQuery参数????name?(String)?:队列名,默认为fx?示例:$("button").click(function()?{????$("div").animate({?left:?'+=200px'?},?2000);????$("div").animate({?top:?'0px'?},?600);????$("div").queue(function()?{????????$(this).toggleClass("red");????????$(this).dequeue();?//用dequeue来结束自定义队列函数,并让队列继续进行下去。?????});????$("div").animate({?left:?'10px',?top:?'30px'?},?700);});文档片段:<html><head>??<style>??div?{?margin:3px;?width:50px;?position:absolute;????????height:50px;?left:10px;?top:30px;?????????background-color:yellow;?}??div.red?{?background-color:red;?}??style>head><body>??<button>Startbutton>??<div>div>body>html>?ps:上面的一些示例用到了一些jquery特效函数,因为本篇主要阐述核心函数,对于单个特效函数这里不再详细说明使用细节。
    ?????
    ????
    ????
  • 浅谈HTTP缓存

    写在前面:最近学习了修言同学的小册,受益良多。对于HTTP缓存这一块,经过资料查询和思考,也有了自己的一些思考认识,希望分享出来与大家一起讨论和成长。

    内容概述
    • 什么是缓存及缓存的优点
    • 缓存的处理步骤
    • 强缓存和协商缓存
    • 缓存决策
    • 总结与思考
    一、缓存及其优点 缓存

    缓存是一种可以自动保存常见资源副本并可以在下一次请求中直接使用副本而非再次获取的技术。

    也就是说,当我们首次进行资源请求之后,服务器在返回资源给客户端的同时,缓存服务器或本地缓存也会保存一份资源副本(在允许缓存的情况下),当我们下次再对该资源进行请求时,则会直接使用资源副本而不会从原始服务器再次请求文档。

    缓存的优点
    1. 缓存可以减少冗余的数据传输。
    2. 缓存可以缓解网络瓶颈的问题。
    3. 缓存可以降低对原始服务器的要求。
    4. 缓存可以降低请求的距离时延。
    数据的冗余传输

    当很多客户端访问同一份文档的时候,原始服务器一遍又一遍地返回给不同的客户端相同内容的文档,这些重复的文档造成了数据的冗余传输。

    网络瓶颈问题

    在大部分情况下,客户端访问代理服务器的速度总是比访问原始服务器更快(带宽大、延迟低),因此如果代理服务器能够提供一份完整的副本,则远远比从原始服务器获取来的快且省流量——尤其针对大文件来说。

    降低原始服务器要求

    突发事件(比如爆炸性新闻、某个名人事件)使很多人几乎同时去访问一个Web文档时,??就会出现瞬间拥塞。由此造成的过多流量峰值可能会使网络和Web服务器产生崩溃。使用缓存便可在一定程度上降低对原服务器的压力。

    降低请求距离时延

    在物理上的距离,也是降低web性能的一个方面。对于同一份资源,原服务器离请求端越近,资源的获取速度则会越快。

    二、强缓存和协商缓存 1、?缓存相关概念解释 缓存命中

    如果某个请求的结果是由已缓存的副本提供的,被称作缓存命中。

    缓存未命中

    如果缓存中没有可用的副本或者副本已经过期,则会将请求转发至原始服务器,这被称作缓存未命中?。

    新鲜度检测

    HTTP通过缓存将服务器文档的副本保留一段时间。在这段时间里,?都认为文档是“新鲜的”,缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副本停留的时间太长,超过了文档的新鲜度限值(freshness?limit),?就认为对象“过时”了,在提供该文档之前,缓存要再次与服务器进行确认,以查看文档是否发生了变化。

    再验证

    原始服务器上的内容可能会随时变化,缓存需要经常对其进行检测,看看它保存的副本是否仍是服务器上最新的副本。这些新鲜度检测被称为?HTTP?再验证。

    缓存可以随时对副本进行再验证,但大部分缓存只在客户端发起请求,并且副本旧得足以需要检测的时候,才会对副本进行再验证。

    再验证命中和再验证未命中

    缓存对缓存的副本进行再验证时,会向原始服务器发送一个再验证请求,如果内容没有发生变化,服务器会以304?Not?Modified进行响应。这被称作是再验证命中或者缓慢命中。如果内容发生了变化,服务器会以200进行响应。这被称作再验证未命中。

    2、?缓存的处理步骤
    • 首先是当用户请求资源时,会判断是否有缓存,如果没有,则会向原服务器请求资源。
    • 如果有缓存,则会进入强缓存的范畴,判断缓存是否新鲜,如果缓存新鲜,则会直接返回缓存副本给客户端。如果缓存不新鲜了,则表示强缓存失败,将会进入到协商缓存。
    • 协商缓存将判断是否存在Etag和Last-Modified首部,通过这些首部验证资源是否发生过变化,如果未发生变化,则表示命中了协商缓存,会重定向到缓存副本,将资源返回给客户端,否则的话表示协商缓存未命中,服务器会返回新的资源。

    大家可以先看几遍这张图,在脑海里对缓存的过程有一个宏观的了解,接下来我会对这张图上的各个部分进行解读。等到最后,再回来看这张图便会觉得缓存是如此简单的一个过程了。

    3、?强缓存和协商缓存的概念 强缓存

    服务端告知客户端缓存时间后,由客户端判断并决定是否使用缓存。

    即首次发起请求时,服务端会在Response?Headers?中写入缓存新鲜时间。当请求再次发出时,如果缓存新鲜,将直接从缓存获取资源,而不会再与服务器发生通信。

    协商缓存

    由服务端决定并告知客户端是否使用缓存。

    协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

    4、?强缓存和协商缓存的实现原理 (1)?强缓存实现原理

    强缓存是通过Expires首部或Cache-Control:?max-age来实现的。

    Expires?和?Cache-Control:?max-age都是用来标识资源的过期时间的首部。

    Expires(HTTP/1.0)

    Expires描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示。

    由于expires是一个绝对时间,如果人为的更改时间,会对缓存的有效期造成影响,使缓存有效期的设置失去意义。因此在http1.1中我们有了expires的完全替代首部cache-control:max-age

    Cache-Control(HTTP/1.1)

    max-age值是一个相对时间,它定义了文档的最大使用期——从第一次生成文档到文档不再新鲜、无法使用为止,最大的合法生存时间(以秒为单位)。

    过程说明
    • 当我们首次请求资源时,服务器在返回资源的同时,会在Response?Headers中写入expires首部或cache-control,标识缓存的过期时间,缓存副本会将该部分信息保存起来。
    • 当再次请求该资源的时候,缓存会对date(Date?是一个通用首部,表示原始服务器消息发出的时间。即表示的是首次请求某个资源的时间。)和expires/cache-control的时间进行对比,从而判断缓存副本是否足够新鲜。

    (2)?协商缓存实现原理

    协商缓存是通过请求头Last-Modified或Etag来实现的。

    Last-Modified?标识的是文档最后修改时间,Etag?则是以文档内容来进行编码的。

    Last-Modified

    说明:

    • 首次请求资源时,服务器在返回资源的同时,会在Response?Headers中写入Last-Modified首部,表示该资源在服务器上的最后修改时间。
    • 当再次请求该资源时,会在Request?Headers?中写入If-Modified-Since首部,此时的If-Modified-Since的值是首次请求资源时所返回的Last-Modified的值。
    • 服务器接收到请求后,会根据If-Modified-Since的值判断资源在该日期之后是否发生过变化。
    • 如果没有,则会返回304?Not?Modified;如果变化了,则会返回变化过后的资源,同时更新Last-Modified的值。

    (1)资源未更新network面板截图

    首次请求:

    再次请求:

    (2)资源发生更新network面板截图

    首次请求:

    再次请求:(大家可以看到Last-Modified和If-Modified-Since标识的时间不一样了)

    Etag

    我们可以看到,Etag的实现过程和Last-Modified完全一样,具体过程可参照Last-Modified,在这里就不做过多介绍了。

    Last-Modified存在的一些问题

    有些文档可能会被周期性地重写,但实际包含的数据常常是一样的。尽管内容没有变化,但修改日期会发生变化。

    有些文档可能被修改了,但所做修改并不重要,不需要让缓存重载数据(比如对拼写或注释的修改)。

    有些服务器提供的文档会在亚秒间隙发生变化(比如,实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。

    通过这些描述,我们可以总结出一些Last-Modified存在的缺陷:

    1. 无法感知文件内容是否真的发生了变化。 不该重新请求的时候,也会重新请求。
    2. 在秒以下的内容变化无法感知到。 该重新请求的时候,反而没有重新请求。

    对于上述问题,Etag作为Last-Modified的补充而出现,Etag?是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的?Etag?就是不同的,因此?Etag?能够精准地感知文件的变化。

    Etag?强验证器和弱验证器

    ETag?分为强验证器和弱验证器。

    强验证器要求文档的每个字节都相等,而弱验证器只要求文档的含义相等。

    强验证:

    弱验证(前面会加上‘?W/’?来标识):

    5、?Cache-Control请求头常用属性说明 max-age/s-maxage

    s-maxage指令的功能和max-age是相同的,它们唯一的不同点就在于s-maxage指令只适用于代理服务器缓存。s-maxage的优先级高于max-age。

    public/private

    public?与?private?是针对资源是否能够被代理服务缓存而存在的一组对立概念。

    如果我们为资源设置了?public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了?private,则该资源只能被浏览器缓存。

    no-cache/no-store

    no-cache?表示客户端要求缓存在提供其已缓存的副本之前必须先和原始服务器对该文档进行验证。即强制跳过强缓存阶段,直接进行协商缓存。强缓存并不能知道缓存是否真的足够新鲜,使用no-cache就是为了防止从缓存中返回过期的资源,对缓存进行再验证。

    no-store表示的是禁止缓存,即每一次都是直接与原服务器进行通信,从原服务器返回资源。一般设置了no-store的资源,都暗示着该资源具有敏感性信息。

    6、优先级问题 (1)Expires?和?Cache-Control:?max-age

    应用HTTP/1.1版本的缓存服务器遇到同时存在Expires首部字段的情况时,会优先处理max-age指令,而忽略掉Expires首部字段。 而HTTP/1.0版本的缓存服务器的情况则相反,max-age指令会被忽略掉。

    (2)Last-Modified?和?Etag(存疑部分)

    看了很多资料都说Etag的优先级高于Last-Modified,但是又有资料说当Etag和Last-Modified同时存在时,是由二者共同决定标识文档是否发生变化的。

    因此我对这里的优先级做了这样一番解读:当二者同时存在时,浏览器会优先判断Etag,如果If-None-Match和服务器资源最后修改时间不一样,则表示文件发生过变化,则直接返回200,此时不需要再对If-Modified-Since做检查。当Etag命中时,才会判断Last-Modified是否也命中,只有当二者都命中的情况下,才从缓存中获取缓存副本。

    注意:此番观点不知是否正确,但觉得这样合乎情理,欢迎大家一起讨论或是指点一二。

    三、缓存决策

    由于并未有过http缓存方面的实际应用经验,在缓存决策方面实在没有什么自己的见解。

    四、思考和总结

    学习关于HTTP缓存方面的东西花费了不少时间,发现里面的概念和知识点比较的杂乱,一开始无法将整个缓存的过程串联起来。通过对博客和书籍的查阅,终于梳理清楚了缓存的流程及各个步骤中涉及到的概念和知识点。既对整个流程有了清晰的把控,也能对流程的各个环节和细节有一定的了解。

    思考了很久应该如何行文,才能够既把大的流程讲述清楚,又能够兼顾每一步涉及到的东西。 希望通过分享,能够给大家带来一些新的东西和思考,那我也就满足了。

    最后回忆一遍缓存的过程,在脑海里画出这张图:

    最后感谢大家的阅读,辛苦啦^_^

    如有错误,欢迎指正?weginjun@163.com

    资料参考

    • 掘金小册——前端性能优化原理与实践
    • 浅谈浏览器http的缓存机制
    • HTTP权威指南?(提取码:4e45)
    • 图解HTTP
  • 用微前端的方式搭建类单页应用
    前言

    微前端由ThoughtWorks?2016年提出,将后端微服务的理念应用于浏览器端,即将?Web?应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

    美团已经是一家拥有几万人规模的大型互联网公司,提升整体效率至关重要,这需要很多内部和外部的管理系统来支撑。由于这些系统之间存在大量的连通和交互诉求,因此我们希望能够按照用户和使用场景将这些系统汇总成一个或者几个综合的系统。

    我们把这种由多个微前端聚合出来的单页应用叫做“类单页应用”,美团HR系统就是基于这种设计实现的。美团HR系统是由30多个微前端应用聚合而成,包含1000多个页面,300多个导航菜单项。对用户来说,HR系统是一个单页应用,整个交互过程非常顺畅;对开发者同学来说,各个应用均可独立开发、独立测试、独立发布,大大提高了开发效率。

    接下来,本文将为大家介绍“微前端构建类单页应用”在美团HR系统中的一些实践。同时也分享一些我们的思考和经验,希望能够对大家有所启发。

    HR系统的微前端设计

    因为美团的HR系统所涉及项目比较多,目前由三个团队来负责。其中:OA团队负责考勤、合同、流程等功能,HR团队负责入职、转正、调岗、离职等功能,上海团队负责绩效、招聘等功能。这种团队和功能的划分模式,使得每个系统都是相对独立的,拥有独立的域名、独立的UI设计、独立的技术栈。但是,这样会带来开发团队之间职责划分不清、用户体验效果差等问题,所以就迫切需要把HR系统转变成只有一个域名和一套展示风格的系统。

    为了满足公司业务发展的要求,我们做了一个HR的门户页面,把各个子系统的入口做了链接归拢。然而我们发现HR门户的意义非常小,用户跳转两次之后,又完全不知道跳到哪里去了。因此我们通过将HR系统整合为一个应用的方式,来解决以上问题。

    一般而言,“类单页应用”的实现方式主要有两种:

    1. iframe嵌入
    2. 微前端合并类单页应用

    其中,iframe嵌入方式是比较容易实现的,但在实践的过程中带来了如下问题:

    • 子项目需要改造,需要提供一组不带导航的功能
    • iframe嵌入的显示区大小不容易控制,存在一定局限性
    • URL的记录完全无效,页面刷新不能够被记忆,刷新会返回首页
    • iframe功能之间的跳转是无效的
    • iframe的样式显示、兼容性等都具有局限性

    考虑到这些问题,iframe嵌入并不能满足我们的业务诉求,所以我们开始用微前端的方式来搭建HR系统。

    在这个微前端的方案里,有几个我们必须要解决的问题:

    1. 一个前端需要对应多个后端
    2. 提供一套应用注册机制,完成应用的无缝整合
    3. 构建时集成应用和应用独立发布部署

    只有解决了以上问题,我们的集成才是有效且真正可落地的,接下来详细讲解一下这几个问题的实现思路。

    一个前端对应多个后端

    HR系统最终线上运行的是一个单页应用,而项目开发中要求应用独立,因此我们新建了一个入口项目,用于整合各个应用。在我们的实践中,把这个项目叫做“Portal项目”或“主项目”,业务应用叫做“子项目”,整个项目结构图如下所示:

    “Portal项目”是比较特殊的,在开发阶段是一个容器,不包含任何业务,除了提供“子项目”注册、合并功能外,还可以提供一些系统级公共支持,例如:

    • 用户登录机制
    • 菜单权限获取
    • 全局异常处理
    • 全局数据打点

    “子项目”对外输出不需要入口HTML页面,只需要输出的资源文件即可,资源文件包括js、css、fonts和imgs等。

    HR系统在线上运行了一个前端服务(Node?Server),这个Server用于响应用户登录、鉴权、资源的请求。HR系统的数据请求并没有经过前端服务做透传,而是被Nginx转发到后端Server上,具体交互如下图所示:

    转发规则上限制数据请求格式必须是?系统名+Api做前缀??这样保障了各个系统之间的请求可以完全隔离。 其中,Nginx的配置示例如下:

    
    server?{
    ????listen??????????80;
    ????server_name?????xxx.xx.com;
    
    ????location??/project/api/?{
    ????????set?$upstream_name?"server.project";
    ????????proxy_pass??http://$upstream_name;
    ????}
    ????...
    
    ????location??/?{
    ????????set?$upstream_name?"web.portal";
    ????????proxy_pass??http://$upstream_name;
    ????}
    }
    
    

    我们将用户的统一登录和认证问题交给了SSO,所有的项目的后端Server都要接入SSO校验登录状态,从而保障业务系统间用户安全认证的一致性。

    在项目结构确定以后,应用如何进行合并呢?因此,我们开始制定了一套应用注册机制。

    应用注册机制

    “Portal项目”提供注册的接口,“子项目”进行注册,最终聚合成一个单页应用。在整套机制中,比较核心的部分是路由注册机制,“子项目”的路由应该由自己控制,而整个系统的导航是“Portal项目”提供的。

    路由注册

    路由的控制由三部分组成:权限菜单树、导航和路由树,“Portal项目”中封装一个组件App,根据菜单树和路由树生成整个页面。路由挂载到DOM树上的下载自动微信抢红包如下:

    let?Router?=?<Router
    ????????????fetchMenu?=?{fetchMenuHandle}
    ????????????routes?=?{routes}
    ????????????app?=?{App}
    ????????????history?=?{history}
    ????????????>
    ReactDOM.render(Router,document.querySelector("#app"));
    
    

    Router是在react-router的基础上做了一层封装,通过menu和routes最后生成一个如下所示的路由树:

    ??
    ????<Route?path="/"?component={App}>
    ??????<Route?path="/namespace/xx"?component={About}?/>
    ??????<Route?path="inbox"?component={Inbox}>
    ????????<Route?path="messages/:id"?component={Message}?/>
    ??????Route>
    ????Route>
    ??Router>
    
    

    具体注册使用了全局的window.app.routes,“Portal项目”从window.app.routes获取路由,“子项目”把自己需要注册的路由添加到window.app.routes中,子项目的注册如下:

    let?app?=?window.app?=?window.app?||?{};?
    app.routes?=?(app.routes?||?[]).concat([
    {
    ??code:'attendance-record',	
    ??path:?'/attendance-record',
    ??component:?wrapper(()?=>?async(require('./nodes/attendance-record'),?'kaoqin')),
    }]);
    
    

    路由合并的同时也把具体的功能做了引用关联,再到构建时就可以把所有的功能与路由管理起来。项目的作用域要怎么控制呢?我们要求“子项目”间是彼此隔离,要避免样式污染,要做独立的数据流管理,我们用项目作用域的方式来解决这些问题。

    项目作用域控制

    在路由控制的时候我们提到了?window.app,我们也是通过这个全局App来做项目作用域的控制。window.app包含了如下几部分:

    let?app?=?window.app?||?{};
    app?=?{
    ????require:function(request){...},
    ????define:function(name,context,index){...},
    ????routes:[...],
    ????init:function(namespace,reducers){...}???????
    };
    
    

    window.app主要功能:

    • define??定义项目的公共库,主要用来解决JS公共库的管理问题
    • require?引用自己的定义的基础库,配合define来使用
    • routes?用于存放全局的路由,子项目路由添加到window.app.routes,用于完成路由的注册
    • init?注册入口,为子项目添加上namesapce标识,注册上子项目管理数据流的reducers

    子项目完整的注册,如下所示:

    import?reducers?from?'./redux/kaoqin-reducer';
    let?app?=?window.app?=?window.app?||?{};?
    app.routes?=?(app.routes?||?[]).concat([
    {
    ??code:'attendance-record',	
    ??path:?'/attendance-record',
    ??component:?wrapper(()?=>?async(require('./nodes/attendance-record'),?'kaoqin')),
    ??//?...?其他路由
    }]);
    ?
    function?wrapper(loadComponent)?{
    ??let?React?=?null;
    ??let?Component?=?null;
    ??let?Wrapped?=?props?=>?(
    ????<div?className="namespace-kaoqin">
    ??????<Component?{...props}?/>
    ????div>
    ??);
    ??return?async?()?=>?{
    ????await?window.app.init('namespace-kaoqin',reducers);
    ????React?=?require('react');
    ????Component?=?await?loadComponent();
    ????return?Wrapped;
    ??};
    }
    
    

    其中做了这几件事情:

    1. 把路由添加到window.app中
    2. 业务第一次功能被调用的时候执行?window.app.init(namespace,reducers),注册项目作用域和数据流的reducers
    3. 对业务功能的挂载节点包装一个根节点:Component挂载在classNamenamespace-kaoqindiv下面

    这样就完成了“子项目”的注册,“子项目”的对外输出是一个入口文件和一系列的资源文件,这些文件由webpack构建生成。

    CSS作用域方面,使用webpack在构建阶段为业务的所有CSS都加上自己的作用域,构建配置如下:

    //webpack打包部分,在postcss插件中?添加namespace的控制
    config.postcss.push(postcss.plugin('namespace',?()?=>?css?=>
    ??css.walkRules(rule?=>?{
    ????if?(rule.parent?&&?rule.parent.type?===?'atrule'?&&?rule.parent.name?!==?'media')?return;
    ????rule.selectors?=?rule.selectors.map(s?=>?`.namespace-kaoqin?${s?===?'body'???''?:?s}`);
    ??})
    ));
    
    

    CSS处理用到postcss-loader,postcss-loader用到postcss,我们添加postcss的处理插件,为每一个CSS选择器都添加名为.namespace-kaoqin的根选择器,最后打包出来的CSS,如下所示:

    .namespace-kaoqin?.attendance-record?{
    ????height:?100%;
    ????position:?relative
    }
    
    .namespace-kaoqin?.attendance-record?.attendance-record-content?{
    ????font-size:?14px;
    ????height:?100%;
    ????overflow:?auto;
    ????padding:?0?20px
    }
    ...?
    
    

    CSS样式问题解决之后,接下来看一下,Portal提供的init做了哪些工作。

    let?inited?=?false;
    let?ModalContainer?=?null;
    app.init?=?async?function?(namespace,reducers)?{
    ??if?(!inited)?{
    ????inited?=?true;
    ????let?block?=?await?new?Promise(resolve?=>?{
    ??????require.ensure([],?function?(require)?{
    ????????app.define('block',?require.context('block',?true,?/^\.\/(?!dev)([^\/]|\/(?!demo))+\.jsx?$/));
    ????????resolve(require('block'));
    ??????},?'common');
    ????});
    ????ModalContainer?=?document.createElement('div');
    ????document.body.appendChild(mtfv3ModalContainer);
    ????let?{?Modal}?=?block;
    ????Modal.getContainer?=?()?=>?ModalContainer;
    ??}
    ??ModalContainer.setAttribute('class',?`${namespace}`);
    ??mountReducers(namepace,reducers)
    };
    
    

    init方法主要做了两件事情:

    1. 挂载“子项目”的reducers,把“子项目”的数据流挂载了redux上
    2. “子项目”的弹出窗全部挂载在一个全局的div上,并为这个div添加对应的项目作用域,配合“子项目”构建的CSS,确保弹出框样式正确

    上述下载自动微信抢红包中还看到了app.define的用法,它主要是用来处理JS公共库的控制,例如我们用到的组件库Block,期望每个“子项目”的版本都是统一的。因此我们需要解决JS公共库版本统一的问题。

    JS公共库版本统一

    为了不侵入“子项目”,我们采用构建过程中替换的方式来做,“Portal项目”把公共库引入进来,重新定义,然后通过window.app.require的方式引用,在编译“子项目”的时候,把引用公共库的下载自动微信抢红包从require('react')全部替换为window.app.require('react'),这样就可以将JS公共库的版本都交给“Portal项目”来控制了。

    define?的下载自动微信抢红包和示例如下:

    /**
    *?重新定义包
    *?@param?name??引用的包名,例如?react
    *?@param?context?资源引用器?实际上是?webpackContext(是一个方法,来引用资源文件)
    *?@param?index?定义的包的入口文件
    */
    app.define?=?function?(name,?context,?index)?{
    ??let?keys?=?context.keys();
    ??for?(let?key?of?keys)?{
    ????let?parts?=?(name?+?key.slice(1)).split('/');
    ????let?dir?=?this.modules;
    ????for?(let?i?=?0;?i?1;?i++)?{
    ??????let?part?=?parts[i];
    ??????if?(!dir.hasOwnProperty(part))?{
    ????????dir[part]?=?{};
    ??????}
    ??????dir?=?dir[part];
    ????}
    ????dir[parts[parts.length?-?1]]?=?context.bind(context,?key);
    ??}
    ??if?(index?!=?null)?{
    ????this.modules[name]['index.js']?=?this.modules[name][index];
    ??}
    };
    //定义app的react?
    //定义一个react资源库:把原来react根目录和lib目录下的.js全部获取到,绑定到新定义的react中,并指定react.js作为入口文件
    app.define('react',?require.context('react',?true,?/^.\/(lib\/)?[^\/]+\.js$/),?'react.js');
    app.define('react-dom',?require.context('react-dom',?true,?/^.\/index\.js$/));
    
    

    “子项目”的构建,使用webpack的externals(外部扩展)来对引用进行替换:

    /**
    ?*?对一些公共包的引用做处理?通过webpack的externals(外部扩展)来解决
    ?*/
    const?libs?=?['react',?'react-dom',?"block"];
    
    module.exports?=?function?(context,?request,?callback)?{
    ????if?(libs.indexOf(request.split('/',?1)[0])?!==?-1)?{
    ????????//如果文件的require路径中包含libs中的?替换为?window.app.require('${request}');?
    ????????//var在这儿是声明的意思?
    ????????callback(null,?`var?window.app.require('${request}')`);
    ????}?else?{
    ????????callback();
    ????}
    };
    
    

    这样项目的注册就完成了,还有一些需要“子项目”自己改造的地方,例如本地启动需要把“Portal项目”的导航加载进来,需要做mock数据等等。

    项目的注册完成了,我们如何发布部署呢?

    构建后集成和独立部署

    在HR系统的整合过程中,开发阶段对“子项目”是“零侵入”,而在发布阶段,我们也希望如此。

    我们的部署过程,大概如下:

    第一步:在发布机上,获取下载自动微信抢红包、安装依赖、执行构建; 第二步:把构建的结果上传到服务器; 第三步:在服务器执行?node?index.js?把服务启动起来。

    “Portal项目”构建之后的文件结构如下:

    “子项目”构建后的文件结构如下:

    线上运行的文件结构如下:

    把“子项目”的构建文件上传到服务器对应的“子项目”文件目录下,然后对“子项目”的资源文件进行集成合并,生成.dist目录中的文件,提供给用户线上访问使用。

    每次发布,我们主要做以下三件事情:

    1. 发布最新的静态资源文件
    2. 重新生成entry-xx.js和index.html(更新入口引用)
    3. 重启前端服务

    如果是纯静态服务,完全可以做到热部署,动态更新一下引用关系即可,不需要重启服务。因为我们在Node服务层做了一些公共服务,所以选择了重启服务,我们使用了公司的基础服务和PM2来实现热启动。

    对于历史文件,我们需要做版本控制,以保障之前的访问能够正常运行。此外,为了保证服务的高可用性,我们上线了4台机器,分别在两个机房进行部署,最终来提高HR系统的容错性。

    总结

    以上就是我们使用React技术栈和微前端方式搭建的“类单页应用”HR业务系统,回顾一下这个技术方案,整个框架流程如下图所示:

    在产品层面上,“微前端类单页应用”打破了独立项目的概念,我们可以根据用户的需求自由组装我们的页面应用,例如:我们可以在HR门户上把考勤、请假、OA审批、财务报销等高频功能放在一起。甚至可以让用户自己定制功能,让用户真的感受到我们是一个系统。

    “微前端构建类单页应用”方案是基于React技术栈开发,如果把路由管理机制和注册机制抽离出来作为一个公共的库,就可以在webpack的基础上封装成一个业务无关性的通用方案,而且使用起来非常的友好。

    截止目前,HR系统已经稳定运行了1年多的时间,我们总结了以下三个优点:

    1. 单页应用的体验比较好,按需加载,交互流畅
    2. 项目微前端化,业务解耦,稳定性有保障,项目的粒度易控制
    3. 项目的健壮性比较好,项目注册仅仅增加了入口文件的大小,30多个项目目前只有12K
    作者简介

    贾召,2014年加入美团,先后主导了OA、HR、财务等企业项目的前端搭建,自主研发React组件库Block,在Block的基础上统一了整个企业平台的前端技术栈,致力于提高研发团队的工作效率。

  • webpack4项目中的实践

    学习webpack4的配置更改

    webpack作为一个模块打包器,主要用于前端工程中的依赖梳理和模块打包,将我们开发的具有高可读性和可维护性的下载自动微信抢红包文件打包成浏览器可以识别并正常运行的压缩下载自动微信抢红包,主要包括样式文件处理成css,各种新式的JavaScript转换成浏览器认识的写法等,也是前端工程师进阶的不二法门。

    webpack.config.js配置项简介
    1. Entry:入口文件配置,Webpack?执行构建的第一步将从?Entry?开始,完成整个工程的打包。
    2. Module:模块,在Webpack里一切皆模块,Webpack会从配置的Entry开始递归找出所有依赖的模块,最常用的是rules配置项,功能是匹配对应的后缀,从而针对下载自动微信抢红包文件完成格式转换和压缩合并等指定的操作。
    3. Loader:模块转换器,用于把模块原内容按照需求转换成新内容,这个是配合Module模块中的rules中的配置项来使用。
    4. Plugins:扩展插件,在Webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。(插件API)
    5. Output:输出结果,在Webpack经过一系列处理并得出最终想要的下载自动微信抢红包后输出结果,配置项用于指定输出文件夹,默认是./dist
    6. DevServer:用于配置开发过程中使用的本机服务器配置,属于webpack-dev-server这个插件的配置项。
    webpack打包流程简介
    • 根据传入的参数模式(development?|?production)来加载对应的默认配置
    • entry里配置的module开始递归解析entry所依赖的所有module
    • 每一个module都会根据rules的配置项去寻找用到的loader,接受所配置的loader的处理
    • entry中的配置对象为分组,每一个配置入口和其对应的依赖文件最后组成一个下载自动微信抢红包块文件(chunk)并输出
    • 整个流程中webpack会在恰当的时机执行plugin的逻辑,来完成自定义的插件逻辑
    基本的webpack配置搭建

    首先通过以下的脚本命令来建立初始化文件:

    npm?init?-y
    npm?i?webpack?webpack-cli?-D?//?针对webpack4的安装
    mkdir?src?&&?cd?src?&&?touch?index.html?index.js
    cd?../?&&?mkdir?dist?&&?mkdir?static
    touch?webpack.config.js
    npm?i?webpack-dev-server?--save-dev
    

    修改生成的package.json文件,来引入webpack打包命令:

    "scripts":?{
    ????"build":?"webpack?--mode?production",
    ????"dev":?"webpack-dev-server?--open?--mode?development"
    }
    

    webpack.config.js文件加入一些基本配置loader,从而基本的webpack4.x的配置成型(以两个页面入口为例):

    const?path?=?require('path');
    const?CopyWebpackPlugin?=?require('copy-webpack-plugin')?//?复制静态资源的插件
    const?CleanWebpackPlugin?=?require('clean-webpack-plugin')?//?清空打包目录的插件
    const?HtmlWebpackPlugin?=?require('html-webpack-plugin')?//?生成html的插件
    const?ExtractTextWebapckPlugin?=?require('extract-text-webpack-plugin')?//CSS文件单独提取出来
    const?webpack?=?require('webpack')
    
    module.exports?=?{
    ????entry:?{
    ????????index:?path.resolve(__dirname,?'src',?'index.js'),
    ????????page:?path.resolve(__dirname,?'src',?'page.js'),
    ????????vendor:'lodash'?//?多个页面所需的公共库文件,防止重复打包带入
    ????},
    ????output:{
    ????????publicPath:?'/',??//这里要放的是静态资源CDN的地址
    ????????path:?path.resolve(__dirname,'dist'),
    ????????filename:'[name].[hash].js'
    ????},
    ????resolve:{
    ????????extensions:?[".js",".css",".json"],
    ????????alias:?{}?//配置别名可以加快webpack查找模块的速度
    ????},
    ????module:?{
    ????????//?多个loader是有顺序要求的,从右往左写,因为转换的时候是从右往左转换的
    ????????rules:[
    ????????????{
    ????????????????test:?/\.css$/,
    ????????????????use:?ExtractTextWebapckPlugin.extract({
    ????????????????????fallback:?'style-loader',
    ????????????????????use:?['css-loader',?'postcss-loader']?//?不再需要style-loader放到html文件内
    ????????????????}),
    ????????????????include:?path.join(__dirname,?'src'),?//限制范围,提高打包速度
    ????????????????exclude:?/node_modules/
    ????????????},
    ????????????{
    ????????????????test:/\.less$/,
    ????????????????use:?ExtractTextWebapckPlugin.extract({
    ????????????????????fallback:?'style-loader',
    ????????????????????use:?['css-loader',?'postcss-loader',?'less-loader']
    ????????????????}),
    ????????????????include:?path.join(__dirname,?'src'),
    ????????????????exclude:?/node_modules/
    ????????????},
    ????????????{
    ????????????????test:/\.scss$/,
    ????????????????use:?ExtractTextWebapckPlugin.extract({
    ????????????????????fallback:?'style-loader',
    ????????????????????use:['css-loader',?'postcss-loader',?'sass-loader']
    ????????????????}),
    ????????????????include:?path.join(__dirname,?'src'),
    ????????????????exclude:?/node_modules/
    ????????????},
    ????????????{
    ????????????????test:?/\.jsx?$/,
    ????????????????use:?{
    ????????????????????loader:?'babel-loader',
    ????????????????????query:?{?//同时可以把babel配置写到根目录下的.babelrc中
    ??????????????????????presets:?['env',?'stage-0']?//?env转换es6?stage-0转es7
    ????????????????????}
    ????????????????}
    ????????????},
    ????????????{?//file-loader?解决css等文件中引入图片路径的问题
    ????????????//?url-loader?当图片较小的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader?进行拷贝
    ????????????????test:?/\.(png|jpg|jpeg|gif|svg)/,
    ????????????????use:?{
    ??????????????????loader:?'url-loader',
    ??????????????????options:?{
    ????????????????????outputPath:?'images/',?//?图片输出的路径
    ????????????????????limit:?1?*?1024
    ??????????????????}
    ????????????????}
    ????????????}
    ????????]
    ????},
    ????plugins:?[
    ????????//?多入口的html文件用chunks这个参数来区分
    ????????new?HtmlWebpackPlugin({
    ????????????template:?path.resolve(__dirname,'src','index.html'),
    ????????????filename:'index.html',
    ????????????chunks:['index',?'vendor'],
    ????????????hash:true,//防止缓存
    ????????????minify:{
    ????????????????removeAttributeQuotes:true//压缩?去掉引号
    ????????????}
    ????????}),
    ????????new?HtmlWebpackPlugin({
    ????????????template:?path.resolve(__dirname,'src','page.html'),
    ????????????filename:'page.html',
    ????????????chunks:['page',?'vendor'],
    ????????????hash:true,//防止缓存
    ????????????minify:{
    ????????????????removeAttributeQuotes:true//压缩?去掉引号
    ????????????}
    ????????}),
    ????????new?webpack.ProvidePlugin({
    ????????????_:'lodash'?//所有页面都会引入?_?这个变量,不用再import引入
    ????????}),
    ????????new?ExtractTextWebapckPlugin('css/[name].[hash].css'),?//?其实这个特性只用于打包生产环境,测试环境这样设置会影响HMR
    ????????new?CopyWebpackPlugin([
    ????????????{
    ????????????????from:?path.resolve(__dirname,?'static'),
    ????????????????to:?path.resolve(__dirname,?'dist/static'),
    ????????????????ignore:?['.*']
    ????????????}
    ????????]),
    ????????new?CleanWebpackPlugin([path.join(__dirname,?'dist')]),
    ????],
    ????devtool:?'eval-source-map',?//?指定加source-map的方式
    ????devServer:?{
    ????????contentBase:?path.join(__dirname,?"dist"),?//静态文件根目录
    ????????port:?3824,?//?端口
    ????????host:?'localhost',
    ????????overlay:?true,
    ????????compress:?false?//?服务器返回浏览器的时候是否启动gzip压缩
    ????},
    ????watch:?true,?//?开启监听文件更改,自动刷新
    ????watchOptions:?{
    ????????ignored:?/node_modules/,?//忽略不用监听变更的目录
    ????????aggregateTimeout:?500,?//防止重复保存频繁重新编译,500毫米内重复保存不打包
    ????????poll:1000?//每秒询问的文件变更的次数
    ????},
    }
    

    在命令行下用以下命令安装loader和依赖的插件,生成完全的package.json项目依赖树。

    npm?install?extract-text-webpack-plugin@next?--save-dev
    npm?i?style-loader?css-loader?postcss-loader?--save-dev
    npm?i?less?less-loader?--save-dev
    npm?i?node-sass?sass-loader?--save-dev
    npm?i?babel-core?babel-loader?babel-preset-env?babel-preset-stage-0?--save-dev
    npm?i?file-loader?url-loader?--save-dev
    
    npm?i?html-webpack-plugin?---save-dev
    npm?i?clean-webpack-plugin?--save-dev
    npm?i?copy-webpack-plugin?--save-dev
    
    npm?run?dev
    

    默认打开的页面是index.html页面,可以加上/page.html来打开page页面看效果。 PS:?关于loader的详细说明可以参考webpack3.x的学习介绍,上面配置中需要注意的是多页面的公共库的引入采用的是vendor+暴露全局变量的方式,其实这种方式有诸多弊端,而webpack4针对这种情况设置了新的API,有兴趣的话,就继续看下面的高级配置吧。

    进阶的webpack4配置搭建

    包含以下几个方面:

    1. 针对CSSJSTreeShaking来减少无用下载自动微信抢红包,针对JS需要对已有的uglifyjs进行一些自定义的配置(生产环境配置)
    2. 新的公共下载自动微信抢红包抽取工具(optimization.SplitChunksPlugin)提取重用下载自动微信抢红包,减小打包文件。(代替commonchunkplugin,生产和开发环境都需要)
    3. 使用HappyPack进行javascript的多进程打包操作,提升打包速度,并增加打包时间显示。(生产和开发环境都需要)
    4. 创建一个webpack.dll.config.js文件打包常用类库到dll中,使得开发过程中基础模块不会重复打包,而是去动态连接库里获取,代替上一节使用的vendor。(注意这个是在开发环境使用,生产环境打包对时间要求并不高,后者往往是项目持续集成的一部分)
    5. 模块热替换,还需要在项目中增加一些配置,不过大型框架把这块都封装好了。(开发环境配置)
    6. webpack3新增的作用域提升会默认在production模式下启用,不用特别配置,但只有在使用ES6模块才能生效。

    关于第四点,需要在package.json中的script中增加脚本: "build:dll":?"webpack?--config?webpack.dll.config.js?--mode?development",

    补充安装插件的命令行:

    npm?i?purify-css?purifycss-webpack?-D?//?用于css的tree-shaking
    npm?i?webpack-parallel-uglify-plugin?-D?//?用于js的tree-shaking
    npm?i?happypack@next?-D?//用于多进程打包js
    npm?i?progress-bar-webpack-plugin?-D?//用于显示打包时间和进程
    npm?i?webpack-merge?-D?//优化配置下载自动微信抢红包的工具
    npm?i?optimize-css-assets-webpack-plugin?-D?//压缩CSS
    npm?i?chalk?-D
    npm?install?css-hot-loader?-D?//?css热更新
    npm?i?mini-css-extract-plugin?-D
    npm?i?cross-env?-D
    

    TreeShaking需要增加的配置下载自动微信抢红包,这一块参考webpack文档,需要三方面因素,分别是:

    • 使用ES6模块(import/export)
    • package.json文件中声明sideEffects指定可以treeShaking的模块
    • 启用UglifyJSPlugin,多入口下用WebpackParallelUglifyPlugin(这是下面的配置下载自动微信抢红包做的事情)
    /*最上面要增加的声明变量*/
    const?glob?=?require('glob')
    const?PurifyCSSPlugin?=?require('purifycss-webpack')
    const?WebpackParallelUglifyPlugin?=?require('webpack-parallel-uglify-plugin')
    
    /*在`plugins`配置项中需要增加的两个插件设置*/
    new?PurifyCSSPlugin({
    ????paths:?glob.sync(path.join(__dirname,?'src/*.html'))
    }),
    new?WebpackParallelUglifyPlugin({
    ????uglifyJS:?{
    ????????output:?{
    ????????????beautify:?false,?//不需要格式化
    ????????????comments:?false?//不保留注释
    ????????},
    ????????compress:?{
    ????????????warnings:?false,?//?在UglifyJs删除没有用到的下载自动微信抢红包时不输出警告
    ????????????drop_console:?true,?//?删除所有的?`console`?语句,可以兼容ie浏览器
    ????????????collapse_vars:?true,?//?内嵌定义了但是只用到一次的变量
    ????????????reduce_vars:?true?//?提取出出现多次但是没有定义成变量去引用的静态值
    ????????}
    ????}
    ????//?有兴趣可以探究一下使用uglifyES
    }),
    

    关于ES6模块这个事情,上文的第六点也提到了只有ES6模块写法才能用上最新的作用域提升的特性,首先webpack4.x并不需要额外修改babelrc的配置来实现去除无用下载自动微信抢红包,这是从webpack2.x升级后支持的,改用sideEffect声明来实现。但作用域提升仍然需要把babel配置中的module转换去掉,修改后的.babelrc下载自动微信抢红包如下:

    {
    ??"presets":?[["env",?{"loose":?true,?"modules":?false}],?"stage-0"]
    }
    

    但这个时候会发现import引入样式文件就被去掉了……只能使用require来改写了。

    打包DLL第三方类库的配置项,用于开发环境:

    1. webpack.dll.config.js配置文件具体内容:
    const?path?=?require('path')
    const?webpack?=?require('webpack')
    const?pkg?=?require('../package.json')
    /**
    ?*?尽量减小搜索范围
    ?*?target:?'_dll_[name]'?指定导出变量名字
    ?*/
    module.exports?=?{
    ????context:?path.resolve(__dirname,?'../'),
    ????entry:?{
    ????????vendor:?Object.keys(pkg.dependencies)
    ????},
    ????output:?{
    ????????path:?path.join(__dirname,?'dist'),
    ????????filename:?'[name].dll.js',
    ????????library:?'_dll_[name]'?//?全局变量名,其他模块会从此变量上获取里面模块
    ????},
    ????//?manifest是描述文件
    ????plugins:?[
    ????????new?webpack.DllPlugin({
    ????????????name:?'_dll_[name]',
    ????????????path:?path.join(__dirname,?'dist',?'manifest.json'),
    ????????????context:?path.resolve(__dirname,?'../')
    ????????})
    ????]
    }
    
  • webpack.config.js中增加的配置项:
  • /*找到上一步生成的`manifest.json`文件配置到`plugins`里面*/
    new?webpack.DllReferencePlugin({
    ????manifest:?require(path.join(__dirname,?'..',?'dist',?'manifest.json')),
    }),
    

    多文件入口的公用下载自动微信抢红包提取插件配置:

    /*webpack4.x的最新优化配置项,用于提取公共下载自动微信抢红包,跟`entry`是同一层级*/
    optimization:?{
    ????splitChunks:?{
    ????????cacheGroups:?{
    ????????????commons:?{
    ????????????????chunks:?"initial",
    ????????????????name:?"common",
    ????????????????minChunks:?2,
    ????????????????maxInitialRequests:?5,
    ????????????????minSize:?0
    ????????????}
    ????????}
    ????}
    }
    
    /*针对生成HTML的插件,需增加common,也去掉上一节加的vendor*/
    new?HtmlWebpackPlugin({
    ????template:?path.resolve(__dirname,'src','index.html'),
    ????filename:'index.html',
    ????chunks:['index',?'common'],
    ????vendor:?'./vendor.dll.js',?//与dll配置文件中output.fileName对齐
    ????hash:true,//防止缓存
    ????minify:{
    ????????removeAttributeQuotes:true//压缩?去掉引号
    ????}
    }),
    new?HtmlWebpackPlugin({
    ????template:?path.resolve(__dirname,'src','page.html'),
    ????filename:'page.html',
    ????chunks:['page',?'common'],
    ????vendor:?'./vendor.dll.js',?//与dll配置文件中output.fileName对齐
    ????hash:true,//防止缓存
    ????minify:{
    ????????removeAttributeQuotes:true//压缩?去掉引号
    ????}
    }),
    

    PS:?这一块要多注意,对应入口的HTML文件也要处理,关键是自定义的vendor项,在开发环境中引入到html

    HappyPack的多进程打包处理:

    /*最上面要增加的声明变量*/
    const?HappyPack?=?require('happypack')
    const?os?=?require('os')?//获取电脑的处理器有几个核心,作为配置传入
    const?happyThreadPool?=?HappyPack.ThreadPool({?size:?os.cpus().length?})
    const?ProgressBarPlugin?=?require('progress-bar-webpack-plugin')
    
    /*在`module.rules`配置项中需要更改的`loader`设置*/
    {
    ????test:?/\.jsx?$/,
    ????loader:?'happypack/loader?id=happy-babel-js',
    ????include:?[path.resolve('src')],
    ????exclude:?/node_modules/,
    },
    
    /*在`plugins`配置项中需要增加的插件设置*/
    new?HappyPack({?//开启多线程打包
    ????id:?'happy-babel-js',
    ????loaders:?['babel-loader?cacheDirectory=true'],
    ????threadPool:?happyThreadPool
    }),
    new?ProgressBarPlugin({
    ????format:?'??build?[:bar]?'?+?chalk.green.bold(':percent')?+?'?(:elapsed?seconds)'
    })
    

    PS:要记住这种使用方法下一定要在根目录下加.babelrc文件来设置babel的打包配置。

    开发环境的下载自动微信抢红包热更新: 其实针对热刷新,还有两个方面要提及,一个是html文件里面写下载自动微信抢红包的热跟新(这个对于框架不需要,如果要实现,建议使用glup,后面有下载自动微信抢红包),一个是写的样式下载自动微信抢红包的热更新,这两部分也要加进去。让我们一起看看热更新需要增加的配置下载自动微信抢红包:

    /*在`devServer`配置项中需增加的设置*/
    hot:true
    
    /*在`plugins`配置项中需要增加的插件设置*/
    new?webpack.HotModuleReplacementPlugin(),?//模块热更新
    new?webpack.NamedModulesPlugin(),?//模块热更新
    

    在业务下载自动微信抢红包中要做一些改动,一个比较low的例子为:

    if(module.hot)?{?//设置消息监听,重新执行函数
    ????module.hot.accept('./hello.js',?function()?{
    ????????div.innerHTML?=?hello()
    ????})
    }
    

    但还是不能实现在html修改后自动刷新页面,这里有个概念是热更新不是针对页面级别的修改,这个问题有一些解决方法,但目前都不是很完美,可以参考这里,现在针对CSS的热重载有一套解决方案如下,需要放弃使用上文提到的ExtractTextWebapckPlugin,引入mini-css-extract-pluginhot-css-loader来实现,前者在webpack4.x上与hot-css-loader有报错,让我们改造一番:

    /*最上面要增加的声明变量*/
    const?MiniCssExtractPlugin?=?require('mini-css-extract-plugin')
    
    /*在样式的`loader`配置项中需增加的设置,实现css热更新,以css为例,其他可以参照我的仓库来写*/
    {
    ????test:?/\.css$/,
    ????use:?['css-hot-loader',?MiniCssExtractPlugin.loader,?'css-loader',?'postcss-loader'],
    ????include:?[resolve('src')],?//限制范围,提高打包速度
    ????exclude:?/node_modules/
    }
    
    /*在`plugins`配置项中需要增加的插件设置,注意这里不能写[hash],否则无法实现热跟新,如果有hash需要,可以开发环境和生产环境分开配置*/
    new?MiniCssExtractPlugin({
    ????filename:?"[name].css",
    ????chunkFilename:?"[id].css"
    })
    

    用于生产环境压缩css的插件,看官方文档说明,样式文件压缩没有内置的,所以暂时引用第三方插件来做,以下是配置示例。

    /*要增加的声明变量*/
    const?OptimizeCSSPlugin?=?require('optimize-css-assets-webpack-plugin')
    
    /*在`plugins`配置项中需要增加的插件设置*/
    new?OptimizeCSSPlugin({
    ????cssProcessorOptions:?{safe:?true}
    })
    
    最终成果

    ??在进阶部分我们对webpack配置文件根据开发环境和生产环境的不同做了分别的配置,因此有必要分成两个文件,然后发现重复的配置下载自动微信抢红包很多,作为有下载自动微信抢红包洁癖的人不能忍,果断引入webpack-merge,来把相同的配置抽出来,放到build/webpack.base.js中,而后在build/webpack.dev.config.js(开发环境)和build/webpack.prod.config.js(生产环境)中分别引用,在这个过程中也要更改之前文件的路径设置,以免打包或者找文件的路径出错,同时将package.json中的脚本命令修改为:

    "scripts":?{
    ????"build":?"cross-env?NODE_ENV='production'?webpack?--config?build/webpack.prod.config.js?--mode?production",
    ????"dev":?"cross-env?NODE_ENV='development'?webpack-dev-server?--open?--config?build/webpack.dev.config.js?--mode?development",
    ????"dll":?"webpack?--config?build/webpack.dll.config.js?--mode?production",
    ????"start":?"npm?run?dll?&&?npm?run?dev",
    ????"prod":?"npm?run?dll?&&?npm?run?build"
    }
    

    接下来就是下载自动微信抢红包的重构过程,这个过程其实我建议大家自己动手做一做,就能对webpack配置文件结构更加清晰。

    build文件夹下的webpack.base.js文件:

    'use?strict'
    const?path?=?require('path');
    const?chalk?=?require('chalk');
    const?ProgressBarPlugin?=?require('progress-bar-webpack-plugin')
    const?HappyPack?=?require('happypack')
    const?os?=?require('os')
    const?happyThreadPool?=?HappyPack.ThreadPool({?size:?os.cpus().length?})
    const?MiniCssExtractPlugin?=?require('mini-css-extract-plugin')
    function?resolve?(dir)?{
    ??return?path.join(__dirname,?'..',?dir)
    }
    
    function?assetsPath(_path_)?{
    ??let?assetsSubDirectory;
    ??if?(process.env.NODE_ENV?===?'production')?{?//?这里需要用cross-env来注入Node变量
    ????assetsSubDirectory?=?'static'?//可根据实际情况修改
    ??}?else?{
    ????assetsSubDirectory?=?'static'
    ??}
    ??return?path.posix.join(assetsSubDirectory,?_path_)
    }
    
    module.exports?=?{
    ??context:?path.resolve(__dirname,?'../'),
    ??entry:?{
    ????index:?'./src/index.js',
    ????page:?'./src/page.js'
    ??},
    ??output:{
    ????path:?resolve('dist'),
    ????filename:'[name].[hash].js'
    ??},
    ??resolve:?{
    ????extensions:?[".js",".css",".json"],
    ????alias:?{}?//配置别名可以加快webpack查找模块的速度
    ??},
    ??module:?{
    ????//?多个loader是有顺序要求的,从右往左写,因为转换的时候是从右往左转换的
    ????rules:[
    ??????{
    ????????test:?/\.css$/,
    ????????use:?['css-hot-loader',?MiniCssExtractPlugin.loader,?'css-loader',?'postcss-loader'],
    ????????include:?[resolve('src')],?//限制范围,提高打包速度
    ????????exclude:?/node_modules/
    ??????},
    ??????{
    ????????test:/\.less$/,
    ????????use:?['css-hot-loader',?MiniCssExtractPlugin.loader,?'css-loader',?'postcss-loader',?'less-loader'],
    ????????include:?[resolve('src')],
    ????????exclude:?/node_modules/
    ??????},
    ??????{
    ????????test:/\.scss$/,
    ????????use:?['css-hot-loader',?MiniCssExtractPlugin.loader,?'css-loader',?'postcss-loader',?'sass-loader'],
    ????????include:?[resolve('src')],
    ????????exclude:?/node_modules/
    ??????},
    ??????{
    ??????????test:?/\.jsx?$/,
    ??????????loader:?'happypack/loader?id=happy-babel-js',
    ??????????include:?[resolve('src')],
    ??????????exclude:?/node_modules/,
    ??????},
    ??????{?//file-loader?解决css等文件中引入图片路径的问题
    ??????//?url-loader?当图片较小的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader?进行拷贝
    ????????test:?/\.(png|jpg|jpeg|gif|svg)/,
    ????????use:?{
    ??????????loader:?'url-loader',
    ??????????options:?{
    ????????????name:?assetsPath('images/[name].[hash:7].[ext]'),?//?图片输出的路径
    ????????????limit:?1?*?1024
    ??????????}
    ????????}
    ??????},
    ??????{
    ????????test:?/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    ????????loader:?'url-loader',
    ????????options:?{
    ??????????limit:?10000,
    ??????????name:?assetsPath('media/[name].[hash:7].[ext]')
    ????????}
    ??????},
    ??????{
    ????????test:?/\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    ????????loader:?'url-loader',
    ????????options:?{
    ??????????limit:?10000,
    ??????????name:?assetsPath('fonts/[name].[hash:7].[ext]')
    ????????}
    ??????}
    ????]
    ??},
    ??optimization:?{?//webpack4.x的最新优化配置项,用于提取公共下载自动微信抢红包
    ????splitChunks:?{
    ??????cacheGroups:?{
    ????????commons:?{
    ??????????chunks:?"initial",
    ??????????name:?"common",
    ??????????minChunks:?2,
    ??????????maxInitialRequests:?5,?//?The?default?limit?is?too?small?to?showcase?the?effect
    ??????????minSize:?0?//?This?is?example?is?too?small?to?create?commons?chunks
    ????????}
    ??????}
    ????}
    ??},
    ??plugins:?[
    ????new?HappyPack({
    ??????id:?'happy-babel-js',
    ??????loaders:?['babel-loader?cacheDirectory=true'],
    ??????threadPool:?happyThreadPool
    ????}),
    ????new?MiniCssExtractPlugin({
    ??????filename:?"[name].css",
    ??????chunkFilename:?"[id].css"
    ????}),
    ????new?ProgressBarPlugin({
    ??????format:?'??build?[:bar]?'?+?chalk.green.bold(':percent')?+?'?(:elapsed?seconds)'
    ????}),
    ??]
    }
    

    webpack.dev.config.js文件内容:

    const?path?=?require('path');
    const?HtmlWebpackPlugin?=?require('html-webpack-plugin')?//?生成html的插件
    const?webpack?=?require('webpack')
    const?baseConfig?=?require('./webpack.base')
    const?merge?=?require('webpack-merge')
    
    const?devWebpackConfig?=?merge(baseConfig,?{
    ??output:{
    ????publicPath:?'/'
    ??},
    ??devtool:?'eval-source-map',?//?指定加source-map的方式
    ??devServer:?{
    ????inline:true,//打包后加入一个websocket客户端
    ????hot:true,//热加载
    ????contentBase:?path.join(__dirname,?"..",?"dist"),?//静态文件根目录
    ????port:?3824,?//?端口
    ????host:?'localhost',
    ????overlay:?true,
    ????compress:?false?//?服务器返回浏览器的时候是否启动gzip压缩
    ??},
    ??watchOptions:?{
    ??????ignored:?/node_modules/,?//忽略不用监听变更的目录
    ??????aggregateTimeout:?500,?//防止重复保存频繁重新编译,500毫米内重复保存不打包
    ??????poll:1000?//每秒询问的文件变更的次数
    ??},
    ??plugins:?[
    ????//?多入口的html文件用chunks这个参数来区分
    ????new?HtmlWebpackPlugin({
    ??????template:?path.resolve(__dirname,?'..',?'src','index.html'),
    ??????filename:'index.html',
    ??????chunks:['index',?'common'],
    ??????vendor:?'./vendor.dll.js',?//与dll配置文件中output.fileName对齐
    ??????hash:true,//防止缓存
    ??????minify:{
    ??????????removeAttributeQuotes:true//压缩?去掉引号
    ??????}
    ????}),
    ????new?HtmlWebpackPlugin({
    ??????template:?path.resolve(__dirname,?'..',?'src','page.html'),
    ??????filename:'page.html',
    ??????chunks:['page',?'common'],
    ??????vendor:?'./vendor.dll.js',?//与dll配置文件中output.fileName对齐
    ??????hash:true,//防止缓存
    ??????minify:{
    ??????????removeAttributeQuotes:true//压缩?去掉引号
    ??????}
    ????}),
    ????new?webpack.DllReferencePlugin({
    ??????manifest:?path.resolve(__dirname,?'..',?'dist',?'manifest.json')
    ????}),
    ????new?webpack.HotModuleReplacementPlugin(),?//HMR
    ????new?webpack.NamedModulesPlugin()?//?HMR
    ??]
    })
    
    module.exports?=?devWebpackConfig
    

    webpack.dev.config.js文件内容:

    'use?strict'
    const?path?=?require('path');
    const?CopyWebpackPlugin?=?require('copy-webpack-plugin')?//?复制静态资源的插件
    const?CleanWebpackPlugin?=?require('clean-webpack-plugin')?//?清空打包目录的插件
    const?HtmlWebpackPlugin?=?require('html-webpack-plugin')?//?生成html的插件
    const?webpack?=?require('webpack')
    const?baseConfig?=?require('./webpack.base')
    const?merge?=?require('webpack-merge')
    
    const?glob?=?require('glob')
    const?PurifyCSSPlugin?=?require('purifycss-webpack')
    const?WebpackParallelUglifyPlugin?=?require('webpack-parallel-uglify-plugin')
    const?OptimizeCSSPlugin?=?require('optimize-css-assets-webpack-plugin')
    
    module.exports?=?merge(baseConfig,?{
    ????output:{
    ????????publicPath:?'./'?//这里要放的是静态资源CDN的地址(只在生产环境下配置)
    ????},
    ????plugins:?[
    ????????new?HtmlWebpackPlugin({
    ????????????template:?path.resolve(__dirname,?'..',?'src',?'index.html'),
    ????????????filename:'index.html',
    ????????????chunks:['index',?'common'],
    ????????????hash:true,//防止缓存
    ????????????minify:{
    ????????????????removeAttributeQuotes:true//压缩?去掉引号
    ????????????}
    ????????}),
    ????????new?HtmlWebpackPlugin({
    ????????????template:?path.resolve(__dirname,?'..',?'src',?'page.html'),
    ????????????filename:'page.html',
    ????????????chunks:['page',?'common'],
    ????????????hash:true,//防止缓存
    ????????????minify:{
    ????????????????removeAttributeQuotes:true//压缩?去掉引号
    ????????????}
    ????????}),
    ????????new?CopyWebpackPlugin([
    ????????????{
    ????????????????from:?path.join(__dirname,?'..',?'static'),
    ????????????????to:?path.join(__dirname,??'..',?'dist',?'static'),
    ????????????????ignore:?['.*']
    ????????????}
    ????????]),
    ????????new?CleanWebpackPlugin(['dist'],?{
    ????????????root:?path.join(__dirname,?'..'),
    ????????????verbose:?true,
    ????????????dry:??false
    ????????}),
    ????????new?OptimizeCSSPlugin({
    ????????????cssProcessorOptions:?{safe:?true}
    ????????}),
    ????????new?PurifyCSSPlugin({
    ????????????paths:?glob.sync(path.join(__dirname,?'../src/*.html'))
    ????????}),
    ????????new?WebpackParallelUglifyPlugin({
    ????????????uglifyJS:?{
    ????????????????output:?{
    ????????????????????beautify:?false,?//不需要格式化
    ????????????????????comments:?false?//不保留注释
    ????????????????},
    ????????????????compress:?{
    ????????????????????warnings:?false,?//?在UglifyJs删除没有用到的下载自动微信抢红包时不输出警告
    ????????????????????drop_console:?true,?//?删除所有的?`console`?语句,可以兼容ie浏览器
    ????????????????????collapse_vars:?true,?//?内嵌定义了但是只用到一次的变量
    ????????????????????reduce_vars:?true?//?提取出出现多次但是没有定义成变量去引用的静态值
    ????????????????}
    ????????????}
    ????????}),
    ????]
    })
    

    多说一句,就是实现JS打包的treeShaking还有一种方法是编译期分析依赖,利用uglifyjs来完成,这种情况需要保留ES6模块才能实现,因此在使用这一特性的仓库中,.babelrc文件的配置为:"presets":?[["env",?{?"modules":?false?}],?"stage-0"],就是打包的时候不要转换模块引入方式的含义。

    接下来就可以运行npm?start,看一下进阶配置后的成果啦,吼吼,之后只要不进行build打包操作,通过npm?run?dev启动,不用重复打包vendor啦。生产环境打包使用的是npm?run?build

    以上就是对webpack4.x配置的踩坑过程,期间参考了大量谷歌英文资料,希望能帮助大家更好地掌握wepback最新版本的配置,以上内容亲测跑通,有问题的话,欢迎加我微信(kashao3824)讨论,到github地址提issue也可,欢迎fork/star

    最新更改:

    • 修复了common会重复打包已有dll库的问题
    • 现在的dll库会自动根据package.json中的配置项生成
    • dll现在是生产环境打包模式,并且vendor.dll.js现在在生产环境下也会注入HTML模板中
    • 生产环境打包使用命令npm?run?prod
    • 修复了process.env.NODE_ENV在打包过程中取不到的问题?issue2
  • 面试官:既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?
    面试官系列(2):如何实现一个Event
    往期

    面试官系列(1):?如何实现深克隆


    前言

    本文标题的题目是由其他问题延伸而来,面试中面试官的常用套路,揪住一个问题一直深挖,在产生这个问题之前一定是这个问题.

    React/Vue不同组件之间是怎么通信的?

    Vue

    1. 父子组件用Props通信
    2. 非父子组件用Event?Bus通信
    3. 如果项目够复杂,可能需要Vuex等全局状态管理库通信
    4. $dispatch(已经废除)和$broadcast(已经废除)

    React

    1. 父子组件,父->子直接用Props,子->父用callback回调
    2. 非父子组件,用发布订阅模式的Event模块
    3. 项目复杂的话用Redux、Mobx等全局状态管理管库
    4. 用新的Context?Api

    我们大体上都会有以上回答,接下来很可能会问到如何实现Event(Bus),因为这个东西太重要了,几乎所有的模块通信都是基于类似的模式,包括安卓开发中的Event?Bus,Node.js中的Event模块(Node中几乎所有的模块都依赖于Event,包括不限于http、stream、buffer、fs等).

    我们仿照Node中Event?API实现一个简单的Event库,他是发布订阅模式的典型应用.

    提前声明:?我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现.


    1.基本构造

    1.1初始化class

    我们利用ES6的class关键字对Event进行初始化,包括Event的事件清单和监听者上限.

    我们选择了Map作为储存事件的结构,因为作为键值对的储存方式Map比一般对象更加适合,我们操作起来也更加简洁,可以先看一下Map的基本用法与特点.

    class?EventEmeitter?{
    ??constructor()?{
    ????this._events?=?this._events?||?new?Map();?//?储存事件/回调键值对
    ????this._maxListeners?=?this._maxListeners?||?10;?//?设立监听上限
    ??}
    }
    

    1.2?监听与触发

    触发监听函数我们可以用applycall两种方法,在少数参数时call的性能更好,多个参数时apply性能更好,当年Node的Event模块就在三个参数以下用call否则用apply.

    当然当Node全面拥抱ES6+之后,相应的call/apply操作用Reflect新关键字重写了,但是我们不想写的那么复杂,就做了一个简化版.

    
    //?触发名为type的事件
    EventEmeitter.prototype.emit?=?function(type,?...args)?{
    ??let?handler;
    ??//?从储存事件键值对的this._events中获取对应事件回调函数
    ??handler?=?this._events.get(type);
    ??if?(args.length?>?0)?{
    ????handler.apply(this,?args);
    ??}?else?{
    ????handler.call(this);
    ??}
    ??return?true;
    };
    
    //?监听名为type的事件
    EventEmeitter.prototype.addListener?=?function(type,?fn)?{
    ??//?将type事件以及对应的fn函数放入this._events中储存
    ??if?(!this._events.get(type))?{
    ????this._events.set(type,?fn);
    ??}
    };
    
    

    我们实现了触发事件的emit方法和监听事件的addListener方法,至此我们就可以进行简单的实践了.

    //?实例化
    const?emitter?=?new?EventEmeitter();
    
    //?监听一个名为arson的事件对应一个回调函数
    emitter.addListener('arson',?man?=>?{
    ??console.log(`expel?${man}`);
    });
    
    //?我们触发arson事件,发现回调成功执行
    emitter.emit('arson',?'low-end');?//?expel?low-end
    

    似乎不错,我们实现了基本的触发/监听,但是如果有多个监听者呢?

    //?重复监听同一个事件名
    emitter.addListener('arson',?man?=>?{
    ??console.log(`expel?${man}`);
    });
    emitter.addListener('arson',?man?=>?{
    ??console.log(`save?${man}`);
    });
    
    emitter.emit('arson',?'low-end');?//?expel?low-end
    

    是的,只会触发第一个,因此我们需要进行改造.


    2.升级改造

    2.1?监听/触发器升级

    我们的addListener实现方法还不够健全,在绑定第一个监听者之后,我们就无法对后续监听者进行绑定了,因此我们需要将后续监听者与第一个监听者函数放到一个数组里.

    
    //?触发名为type的事件
    EventEmeitter.prototype.emit?=?function(type,?...args)?{
    ??let?handler;
    ??handler?=?this._events.get(type);
    ??if?(Array.isArray(handler))?{
    ????//?如果是一个数组说明有多个监听者,需要依次此触发里面的函数
    ????for?(let?i?=?0;?i?if?(args.length?>?0)?{
    ????????handler[i].apply(this,?args);
    ??????}?else?{
    ????????handler[i].call(this);
    ??????}
    ????}
    ??}?else?{?//?单个函数的情况我们直接触发即可
    ????if?(args.length?>?0)?{
    ??????handler.apply(this,?args);
    ????}?else?{
    ??????handler.call(this);
    ????}
    ??}
    
    ??return?true;
    };
    
    //?监听名为type的事件
    EventEmeitter.prototype.addListener?=?function(type,?fn)?{
    ??const?handler?=?this._events.get(type);?//?获取对应事件名称的函数清单
    ??if?(!handler)?{
    ????this._events.set(type,?fn);
    ??}?else?if?(handler?&&?typeof?handler?===?'function')?{
    ????//?如果handler是函数说明只有一个监听者
    ????this._events.set(type,?[handler,?fn]);?//?多个监听者我们需要用数组储存
    ??}?else?{
    ????handler.push(fn);?//?已经有多个监听者,那么直接往数组里push函数即可
    ??}
    };
    

    是的,从此以后可以愉快的触发多个监听者的函数了.

    //?监听同一个事件名
    emitter.addListener('arson',?man?=>?{
    ??console.log(`expel?${man}`);
    });
    emitter.addListener('arson',?man?=>?{
    ??console.log(`save?${man}`);
    });
    
    emitter.addListener('arson',?man?=>?{
    ??console.log(`kill?${man}`);
    });
    
    //?触发事件
    emitter.emit('arson',?'low-end');
    //expel?low-end
    //save?low-end
    //kill?low-end
    

    2.2?移除监听

    我们会用removeListener函数移除监听函数,但是匿名函数是无法移除的.

    EventEmeitter.prototype.removeListener?=?function(type,?fn)?{
    ??const?handler?=?this._events.get(type);?//?获取对应事件名称的函数清单
    
    ??//?如果是函数,说明只被监听了一次
    ??if?(handler?&&?typeof?handler?===?'function')?{
    ????this._events.delete(type,?fn);
    ??}?else?{
    ????let?postion;
    ????//?如果handler是数组,说明被监听多次要找到对应的函数
    ????for?(let?i?=?0;?i?if?(handler[i]?===?fn)?{
    ????????postion?=?i;
    ??????}?else?{
    ????????postion?=?-1;
    ??????}
    ????}
    ????//?如果找到匹配的函数,从数组中清除
    ????if?(postion?!==?-1)?{
    ??????//?找到数组对应的位置,直接清除此回调
    ??????handler.splice(postion,?1);
    ??????//?如果清除后只有一个函数,那么取消数组,以函数形式保存
    ??????if?(handler.length?===?1)?{
    ????????this._events.set(type,?handler[0]);
    ??????}
    ????}?else?{
    ??????return?this;
    ????}
    ??}
    };
    

    3.发现问题

    我们已经基本完成了Event最重要的几个方法,也完成了升级改造,可以说一个Event的骨架是被我们开发出来了,但是它仍然有不足和需要补充的地方.

    1. 鲁棒性不足:?我们没有对参数进行充分的判断,没有完善的报错机制.
    2. 模拟不够充分:?除了removeAllListeners这些方法没有实现以外,例如监听时间后会触发newListener事件,我们也没有实现,另外最开始的监听者上限我们也没有利用到.

    当然,这在面试中现场写一个Event已经是很够意思了,主要是体现出来对发布-订阅模式的理解,以及针对多个监听状况下的处理,不可能现场撸几百行写一个完整Event.

    索性Event库帮我们实现了完整的特性,整个下载自动微信抢红包量有300多行,很适合阅读,你可以花十分钟的时间通读一下,见识一下完整的Event实现.

  • 一招一式更多