SYLiu

SYLiu 查看完整档案

北京编辑华东交通大学  |  软件工程 编辑xxx  |  bug生产工程师 编辑 www.lasy.site 编辑
编辑

个人动态

SYLiu 赞了文章 · 2020-11-09

postman模拟post上传文件

postman模拟post上传文件
图片描述

  1. 输入url.
  2. 选择Body.
  3. 选中Body中的form-data.
  4. 添加字段名为file的key, 选择类型为File.
  5. 点击选择文件添加文件.
  6. 检查Headers中的content-type. 如果有值, 则清空.

最后点击Send.

查看原文

赞 1 收藏 0 评论 0

SYLiu 赞了回答 · 2020-11-09

解决node怎么执行多条linux命令,即如何获取执行后的linux环境

//原理:
//子进程并不是bash进程,进程只是一个空间,用来运行某个软件。其中bash就是其中一个软件。
//spawn函数返回的,就是这个软件的上下文。可以向该上下文发生命令。执行程序。
var spawn = require('child_process').spawn;//子进程操作模块
// var subProcess = spawn("/bin/bash");//使用子程序去运行某个软件。在这里就是运行bash软件。并获取其上下文。
var subProcess = spawn("bash");//使用子程序去运行某个软件。在这里就是运行bash软件。并获取其上下文。

//消息监听,监听子进程的输出。并在主进程中打印出来。
function onData(data) {
    process.stdout.write(data);//获取当前进程,并在输出中写入某内容。关键是process表示的是当前进程
}
//整个进程的错误监听
subProcess.on('error', function () {
    console.log("error");
    console.log(arguments);
});
//设置消息监听
subProcess.stdout.on('data', onData);
subProcess.stderr.on('data', onData);
subProcess.on('close', (code) => { console.log(colors.blue(`子进程退出码:${code}`)); }); // 监听进程退出
//向子进程发送命令
subProcess.stdin.write('cd / \n');   // 写入数据
subProcess.stdin.write('pwd \n');   // 写入数据
subProcess.stdin.write('ls');   // 写入数据
subProcess.stdin.end();

关注 2 回答 2

SYLiu 赞了文章 · 2020-10-15

Node设置cors,后端解决跨域问题

跨域说的太多了,这里不再解释,浏览器web的同源策略造成的,目前跨域前端和后端都是可以解决的,今天主要说后端常用的解决办法
CORS:W3C的标准技术,主要全称是Cross-Origin Resource Sharing,中文叫跨域资源共享技术,官网:'https://www.w3.org/TR/cors/',如果你要看这个官网的解释,可以看看下面的这个,122.png简单解释就是当代浏览器会将跨站点异步访问的权限交给CORS来处理解决跨域问题,通常有两种设置方式,本文案例以node为例子,上代码

方式一:直接使用npm里面的cors包,简单粗暴.

**安装包  npm install cors  -S**


const app=express();//基于node里面的express服务器

//我这边使用了中间件
var cors=require("cors");

app.use(cors()); 

//后面的代码会引入我后端的接口,类似于一个react.js,通过express路由引入后,服务端接口配置完毕,此方式太暴力,解决了所有请求头和方式设置的繁琐问题,缺点如何要携带cookie这种方式显然不适合

方式二:也是基于express中间件设置,只不过会设置具体请求头,请求方式,可以携带Cookie.

const express = require('express')

const app = express();

app.use((req, res, next) => {
//判断路径
  if(req.path !== '/' && !req.path.includes('.')){
    res.set({
      'Access-Control-Allow-Credentials': true, //允许后端发送cookie
      'Access-Control-Allow-Origin': req.headers.origin || '*', //任意域名都可以访问,或者基于我请求头里面的域
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type', //设置请求头格式和类型
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',//允许支持的请求方式
      'Content-Type': 'application/json; charset=utf-8'//默认与允许的文本格式json和编码格式
    })
  }
  req.method === 'OPTIONS' ? res.status(204).end() : next()
})

补充一点OPTIONS:OPTIONS方法是用于请求获得由Request-URI标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能,我们常见的请求就是get或者post,那么哪里查看options请求,看图:
]J1CMNU${00{F()Y7)L1E38.png

至于后端是Java或者php,也是一样可以用这种方式设置的,只是写法不同

查看原文

赞 4 收藏 2 评论 7

SYLiu 赞了文章 · 2020-08-26

工作一两年的程序员,有点钱,买房还是买车?

先看再点赞,给自己一点思考的时间;欢迎微信搜索【沉默王二】关注这个有颜值却假装靠才华苟且的程序员。
本文 GitHub github.com/itwanger 已收录,里面还有我精心准备的一线大厂面试题。

有个朋友小白,在成都,也是个程序员,最近打算买车,但有点犹豫,就问我,“二哥,很多人劝我攒钱先买房,但我就是想先买辆车,你的建议呢?”

我的读者群体里,年轻人居多,尤其是像小白这样的,刚参加工作一两年的,有点钱,但买房难度有点大,买车到还好(比房子还贵的车不考虑在内哈)。我约摸着不少读者有这种困惑,所以我打算把我的观点抛出来,给大家一点点参考。(仅供参考,仅供参考,我得先撇清责任,呵呵)

买房还是买车,确实是要看情况的,不能一概而论。

我就问小白啊,“成都房价你现在能承受得起吗?将来打算在成都定居吗?你现在攒的钱能够首付吗?家里给的支援能到位吗?”

小白有些无奈地回答,“成都的房价,以我现在的能力,真有点负担不起,我和二哥都是农村来的,支援不了那么多啊!我想着过些年,还是回咱河南工作。但问了很多人,都劝我先买房,可是在我心目中,真的非常非常想拥有一辆自己的车子!”

其实对于小白这种情况,答案已经很清晰了,对不对?假如房子能买得起,我也劝他买。

我 2014 年回到洛阳的时候,一心就想租个房子住。

第一,我那时候就没有买房的意愿,觉得租房挺好的,当时 1200 元租了三室两厅两卫,我和女朋友一块住,奢侈得不要不要的。

第二,工作了三年半,就攒了不到十万块,也不想找家里要钱,即便是要了,离一套房子的首付还有点距离,必须得再借点朋友同事的。

我那时候的心态,是不是很佛系?能配得上我的头像吧!

假如那时候,谁劝我买房,我就想问一句,你能不能借我点?相信我,我有能力偿还的,用人品保证。

理想和现实总有点差距,对不对?可日子还是要一天天过啊。

等到了 2016 年,女朋友开始催我买房,毕竟要结婚了,买房是刚需。这时候,我虽然有点不情愿,但前后合计合计,好像能买得起房了,虽然房价在两年内涨了不少。

父母能给我们支援 15 万,再加上同事朋友能借 10 万,加上我们的那些积蓄,首付够了,并且装修家居的钱也够了,关键是女朋友的公积金能够负担起买房的贷款。

这就满足了买房的条件对不对?假如买房的条件达成了,那肯定要买房。买房的好处是什么?

第一,有了房,就满足了结婚的必要条件。

第二,有了房,就有了这座城市的归属感。我虽然是个洛阳人,但在市区没有房的话,只能算是洛阳市洛宁县河底乡东河村的村民。

第三,有了房,就不用搬家了。我们当初租的那套三室,后来拆迁了,如果没买房,就得搬家。

但每个人的情况都不尽相同,小白就和我不同。

第一,小白在成都还买不起房,能力有待发展,家里的支援也不到位。一味追求买房就是个空谈。

第二,就算是首付凑得够,乱七八糟的生活费和房贷加在一起,压力特别大,这对于追求享受生活的年轻人来说,受不了。

第三,小白也没打算一直在成都。

第四,小白就想买辆车,他觉得有了车,就实现了自己多年的梦想,这份激励会变成他努力工作的动力,而不是压力。

那有不少读者就说,买车有很多弊端啊!

第一,车是消耗品,过几年就掉价了,亏得慌啊。

第二,城市这么堵,买了不一定能开几次,还有油费,停车费,保险,等等乱七八糟的费用。到时候还得去挤地铁。

那一个人活着是为了什么?

谈恋爱是要花钱的,对吧?没有人不承认这一点吧?虽然爱情是美好的,但也得付出啊。

总不能为了省下谈恋爱花的那些钱,就不去谈啊!要知道,谈恋爱不仅能够在精神上获得满足,获得幸福,还能。。。。。。你懂吧?

很多程序员,都酷爱电子产品,比如说显示器、鼠标、键盘、电脑、耳机、手机、游戏机,这些东西买便宜的也能使啊,可工作效率就降低了,久而久之,就倦怠了,对吧?我那么辛苦工作图个啥?

每个人都应该有一些追求,追求物质、追求精神,追求车,追求房,追求爱情。在人生的不同阶段,应该按照自己的心意,在适可的范围内,实现一些追求,这样才会获得前进的动力。

以我对小白的了解,买车绝对可以激发一个更好的他,成为他努力工作的动力,用不了两年,他就有能力买房了。即便是他现在的薪资不算高,但以他的进步速度,再加上买车后的刺激,两年后绝对可以超越大多数人。

我 31 岁了,你能猜得到,我下一个物质上的欲望是什么吗?我想买一辆机车(摩托车),酷酷的那种。工作压力大了,生活压力大了,我就一个人骑一辆车,自由地奔驰去。

人是应该面对现实,不要空谈理想。但如果只是在现实中挣扎,那就失去了活着的意义。

想象一个画面,你想去一个地方,车上载着女朋友,那种自由奔驰、兜风,把一切烦恼抛诸脑后的快感,是不是贼爽?

我家的车,买了三年多,跑了不到三万公里,但每次下雨,每次出远门,我都要重复一句话,“车,真的是一个神奇的玩意,太方便了。人也太能了吧?竟然发明了车。”

买了车,不意味着就不要买房,买了房也不意味着就不要买车,这两样东西,是近几十年,普通人不得不面对的两样魔幻的东西。先买哪个?得看能力,得看这个东西带给你的潜在价值。

小时候,我特别羡慕别人家的摩托车,即便是到了大学的时候,我们家仍然穷得买不起摩托车,于是,每次放假回家,我都是从下了公共汽车的车站一路走回家的。

我讨厌求别人搭顺风车的感觉,尤其是会遭到拒绝的时候。我就狠着心告诉自己,“劳资哪怕是走,都不要搭便车。”——特别有骨气,有没有?

等我在苏州实习的时候,我特别羡慕那些苏州土著,一个月和我一样拿 1200 元的实习工资,却能开着好车上下班。我又暗下决心,“一定要拥有一辆自己的车!”

买车是很多年轻人的一个梦,尤其是像我和小白这种从贫困的家庭走出来的年轻人。并不是说,买了车就实现了阶级跨越,不是的,而是它能够给我们一种心灵上的安抚,让我们变得自信,让我们敢对着操蛋的世界大喊一句,“去你大爷的,我也可以活得更好!”

现如今的我,仍然是一个普普通通的程序员,每天敲着“改变世界”的代码,但我可以在洛阳这座城市扎根了,并且“车和房”都有了,靠的是自己。


我是沉默王二,一枚有颜值却假装靠才华苟且的程序员。关注即可提升学习效率,别忘了三连啊,点赞、收藏、留言,我不挑,奥利给

最近,有很多读者问我,有没有大厂的面经啊,时不时要打怪进阶一下?那问二哥就对了,微信搜索「沉默王二」;本文 GitHub github.com/itwanger 已收录,欢迎 star。

查看原文

赞 1 收藏 0 评论 1

SYLiu 赞了回答 · 2020-08-11

chrome中性能分析工具分析页面中Idle(空闲时间)占用太长时间,会不会影响页面性能,如果会是什么原因造成的?

看了楼上诸多回答,真是为现在前端开发者捏了一把汗啊!题主不懂就罢了,答题的人不懂也硬往上凑,你们心可真大啊……

吐槽完毕,正经回答一下。

这里的 idle 含义是复合性质的,不能完全等同于服务器加载的反应时间,这是一次 timeline 捕获中的无效(无实际捕获的)时间。只要我愿意,我完全可以捕获出这样的结果:

图片描述

然而这根本不能说明我这个页面加载的等待时间长达 11s,只是因为我开始捕获之后啥也没干,等了 10s 之后才刷新页面而已。

题主的问题在于没有说明你的截图是怎么录制的,所以这个 idle time 里面包含了多少无效的等待时间也就无法根据截图来准确反映了。

如果你真想准确的录制一次页面加载的完整过程(没有无效的等待),那么你应该选择:

clipboard.png

然后点击菜单右边的刷新按钮:

clipboard.png

这时候浏览器会自动刷新并同时开始录制 timeline,在完全加载+执行+渲染完成后自动停止录制,这时候看到的饼图才是相对精确的。之所以说相对精确,是因为 timeline 本身也是有运行消耗的,只能作为相对参考值。

需要注意的是,这里的 idle 时间并不完全等同于服务器的响应时间,你需要注意到不同的捕获事件(loading, scripting, rendering, painting 等等)之间也会有短暂的空白,这些碎片时间都会被计入到 idle time 里面(所以也包括我在最前面演示的啥也没干的一大段空白时间)

timeline 不是专用于测试页面加载时间的,一堆人都没有搞明白 timeline 的作用,别再误导楼主了。

关注 9 回答 5

SYLiu 赞了文章 · 2020-08-06

页面性能优化实践总结

页面性能优化

学而不思则惘,思而不学则殆

前几天接到一个页面效果优化的任务,边做边查阅了一些关于页面性能的资料。做完任务之后,抽空写了一篇总结,梳理一下思路,加深自己的理解。

1. chrome的timeline

先思考这样的一个问题:

什么叫页面性能好?如何进行评判?

直观上讲,我们通常会通过一个页面流不流畅来判断一个页面的性能好不好。但是开发中,总不能这么随意吧。

1-1 fps

FPS(frame per second),即一秒之间能够完成多少次重新渲染.

网页动画的每一帧(frame)都是一次重新渲染,每秒低于24帧的动画,人眼就能感受到停顿。一般的网页动画,需要达到每秒30帧到60帧的频率,才能比较流畅

而大多数显示器的刷新频率是60Hz,为了与系统一致,以及节省电力,浏览器会自动按照这个频率,刷新动画。所以,如果网页能够做到每秒60帧,就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,一秒之内进行60次重新渲染,每次重新渲染的时间不能超过16.66ms

在实际的开发,只要达到30fps就可以了

1-2 timeline

强大的chrome给我们提供了一个工具,叫做timeline,在帧模式下,我们可以看到代码的执行情况

图片描述

  1. 柱状'frame':表示渲染过程中的一帧,也就是浏览器为了渲染单个内容块而必须要做的工作,包括:执行js,处理事件,修改DOM,更改样式和布局,绘制页面等。帧柱的高度表示了该帧的总耗时,帧柱中的颜色分别对应该帧中包含的不停类型的事件。我们的目标就是控制其在30fps,即1000ms / 30 = 33.34ms

    • 蓝色: 网络和HTML解析

    • 黄色: JavaScript 脚本运行

    • 紫色: 样式重计算和布局 ( Layout , Recaculate Style, Update Layer tree)

    • 绿色: 绘制和合成 ( Paint , Composite Layers)

  2. 30fps和60fps的基准线,可以直观地看到页面每一帧的情况

  3. 灰色区块:那些没有被DevTools感知到的活动

  4. 空白区块:显示刷新周期(display refresh cycles)中的空闲时间段??

  5. event :事件,上面可以看到触发了什么的事件,然后执行的语句是哪些,

  6. recalculate style: 重新计算样式

  7. update layer tree: 【耗时】

  8. composite layers: 【耗时】

  9. paint X n: 【耗时】

2. 页面渲染的原理和过程

接下来思考这个问题:

什么是update layer tree,什么是compsite layers,它们为什么那么耗时?

要理解update layer treecomposite layers,我们必须了解页面的渲染原理和过程。

2-1 页面生成的过程

我们都知道网页生成过程,大致可以分成五步

  1. HTML代码转化为DOM

  2. CSS代码转化成CSSOM(CSS Object Model)

  3. 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)

  4. 生成布局(layout),即将所有渲染树的所有节点进行平面合成

  5. 将布局绘制(paint)在屏幕上

那么,浏览器是如何进行渲染的?

2-2 理解图层

浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。浏览器实际所做的工作有:

  1. 获取DOM后分隔为多个图层

  2. 对每个图层的节点计算样式结果(recalculate style)

  3. 为每个节点生成图形和位置(layout即reflow和重布局)

  4. 将每个节点绘制填充到图层位图汇总(paint,repaint)

  5. 图层作为纹理加载到GPU

  6. 合并多个图层到页面上,生成最终图像(composite layers)

渲染的过程通常是相当耗时,低效的代码往往就是触发过程的layout,paint,composite layers,导致页面卡顿。

3. 低效的代码

明白了整个渲染的过程和timeline的操作的含义,那么可以思考这样的一个问题:

什么样的代码会触发这么耗时的操作,导致我们的页面卡顿?

3-1. 重排和重绘

网页生成的时候,至少会渲染一次。而我们需要关注的是用户访问过程中,那些会导致网页重新渲染的行为:

  • 修改DOM

  • 修改样式表

  • 用户事件(例如鼠标悬停,页面滚动,输入框输入文字等)

重新渲染,就涉及重排重绘

重排(reflow)

即重新生成布局,重排必然导致重绘,如元素位置的改变,就会触发重排和重绘。

会触发重排的的属性:

  1. 盒子模型相关属性会触发重布局:

    • width

    • height

    • padding

    • margin

    • display

    • border-width

    • border

    • min-height

  2. 定位属性及浮动也会触发重布局:

    • top

    • bottom

    • left

    • right

    • position

    • float

    • clear

  3. 改变节点内部文字结构也会触发重布局:

    • text-align

    • overflow-y

    • font-weight

    • overflow

    • font-family

    • line-height

    • vertival-align

    • white-space

    • font-size

重绘(repaint)

即重新绘制,需要注意的是,重绘不一定需要重排,比如改变某个元素的颜色,就只会触发重绘,而不会触发重排。

会触发重绘的属性

  • color

  • border-style

  • border-radius

  • visibility

  • text-decoration

  • background

  • background-image

  • background-position

  • background-repeat

  • background-size

  • outline-color

  • outline

  • outline-style

  • outline-width

  • box-shadow

手机就算重绘也很慢

重排和重绘会不断触发,这是不可避免的,但是它们非常消耗资源,是导致网页性能低下的根本原因。

提高网页性能,就是要降低重排和重绘的频率和成本,尽量少触发重新渲染

大部分浏览器通过队列化修改批量显示优化重排版过程。然而有些操作会强迫刷新并要求所有计划改变的部分立刻应用。

3-2 创建图层

1. 创建图层有什么用?

我们知道浏览器layout和paint是在每一个图层上进行的,当有一个元素经常变化,为了减少这个元素对页面的影响,我们可以为这个元素创建一个单独的图层,来提供页面的性能。

2. 在什么时候会创建图层?

  • 3D或透视变换(perspective transform)CSS属性(例如translateZ(0)/translate3d(0,0,0))

  • 使用加速视频解码的<video>节点

  • 拥有3D(WebGL)上下文或加速的2D上下文的<canvas>节点

  • 混合插件(如Flash)

  • 对自己的opacity做CSS动画或使用一个动画webkit变换的元素

  • 拥有加速CSS过滤器的元素

  • 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)

  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

position为fixed也会创建图层,而absolute则不会

3. 创建图层的弊端

图层的创建也需要一定的开销,太多的图层会消耗过多的内存。这可能导致出现预期之外的行为,可能会导致潜在的崩溃。

3-3 硬件加速

1. 什么是硬件加速?

现代浏览器大都可以利用GPU来加速页面渲染。在GPU的众多特性之中,它可以存储一定数量的纹理(一个矩形的像素点集合)并且高效地操作这些纹理(比如进行特定的移动、缩放和旋转操作)。这些特性在实现一个流畅的动画时特别有用。浏览器不会在动画的每一帧都绘制一次,而是生成DOM元素的快照,并作为GPU纹理(也被叫做层)存储起来。之后浏览器只需要告诉GPU去转换指定的纹理来实现DOM元素的动画效果。这就叫做GPU合成,也经常被称作硬件加速

2. 怎么启用硬件加速?

CSS animations, transforms 以及 transitions 不会自动开启GPU加速,而是由浏览器的缓慢的软件渲染引擎来执行。那我们怎样才可以切换到GPU模式呢,很多浏览器提供了某些触发的CSS规则。

  • translate3d(0,0,0)

  • rotate3d(0,0,0,0)

  • scale3d(0,0,0)

  • translateZ(0)【可能】

只需要在css中使用这类属性,即可开启硬件加速

3. 硬件加速真的那么好吗?

从本人在移动端开发的实践来看,硬件加速是比较坑的。开启硬件加速会占有手机过多的内存而导致手机卡顿(这个时候页面也肯定卡顿了),因此在我们团队中,是禁止掉硬件加速的。

具体的原理可以参考链接5

4. 总结

做完这个任务之后, 才觉得自己真正是在做开发。严谨细致的工匠精神,把控好自己的每一行代码。面对复杂的问题,一步步分析情况,查阅资料,不断地debug,感觉提高不少。希望自己继续加油,也与抽空看这篇文章的你共勉。

5. 参考的文章

  1. http://frontenddev.org/link/the-timeline-panel-of-the-chrome-developer-tools.html#heading-1-2

  2. https://segmentfault.com/a/1190000003991459

  3. http://gold.xitu.io/entry/5584c9a2e4b06b8a728fe53d

  4. https://segmentfault.com/a/1190000000490328

  5. http://efe.baidu.com/blog/hardware-accelerated-css-the-nice-vs-the-naughty/

查看原文

赞 14 收藏 31 评论 2

SYLiu 赞了文章 · 2020-07-31

超详细的webpack原理解读

webpack原理解读

本文抄自《深入浅出webpack》,建议想学习原理的手打一遍,操作一遍,给别人讲一遍,然后就会了

在阅读前希望您已有webpack相关的实践经验,不然读了也读不懂

本文阅读需要几分钟,理解需要自己动手操作蛮长时间

0 配置文件

首先简单看一下webpack配置文件(webpack.config.js):

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
  // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
  entry: {
    bundle: [
      'webpack/hot/dev-server',
      'webpack-dev-server/client?http://localhost:8080',
      path.resolve(__dirname, 'app/app.js')
    ]
  },
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      'react': pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

1. 工作原理概述

1.1 基本概念

在了解webpack原理之前,需要掌握以下几个核心概念

  • Entry: 入口,webpack构建第一步从entry开始
  • module:模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块
  • Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割
  • Loader: 模块转换器,用于将模块的原内容按照需求转换成新内容
  • Plugin:拓展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情

1.2 流程概述

webpack从启动到结束依次执行以下操作:

graph TD
初始化参数 --> 开始编译 
开始编译 -->确定入口 
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成

各个阶段执行的操作如下:

  1. 初始化参数:从配置文件(默认webpack.config.js)和shell语句中读取与合并参数,得出最终的参数
  2. 开始编译(compile):用上一步得到的参数初始化Comiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译
  3. 确定入口:根据配置中的entry找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过处理
  5. 完成编译模块:经过第四步之后,得到了每个模块被翻译之后的最终内容以及他们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置(webpack.config.js && shell)确定输出的路径和文件名,将文件的内容写入文件系统中(fs)

在以上过程中,webpack会在特定的时间点广播特定的事件,插件监听事件并执行相应的逻辑,并且插件可以调用webpack提供的api改变webpack的运行结果

1.3 流程细节

webpack构建流程可分为以下三大阶段。

  1. 初始化:启动构建,读取与合并配置参数,加载plugin,实例化Compiler
  2. 编译:从Entry出发,针对每个Module串行调用对应的Loader去翻译文件中的内容,再找到该Module依赖的Module,递归的进行编译处理
  3. 输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统中

如果只执行一次,流程如上,但在开启监听模式下,流程如下图

graph TD

  初始化-->编译;
  编译-->输出;
  输出-->文本发生变化
  文本发生变化-->编译

1.3.1初始化阶段

在初始化阶段会发生的事件如下

事件描述
初始化参数从配置文件和shell语句中读取与合并参数,得出最终的参数,这个过程还会执行配置文件中的插件实例化语句 new Plugin()
实例化Compiler实例化Compiler,传入上一步得到的参数,Compiler负责文件监听和启动编译。在Compiler实例中包含了完整的webpack配置,全局只有一个Compiler实例。
加载插件依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时向插件中传入compiler实例的引用,以方便插件通过compiler调用webpack的api
environment开始应用Node.js风格的文件系统到compiler对象,以方便后续的文件寻找和读取
Entry-option读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备
After-plugins调用完所有内置的和配置的插件的apply方法
After-resolvers根据配置初始化resolver,resolver负责在文件系统中寻找指定路径的文件

#### 1.3.2 编译阶段 (事件名全为小写)

事件解释
run启动一次编译
Watch-run在监听模式下启动编译,文件发生变化会重新编译
compile告诉插件一次新的编译将要启动,同时会给插件带上compiler对象
compilation当webpack以开发模式运行时,每当检测到文件的变化,便有一次新的compilation被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。compilation对象也提供了很多事件回调给插件进行拓展
make一个新的compilation对象创建完毕,即将从entry开始读取文件,根据文件类型和编译的loader对文件进行==编译==,编译完后再找出该文件依赖的文件,递归地编译和解析
after-compile一次compilation执行完成
invalid当遇到错误会触发改事件,该事件不会导致webpack退出

在编译阶段最重要的事件是compilation,因为在compilation阶段调用了Loader,完成了每个模块的==转换==操作。在compilation阶段又会发生很多小事件,如下表

事件解释
build-module使用相应的Loader去转换一个模块
Normal-module-loader在使用loader转换完一个模块后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便webpack对代码进行分析
program从配置的入口模块开始,分析其AST,当遇到require等导入其他模块的语句时,便将其加入依赖的模块列表中,同时对于新找出来的模块递归分析,最终弄清楚所有模块的依赖关系
seal所有模块及依赖的模块都通过Loader转换完成,根据依赖关系生成Chunk

2.3 输出阶段

输出阶段会发生的事件及解释:

事件解释
should-emit所有需要输出的文件已经生成,询问插件有哪些文件需要输出,有哪些不需要输出
emit确定好要输出哪些文件后,执行文件输出,==可以在这里获取和修改输出的内容==
after-mit文件输出完毕
done成功完成一次完整的编译和输出流程
failed如果在编译和输出中出现错误,导致webpack退出,就会直接跳转到本步骤,插件可以在本事件中获取具体的错误原因

在输出阶段已经得到了各个模块经过转化后的结果和其依赖关系,并且将相应的模块组合在一起形成一个个chunk.在输出阶段根据chunk的类型,使用对应的模板生成最终要输出的文件内容. |

//以下代码用来包含webpack运行过程中的每个阶段
//file:webpack.config.js

const path = require('path');
//插件监听事件并执行相应的逻辑
class TestPlugin {
  constructor() {
    console.log('@plugin constructor');
  }

  apply(compiler) {
    console.log('@plugin apply');

    compiler.plugin('environment', (options) => {
      console.log('@environment');
    });

    compiler.plugin('after-environment', (options) => {
      console.log('@after-environment');
    });

    compiler.plugin('entry-option', (options) => {
      console.log('@entry-option');
    });

    compiler.plugin('after-plugins', (options) => {
      console.log('@after-plugins');
    });

    compiler.plugin('after-resolvers', (options) => {
      console.log('@after-resolvers');
    });

    compiler.plugin('before-run', (options, callback) => {
      console.log('@before-run');
      callback();
    });

    compiler.plugin('run', (options, callback) => {
      console.log('@run');
      callback();
    });

    compiler.plugin('watch-run', (options, callback) => {
      console.log('@watch-run');
      callback();
    });

    compiler.plugin('normal-module-factory', (options) => {
      console.log('@normal-module-factory');
    });

    compiler.plugin('context-module-factory', (options) => {
      console.log('@context-module-factory');
    });

    compiler.plugin('before-compile', (options, callback) => {
      console.log('@before-compile');
      callback();
    });

    compiler.plugin('compile', (options) => {
      console.log('@compile');
    });

    compiler.plugin('this-compilation', (options) => {
      console.log('@this-compilation');
    });

    compiler.plugin('compilation', (options) => {
      console.log('@compilation');
    });

    compiler.plugin('make', (options, callback) => {
      console.log('@make');
      callback();
    });

    compiler.plugin('compilation', (compilation) => {

      compilation.plugin('build-module', (options) => {
        console.log('@build-module');
      });

      compilation.plugin('normal-module-loader', (options) => {
        console.log('@normal-module-loader');
      });

      compilation.plugin('program', (options, callback) => {
        console.log('@program');
        callback();
      });

      compilation.plugin('seal', (options) => {
        console.log('@seal');
      });
    });

    compiler.plugin('after-compile', (options, callback) => {
      console.log('@after-compile');
      callback();
    });

    compiler.plugin('should-emit', (options) => {
      console.log('@should-emit');
    });

    compiler.plugin('emit', (options, callback) => {
      console.log('@emit');
      callback();
    });

    compiler.plugin('after-emit', (options, callback) => {
      console.log('@after-emit');
      callback();
    });

    compiler.plugin('done', (options) => {
      console.log('@done');
    });

    compiler.plugin('failed', (options, callback) => {
      console.log('@failed');
      callback();
    });

    compiler.plugin('invalid', (options) => {
      console.log('@invalid');
    });

  }
}
#在目录下执行
webpack
#输出以下内容
@plugin constructor
@plugin apply
@environment
@after-environment
@entry-option
@after-plugins
@after-resolvers
@before-run
@run
@normal-module-factory
@context-module-factory
@before-compile
@compile
@this-compilation
@compilation
@make
@build-module
@normal-module-loader
@build-module
@normal-module-loader
@seal
@after-compile
@should-emit
@emit
@after-emit
@done
Hash: 19ef3b418517e78b5286
Version: webpack 3.11.0
Time: 95ms
    Asset     Size  Chunks             Chunk Names
bundle.js  3.03 kB       0  [emitted]  main
   [0] ./main.js 44 bytes {0} [built]
   [1] ./show.js 114 bytes {0} [built]

2 输出文件分析

2.1 举个栗子

下面通过 Webpack 构建一个采用 CommonJS 模块化编写的项目,该项目有个网页会通过 JavaScript 在网页中显示 Hello,Webpack

运行构建前,先把要完成该功能的最基础的 JavaScript 文件和 HTML 建立好,需要如下文件:

页面入口文件 index.html

<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 Webpack 输出的 JavaScript 文件-->
<script data-original="./dist/bundle.js"></script>
</body>
</html>

JS 工具函数文件 show.js

// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通过 CommonJS 规范导出 show 函数
module.exports = show;

JS 执行入口文件 main.js

// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');

Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js 文件读取配置,所以你还需要新建它,其内容如下:

const path = require('path');

module.exports = {
  // JavaScript 执行入口文件
  entry: './main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  }
};

由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object 对象。

|-- index.html
|-- main.js
|-- show.js
|-- webpack.config.js

一切文件就绪,在项目根目录下执行 webpack 命令运行 Webpack 构建,你会发现目录下多出一个 dist目录,里面有个 bundle.js 文件, bundle.js 文件是一个可执行的 JavaScript 文件,它包含页面所依赖的两个模块 main.jsshow.js 及内置的 webpackBootstrap 启动函数。 这时你用浏览器打开 index.html 网页将会看到 Hello,Webpack

2.2 bundle.js文件做了什么

看之前记住:一个模块就是一个文件,

首先看下bundle.js长什么样子:

image-20190113213327207

注意:序号1处是个自执行函数,序号2作为自执行函数的参数传入

具体代码如下:(建议把以下代码放入编辑器中查看,最好让index.html执行下,弄清楚执行的顺序)

(function(modules) { // webpackBootstrap
  // 1. 缓存模块
  var installedModules = {};
  // 2. 定义可以在浏览器使用的require函数
  function __webpack_require__(moduleId) {

    // 2.1检查模块是否在缓存里,在的话直接返回
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 2.2 模块不在缓存里,新建一个对象module=installModules[moduleId] {i:moduleId,l:模块是否加载,exports:模块返回值}
    var module = installedModules[moduleId] = {
      i: moduleId,//第一次执行为0
      l: false,
      exports: {}
    };//第一次执行module:{i:0,l:false,exports:{}}
    // 2.3 执行传入的参数中对应id的模块 第一次执行数组中传入的第一个参数
          //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函数)
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 2.4 将这个模块标记为已加载
    module.l = true;
    // 2.5 返回这个模块的导出值
    return module.exports;
  }
  // 3. webpack暴露属性 m c d n o p
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  __webpack_require__.p = "";
  // 4. 执行reruire函数引入第一个模块(main.js对应的模块)
  return __webpack_require__(__webpack_require__.s = 0);
})
([ // 0. 传入参数,参数是个数组

  /* 第0个参数 main.js对应的文件*/
  (function(module, exports, __webpack_require__) {

    // 通过 CommonJS 规范导入 show 函数
    const show = __webpack_require__(1);//__webpack_require__(1)返回show
    // 执行 show 函数
    show('Webpack');

  }),
  /* 第1个参数 show.js对应的文件 */
  (function(module, exports) {

    // 操作 DOM 元素,把 content 显示到网页上
    function show(content) {
      window.document.getElementById('app').innerText = 'Hello,' + content;
    }
    // 通过 CommonJS 规范导出 show 函数
    module.exports = show;

  })
]);

以上看上去复杂的代码其实是一个自执行函数(文件作为自执行函数的参数),可以简写如下:

(function(modules){
    //模拟require语句
    function __webpack_require__(){}
    //执行存放所有模块数组中的第0个模块(main.js)
    __webpack_require_[0]
})([/*存放所有模块的数组*/])

bundles.js能直接在浏览器中运行的原因是,在输出的文件中通过__webpack_require__函数,定义了一个可以在浏览器中执行的加载函数(加载文件使用ajax实现),来模拟Node.js中的require语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

修改main.js,改成import引入模块

import show from './show';
show('Webpack');

在目录下执行webpack,会发现:

  1. 生成的代码会有所不同,但是主要的区别是自执行函数的参数不同,也就是2.2代码的第二部分不同
([//自执行函数和上面相同,参数不同
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__show__ = __webpack_require__(1);

Object(__WEBPACK_IMPORTED_MODULE_0__show__["a" /* default */])('Webpack');


}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = show;
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}


})
]);

参数不同的原因是es6的import和export模块被webpack编译处理过了,其实作用是一样的,接下来看一下在main.js中异步加载模块时,bundle.js是怎样的

2.3异步加载时,bundle.js代码分析

main.js修改如下

import('./show').then(show=>{
    show('Webpack')
})

构建成功后会生成两个文件

  1. bundle.js 执行入口文件
  2. 0.bundle.js 异步加载文件

其中0.bundle.js文件的内容如下:

webpackJsonp(/*在其他文件中存放的模块的ID*/[0],[//本文件所包含的模块
/* 0 */,
/* 1 show.js对应的模块 */
(function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  /* harmony export (immutable) */ 
  __webpack_exports__["default"] = show;

  function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
  }

})
]);

bundle.js文件的内容如下:

注意:bundle.js比上面的bundle.js的区别在于:

  1. 多了一个__webpack_require__.e,用于加载被分割出去的需要异步加载的chunk对应的文件
  2. 多了一个webpackJsonp函数,用于从异步加载的文件中安装模块
(function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
  var parentJsonpFunction = window["webpackJsonp"];
  // webpackJsonp用于从异步加载的文件中安装模块
  // 将webpackJsonp挂载到全局是为了方便在其他文件中调用
  /**
   * @param chunkIds 异步加载的模块中需要安装的模块对应的id
   * @param moreModules 异步加载的模块中需要安装模块列表
   * @param executeModules 异步加载的模块安装成功后需要执行的模块对应的index
   */
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, resolves = [], result;
        for(;i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if(installedChunks[chunkId]) {
                resolves.push(installedChunks[chunkId][0]);
            }
            installedChunks[chunkId] = 0;
        }
        for(moduleId in moreModules) {
            if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                modules[moduleId] = moreModules[moduleId];
            }
        }
        if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
        while(resolves.length) {
            resolves.shift()();
        }
    };
    // The module cache
    var installedModules = {};
    // objects to store loaded and loading chunks
    var installedChunks = {
        1: 0
    };
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  /**
   * 用于加载被分割出去的需要异步加载的chunk对应的文件
   * @param chunkId 需要异步加载的chunk对应的id
   * @returns {Promise}
   */
    __webpack_require__.e = function requireEnsure(chunkId) {
      var installedChunkData = installedChunks[chunkId];
      if(installedChunkData === 0) {
        return new Promise(function(resolve) { resolve(); });
      }
      // a Promise means "currently loading".
      if(installedChunkData) {
        return installedChunkData[2];
      }
      // setup Promise in chunk cache
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      installedChunkData[2] = promise;
      // start chunk loading
      var head = document.getElementsByTagName('head')[0];
      var script = document.createElement('script');
      script.type = "text/javascript";
      script.charset = 'utf-8';
      script.async = true;
      script.timeout = 120000;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
      var timeout = setTimeout(onScriptComplete, 120000);
      script.onerror = script.onload = onScriptComplete;
      function onScriptComplete() {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) {
          if(chunk) {
            chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
          }
          installedChunks[chunkId] = undefined;
        }
      };
      head.appendChild(script);
      return promise;
    };
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module['default']; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, 'a', getter);
        return getter;
    };
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    // __webpack_public_path__
    __webpack_require__.p = "";
    // on error function for async loading
    __webpack_require__.oe = function(err) { console.error(err); throw err; };
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([//存放没有经过异步加载的,随着执行入口文件加载的模块
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(show=>{
    show('Webpack')
})


/***/ })
]);
查看原文

赞 73 收藏 51 评论 3

SYLiu 赞了文章 · 2020-07-30

Webpack原理-编写Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  
  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。
在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。
插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
并且可以通过 compiler 对象去操作 Webpack。

通过以上最简单的 Plugin 相信你大概明白了 Plugin 的工作原理,但实际开发中还有很多细节需要注意,下面来详细介绍。

Compiler 和 Compilation

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。
Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。
这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。
Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。
Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。
Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {
  
});

同理,compilation.apply 和 compilation.plugin 使用方法和上面一致。

在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。

在开发插件时,还需要注意以下两点:

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:

    compiler.plugin('emit',function(compilation, callback) {
      // 支持处理逻辑
    
      // 处理完毕后执行 callback 以通知 Webpack 
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
      callback();
    });

常用 API

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。
由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。

读取输出资源、代码块、模块及其依赖

有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。
插件代码如下:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一个代码块
        // 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
        chunk.forEachModule(function (module) {
          // module 代表一个模块
          // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
        // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
        // 该 Chunk 就会生成 .js 和 .css 两个文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放当前所有即将输出的资源
          // 调用一个输出资源的 source() 方法能获取到输出资源的内容
          let source = compilation.assets[filename].source();
        });
      });

      // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
      callback();
    })
  }
}

监听文件变化

4-5使用自动刷新 中介绍过 Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时,
就会触发一次新的 Compilation。

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:

// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
    // 获取发生变化的文件列表
    const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
    // changedFiles 格式为键值对,键为发生变化的文件路径。
    if (changedFiles[filePath] !== undefined) {
      // filePath 对应的文件发生了变化
    }
    callback();
});

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。
由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。
为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好,
需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

读取 compilation.assets 的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});

判断 Webpack 使用了哪些插件

在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 Webpack 当前的插件配置情况。
以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

实战

下面我们举一个实际的例子,带你一步步去实现一个插件。

该插件的名称取名叫 EndWebpackPlugin,作用是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。
同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法如下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败,err 是导致错误的原因
      console.error(err);        
    })
  ]
}

要实现该插件,需要借助两个事件:

  • done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;

实现该插件非常简单,完整代码如下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // 在 done 事件中回调 doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // 在 failed 事件中回调 failCallback
        this.failCallback(err);
    });
  }
}
// 导出插件 
module.exports = EndWebpackPlugin;

从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。
5-1工作原理概括 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。

本实例提供项目完整代码

《深入浅出Webpack》全书在线阅读链接

阅读原文

查看原文

赞 46 收藏 39 评论 4

SYLiu 赞了文章 · 2020-07-20

剖析Vue原理&实现双向绑定MVVM

本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm

相信大家对mvvm双向绑定应该都不陌生了,一言不合上代码,下面先看一个本文最终实现的效果吧,和vue一样的语法,如果还不了解双向绑定,猛戳Google

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>

<script data-original="./js/observer.js"></script>
<script data-original="./js/watcher.js"></script>
<script data-original="./js/compile.js"></script>
<script data-original="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

效果:
图片描述

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

上述流程如图所示:
图片描述

1、实现Observer

ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,let's do it
我们知道可以利用Obeject.defineProperty()来监听属性变动
那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。。相关代码可以是这样:

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
图片描述

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attrv-text便是指令,而other-attr不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
如果有点乱,可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

ok, Watcher也已经实现了,完整代码
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈

至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

最后,感谢您的阅读!

查看原文

赞 1326 收藏 1792 评论 152

SYLiu 发布了文章 · 2020-06-01

防抖、节流

防抖(debounce)

n秒内执行多次同一函数只有最后一次有效。

实现方式:使用计时器,在 n 秒内再执行该函数,如果 n 秒内再次调用该函数,则重置计时器。

节流(throttle)

n秒内执行多次同一函数只有第一次有效。

实现方式:第一次调用立即执行。使用计时器,如果 n 秒内再次调用该函数,此次调用不执行函数。

比较

  • 防抖只有最后一次才有效;节流会有很多次有效(但是频率会被稀释)
  • 防抖的函数执行会有延迟;节流的立即执行第一次

想象一个绑定了点击回调函数的按钮,你以极快的速度不停地点击它。

  • 如果加了防抖,那么函数一直不会执行。只有当你停下来 n 秒后函数才会执行
  • 如果加了节流,那么函数会立即执行,但是后面的函数执行频率会远小于你的点击频率

如果快速记忆呢?很简单:

  • 防抖可以保证你最后一次的执行是稳定的,不会出现“抖动”,因此叫做“防抖”。但是防抖不关心“流量”(函数执行的频率),即一直没有“流量”也没有关系,只要最后有一次成功即可。
  • 节流限制了频率,但不会出现一直不执行的情况,所以叫做“节流”,也就是说还是会有“流量”。

因此应用起来的区别也很明显了:两者都可以降低频率,如果你想要保证最后一次必须执行,就用防抖;如果想要保证第一次必须执行,就用节流。

查看原文

赞 1 收藏 0 评论 0

SYLiu 赞了文章 · 2020-04-07

HTML5:给汉字加拼音?收起展开组件?让我秀给你看

来看看 HTML 的历史和规范常识。HTML 规范是 W3C 与 WHATWG 合作共同产出的,HTML5 因此也不例外。其中:

  • W3C 指 World Wide Web Consortium
  • WHATWG 指 Web Hypertext Application Technology Working Group

说好听了是“合作产出”,但其实更像是“HTML5 有两套规范”。但话说天下大势合久必分,分久必合,如今(就在前几天,2018.5.29)它们又表示将会开发单一版本的 HTML 规范。

HTML5新增的标签和功能,常规的我相信大家都知道,这里就不啰嗦了,这里介绍两个大家可能不知道的功能,很实用!

给汉字加拼音

代码如下

<ruby>
  做工程师不做码农
  <rt>zuo gong cheng shi bu zuo ma nong</rt>
</ruby>

效果如下

展开收起组件

简单几行代码

<details>
  <summary>公众号《前端外文精选》</summary>
  欢迎大家关注我的公众号,专注大前端、全栈、程序员成长的公众号!如果对你有所启发和帮助,可以点个关注、收藏,也可以留言讨论,这是对作者的最大鼓励。
  作者简介:Web前端工程师,全栈开发工程师、持续学习者。
</details>

就可以实现如下效果

是不是很棒啊 ?

以往要实现这样的内容,我们都必须依靠 JavaScript 实现。现在来看,HTML 也变得更加具有“可交互性”。

原生进度条和度量

progress 标签显示进度:

值得一提的是:progress 不适合用来表示度量衡,如果想表示度量衡,我们应该使用 meter 标签代替。这又是什么标签?

meter 用来度量给定范围(gauge)内的数据:

<meter value="3" min="0" max="10"></meter> 十分之三<br>
<meter value="0.6"></meter> 60%

Chrome显示效果如下

本文示例效果和完整代码已放在我的博客小码页面。


相关阅读:


如果对你有所启发和帮助,可以点个关注、收藏、转发,也可以留言讨论,这是对作者的最大鼓励。

作者简介:Web前端工程师,全栈开发工程师、持续学习者。

现在关注 《前端外文精选》 微信公众号,还送某网精品视频课程网盘资料啊,准能为你节省不少钱!

查看原文

赞 23 收藏 16 评论 2

SYLiu 关注了用户 · 2020-02-27

美团技术团队 @meituanjishutuandui

一个只分享有价值技术干货的微信公众号

美团技术团队会定期推送来自一线的实践技术文章,涵盖前端(Web、iOS和Android)、后台、大数据、AI/算法、测试、运维等技术领域。 在这里,与8000多业界一流工程师交流切磋。

关注 4833

SYLiu 关注了用户 · 2020-02-27

前端小智 @minnanitkong

我不是什么大牛,我其实想做的就是一个传播者。内容可能过于基础,但对于刚入门的人来说或许是一个窗口,一个解惑之窗。我要先坚持分享20年,大家来一起见证吧。

关注 9470

SYLiu 关注了用户 · 2020-02-27

shanyue @xiange

暮从碧山下,山月(shanyue)随人归。

欢迎关注公众号山月行

关注 602

SYLiu 赞了文章 · 2020-02-27

前端高级进阶:网站的缓存控制策略最佳实践及注意事项

这是山月关于高级前端进阶暨前端工程系列文章的第 M 篇文章 (M 随便打的,毕竟也不知道能写多少篇),关于前 M-1 篇文章,可以从我的 github repo shfshanyue/blog 中找到,如果点进去的话可以捎带~点个赞~,如果没有点进去的话,那就给这篇文章点个赞。。今天的文章开始了

本篇文章地址在 前端工程化系列,欢迎订阅。

  1. 前端高级进阶:javascript 代码是如何被压缩
  2. 前端高级进阶:如何更好地优化打包资源
  3. 前端高级进阶:网站的缓存控制策略最佳实践及注意事项

对于一个网站来讲,性能关乎用户体验,你在更短的时间内打开网站,你将会留住更多的用户。如果你的页面十秒才能打开,那再好的用户交互也是徒然。

缓存控制是网站性能优化中至为常见及重要的一环,好的缓存控制,除了使网站在性能方面有所提升,在财务方面也有重要提升: 更好的缓存策略意味着更少的请求,更少的流量,更少的峰值带宽,从而节省一大笔服务器或者 CDN 的费用。

缓存控制策略就是 http caching 的策略,化繁为简,最有效的策略往往是很简单的。在最简单的粗略下,你对 http cache 只需要了解一个 Cache-Control 的头部。

一个较好的缓存策略只需要两部分,而它们只需要通过 Cache-Control 控制:

  1. 带指纹资源: 永久缓存
  2. 非带指纹资源: 每次进行新鲜度校验

作图如下:

缓存控制策略

带指纹资源: 永久缓存

Cache-Control: max-age=31536000

天下武功,无坚不摧,唯快不破。资源请求最快的方式就是不向服务器发起请求,通过以上响应头可以对资源设置永久缓存。

  1. 静态资源带有 hash 值,即指纹
  2. 对资源设置一年过期时间,即 31536000,一般认为是永久缓存
  3. 在永久缓存期间浏览器不需要向服务器发送请求

那为什么带有 hash 值的资源可以永久缓存呢?

因为该文件的内容发生变化时,会生成一个带有新的 hash 值的 URL。 前端将会发起一个新的 URL 的请求。

非带指纹资源: 每次进行新鲜度校验

Cache-Control: no-cache
  1. 由于不带有指纹,每次都需要校验资源的新鲜度。(从缓存中取到资源,可能是过期资源)
  2. 如果校验为最新资源,则从浏览器的缓存中加载资源

index.html 为不带有指纹资源,如果把它置于缓存中,则如何保证服务器刷新数据时,被浏览器可以获取到新鲜的资源?

因此,使用 Cache-Control: no-cache 时,客户端每次对服务器进行新鲜度校验。

PS:no-cache 与 no-store 的区别是什么?

即使每次校验新鲜度,也不需要每次都从服务器下载资源: 如果浏览器/CDN上缓存经校验没有过期。这被称为协商缓存,此时 http 状态码返回 304,指 Not Modified,即没有变更。

幸运的是,关于协商缓存,你无需管理,也无需配置,nginx 或者一些 OSS 都会自动配置协商缓存。

而对于协商缓存,也有它们自己的算法,协商缓存的背后基于响应头 Last-Modified/ETag。浏览器每次请求资源时,会携带上次服务器响应的 ETag/Last-Modified 作为标志,与服务端此时的 ETag/Last-Modified 作比较,来判断内容更改。

http 响应头中的 ETag 值是如何生成的?

而在操作系统底层,Last-Modified 往往通过文件系统(file system)中的 mtime 属性生成。而 ETag 提供比 Last-Modified 更精细的检验粒度,由文件内容的 hash 或者 mtime/size 生成。当然,这是后话。

一定要为你的资源添加 Cache-Control 响应头

我会经常接触到一些网站,他们的资源文件并没有 Cache-Control 这个响应头。究其原因,在于缓存策略配置这个工作的职责不清,有时候它需要协调前端和运维。

那如果不添加 Cache-Control 这个响应头会怎么样?

是不是每次都会自动去服务器校验新鲜度,很可惜,不是。 此时会对资源进行强制缓存,而对不带有指纹信息的资源很有可能获取到过期资源。 如果过期资源存在于浏览器上,还可以通过强制刷新浏览器来获取最新资源。但是如果过期资源存在于 CDN 的边缘节点上,CDN 的刷新就会复杂很多,而且有可能需要多人协作解决。

那默认的强制缓存时间是多少

首先要明确两个响应头代表的含义:

  1. Date: 指源服务器响应报文生成的时间,差不多与发请求的时间等价
  2. Last-Modified: 指静态资源上次修改的时间,取决于 mtime

LM factor 算法认为当请求服务器时,如果没有设置 Cache-Control,如果距离上次的 Last-Modified 越远,则生成的强制缓存时间越长。

用公式表示如下,其中 factor 介于 0 与 1 之间:

MaxAge = (Date - LastModified) * factor

Bundle Splitting:尽量减少资源变更

得益于单页应用与前端工程化的发展,经过打包后,基本上所有资源都是带有指纹信息的,这意味着所有的资源都是能够设置永久缓存。打包策略如下图所示:

缓存控制策略

但仅仅如此了吗?

如果你所有的 js 资源都打包成一个文件,它确实有永久缓存的优势。但是当有一行文件进行修改时,这一个大包的指纹信息发生改变,永久缓存失效。

所以我们现在需要做到的是:当修改文件后,造成最小范围的缓存失效。webpack 等打包工具虽然在 optimization 上内置了很多性能优化,但它不会帮你做这件事,这件事情需要自己动手。

缓存控制策略

此时我们可以对资源进行分层次缓存的打包方案,这是一个建议方案:

  1. webpack-runtime: 应用中的 webpack 的版本比较稳定,分离出来,保证长久的永久缓存
  2. react/react-dom: react 的版本更新频次也较低
  3. vendor: 常用的第三方模块打包在一起,如 lodashclassnames 基本上每个页面都会引用到,但是它们的更新频率会更高一些。另外对低频次使用的第三方模块不要打进来
  4. pageA: A 页面,当 A 页面的组件发生变更后,它的缓存将会失效
  5. pageB: B 页面
  6. echarts: 不常用且过大的第三方模块单独打包
  7. mathjax: 不常用且过大的第三方模块单独打包
  8. jspdf: 不常用且过大的第三方模块单独打包

随着 http2 的发展,特别是多路复用,初始页面的静态资源不受资源数量的影响。因此为了更好的缓存效果以及按需加载,也有很多方案建议把所有的第三方模块进行单模块打包。

小结

缓存控制策略

与我交流

扫码添加我的机器人微信,将会自动(自动拉人程序正在研发中)把你拉入前端高级进阶学习群

我是山月,可以加我微信 shanyue94 与我交流,备注交流。另外可以关注我的公众号【全栈成长之路】

如果你对全栈面试,前端工程化,graphql,devops,个人服务器运维以及微服务感兴趣的话,可以关注我

查看原文

赞 8 收藏 5 评论 1

SYLiu 关注了问题 · 2020-02-27

解决在vue中如果import一个不存在的模块,如何使程序 正常运行?

在vue中如果import一个不存在的文件,如何使程序 正常运行?
比如使用这条语句

import moduleName from 'module';

程序报错
image.png

如果试用try catch

try {
    import math from '..test/'
}
catch(err){
    console.log(err)
}

就会报这个错误
image.png

但是我想让程序正常运行,或者友好提示报错,而不是终止程序,该怎么做?

关注 4 回答 3

SYLiu 回答了问题 · 2020-02-27

如何用vue写一个方法,按顺序同步执行?

<div :class="animateClass"></div>

然后动态修改 animateClass 的值。

关注 2 回答 2

SYLiu 赞了文章 · 2020-02-23

小程序本地测试:开发者工具能请求后台数据,手机预览却不行

在微信小程序本地开发测试过程中(这里指的是本地测试本地测试本地测试,重要的事说三遍),会遇到一个坑:在微信开发者工具中能正常请求本地后台数据,但在手机预览中却请求不到,如下图所示:

1.在微信开发者工具中数据正常显示

2.在手机预览中无数据

解决此问题需要有以下4点设置:

1、在微信开发者工具中设置:不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书

2、wx.request请求的地址不得使用localhost,而应改成本地服务器所在的电脑IP

假设电脑的IP为192.168.0.110;要请求的地址为:index/list/getdata;代码书写如下:

wx.request({  
  //url: 'http://localhost/index/list/getdata',错误请求地址  
  url: 'http://192.168.0.110/index/list/getdata',//正确请求地址  
  data: {},  
  header: {    
   'content-type': 'application/json'  
  },  
  success (res) {    
   console.log(res.data)  
  }
})

3、手机和电脑(本地服务器)需要连接同一局域网(WIFI网络)

4、手机扫码进入小程序后,需要打开调试模式才能请求到数据

进入小程序后,此时页面的数据依旧是空的;点击右上角三个点,打开调试

打开调试后会自动关闭当前小程序,需要重新进入,调试模式才生效

重新进入小程序,可以看到页面数据已经出来了,并且右下角有个绿色方形的调试工具按钮

点击右下角的调试工具按钮,可以看到页面的所有数据,方便开发者在手机端更好的测试

总结

1.本地测试时,微信开发者工具务必勾选(不校验合法域名)此设置,否则微信开发者工具请求不到数据;待上线时,再取消此设置。有关为何要设置(不校验合法域名)的文档地址如下:
https://developers.weixin.qq....

2.在使用wx.request请求地址,如果url是localhost格式的话,虽然在微信开发者工具中是可以请求到后台数据,但是手机预览时,数据无法请求。因为localhost是指本地服务器所在的那台电脑,手机访问localhost并不知道localhost是什么,所以需要使用本机的IP+请求地址

3.疑问:为何手机开启了调试模式就可以请求到数据,未开启却请求不到?这其实跟前面微信开发者工具设置不校验合法域名的道理是一样的

最后

觉得文章不错的,给我点个赞哇,关注一下呗!
技术交流可关注微信公众号【GitWeb】,加我好友一起探讨

查看原文

赞 37 收藏 2 评论 3

SYLiu 发布了文章 · 2020-02-23

使用 GitHub Actions 自动部署博客教程

本篇以 Github Pages 为例,并且假设你已经掌握了 GitHub Pages 的使用。

假设你的文章和静态文件在同一个仓库,使用 master 分支管理文章和代码,使用 gh-pages 分支存放生成的静态文件

一般部署博客的流程是:

  1. 写一篇文章
  2. 生成静态文件:npm run build
  3. 切换 gh-pages 分支
  4. 复制静态文件到 gh-pages 分支
  5. 访问网址验证是否成功

博客就是用来写文章的,每次写篇文章还要搞那么多操作。

当你使用了 GitHub Actions 之后,流程可以简化为:

  1. 写一篇文章
  2. 提交到 GitHub

结束了,是不是很方便?

从机械的流程中解脱,专注于写作。

那么开始打造我们的 GitHub Actions 吧。

我创建了一个示例项目在我的 GitHub 仓库,用的是 VuePress(一个 Vue 官方的静态站点生成器)。

设置 Secrets

后面部署的 Action 需要有操作你的仓库的权限,因此提前设置好 GitHub personal access(个人访问令牌)。

生成教程可以看 GitHub 官方的帮助文档:创建用于命令行的个人访问令牌

授予权限的时候只给 repo 权限即可。

1W3GRA.png

令牌名字一定要叫:ACCESS_TOKEN,这是后面的 Action 需要用的。

1W35i4.png

编写 workflow 文件

持续集成一次运行的过程,就是一个 workflow(工作流程)。

项目结构如图:

123CDO.png

创建.github/workflows/main.yml文件(可以到我的示例仓库中查看),内容如下:

name: Deploy GitHub Pages

# 触发条件:在 push 到 master 分支后
on:
  push:
    branches:
      - master

# 任务
jobs:
  build-and-deploy:
    # 服务器环境:最新版 Ubuntu
    runs-on: ubuntu-latest
    steps:
      # 拉取代码
      - name: Checkout
        uses: actions/checkout@v2
        with:
          persist-credentials: false

      # 生成静态文件
      - name: Build
        run: npm install && npm run docs:build

      # 部署到 GitHub Pages
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          BRANCH: gh-pages
          FOLDER: docs/.vuepress/dist

这里我就不对语法作讲解了,需要了解 workflow 的基本语法可以查看官方帮助,也可以参考阮一峰老师的 GitHub Actions 入门教程

这里我写了三步:

  1. 拉取代码。用到了一个 GitHub 官方 action:actions/checkout
  2. 生成静态文件。直接运行脚本,如果你不是用的 VuePress 或者脚本不一样,要修改成你自己的。
  3. 部署到 GitHub Pages。使用了第三方作者的 action:JamesIves/github-pages-deploy-action@releases/v3。我详细介绍下这个 action:

使用 with 参数向环境中传入了三个环境变量:

  1. ACCESS_TOKEN:读取 GitHub 仓库 secrets 的 ACCESS_TOKEN 变量,也就是我们前面设置的
  2. BRANCH:部署分支 gh-pages(GitHub Pages 读取的分支)
  3. FOLDER:需要部署的文件在 docs/.vuepress/dist 路径,也就是我们使用 npm run docs:build 生成的静态文件的位置
这里有一点需要注意:我使用的是 v3 版本,需要使用 with 参数传入环境变量,且需要自行构建;网上常见的教程使用的是 v2 版本,使用 env 参数传入环境变量,不需要自行构建,可使用 BUILD_SCRIPT 环境变量传入构建脚本

至此,配置工作均已完成。提交你的代码,就会开启自动构建。

以后,你每次有代码 push 到 master 分支时,GitHub 都会开始自动构建。

验证

部署失败

按照教程是不会失败的,但是建议你了解下失败的情况。

如果部署失败你会收到一封部署失败的邮件

1WR8YQ.png

点击可以跳转到仓库的 Action 页面查看日志

1WRclR.png

展开错误的部署 job 查看日志

1WRX0f.png

可以看到有这么一个错误日志:No such file or directory,经排查,是没有生成静态文件,因此导致路径不存在。

我这个错误是由于参考了网上常见的教程导致的。网上大部分教程(包括前面提到的阮一峰老师的教程)使用的是:JamesIves/github-pages-deploy-actionv2 版本,然而引用的时候写的都是:JamesIves/github-pages-deploy-action@master,引用的 master 分支。在作者写的时候可能当时这个 action 最新的就是 v2 版本,所以没有什么问题。然而现在 master 分支已经是 v3 版本了,语法有变化,完全按照教程就会出错。如果继续使用他的教程可以改成JamesIves/github-pages-deploy-action@releases/v2

修复后重新提交,GitHub 会再次部署。

部署成功

点击仓库的 Actions,查看部署情况。

如果正在部署中,应该是图中标出的这个样子。

1WWL8J.png

这里显示所有的部署(应该叫做 Workflow,便于理解,就叫 部署 了)记录。图中有三种状态,分别是:部署中(黄色动态图标)、部署完成(绿色对号图标)、部署失败(红色错号图标)。

点击进入查看部署情况。

147bLD.png

部署成功,全是绿色~

接下来访问网站验证一下:https://lasyislazy.github.io/gh-pages-action-demo/

1W4BuR.png

完美结束,享受 GitHub Actions 带来的愉快体验吧~

其他

鉴于还是有很多人不是用的 GitHub Pages,我这里再提供一下其他方式的思路,其实都是一样的,大概分成三步:

  1. 拉取代码
  2. 生成静态文件
  3. 部署到服务器

前两步都是一样的,不同的方式区别也就在于第三步。

使用 GitHub Pages 的话可以使用 JamesIves/github-pages-deploy-action 这个 action,使用其他的方式其实也可以找到对应的 action。

例如,我的网站是部署在虚拟主机空间上的,平时部署是用 FTP 的方式替换主机空间上的静态文件,因此找到了一个可以部署到 FTP 上的 acton SamKirkland/FTP-Deploy-Action,然后第三步就变为了:

      - name: FTP Deploy
        uses: SamKirkland/FTP-Deploy-Action@2.0.0
        env:
          FTP_SERVER: xxx.xxx.com
          FTP_USERNAME: xxxx
          FTP_PASSWORD: ${{ secrets.BLOG_FTP_PASSWORD }}
          LOCAL_DIR: docs/.vuepress/dist
          REMOTE_DIR: /htdocs
          ARGS: --delete --transfer-all --exclude=logreport --verbose

一般都是传一些环境变量进去就可以了,需要哪些环境变量去这个 action 的 GitHub 上看下介绍就好了。

最后再说一下怎么找 action,以下是几个常用的网址:

然后就是要利用好搜索引擎了。

实在找不到就得自己写 action 了,本篇就不讨论了。

以上便是本篇教程全部内容,本篇首发于我的个人博客:https://www.lasy.site/

参考链接:

查看原文

赞 5 收藏 1 评论 0