沙雕爹爹

Stay foolish, stay humble.


  • Home

  • Archives

will-change属性提高页面滚动、动画渲染性能(译)

Posted on 2019-04-30

参考链接:

  • fix-scrolling-performance-css-will-change-property
  • introduction-css-will-change-property
  • MDN

问题引入

最开始是为了解决一个滚动引起的性能问题:一个很大的元素,有一张视窗大小的背景图,使用background-attachment: fixed让它在滚动时一直处于可视区内,实现视觉差的效果。

fixed带来的问题

使用background-attachment: fixed会导致每次用户滚动时都会引发绘制操作。为什么?页面需要重新定位内容,但它的背景图却应该仍在原来的位置不动,浏览器必须在相对于它实际DOM元素的新的位置上重绘。这是个很大的性能问题。

诊断问题

使用Chrome DevTools来查看Frame的绘制性能。

优化前:

优化前

优化后:

优化后

解决问题

使用will-change属性。

原始的CSS:

1
2
3
4
5
6
7
8
9
10
.what-we-do-cards {
@include clearfix;
border-top: 10px solid rgba(255, 255, 255, .46);
background-color: white;
background: url('/img/front/strategy.jpg') no-repeat center center;
background-attachment: fixed;
background-size: cover;
color: $white;
padding-bottom: 4em;
}

我们的背景图片使用了两个GPU敏感的CSS特性:background-size: cover和background-attachment: fixed。

GPU友好的CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.what-we-do-cards {
@include clearfix;
border-top: 10px solid rgba(255, 255, 255, .46);
color: $white;
padding-bottom: 4em;
overflow: hidden; // added for pseudo-element
position: relative; // added for pseudo-element

// Fixed-position background image
&::before {
content: ' ';
position: fixed; // instead of background-attachment
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: white;
background: url('/img/front/strategy.jpg') no-repeat center center;
background-size: cover;
will-change: transform; // creates a new paint layer
z-index: -1;
}
}

最重要的部分是伪元素上的will-change: transform属性。这个属性的作用是跟浏览器说:“hi浏览器,这个元素在未来会改变,请在新的层绘制它,这样它的邻居不会影响它的绘制。“

深入will-change

背景

很多前端工程师依赖CSS3 transitions,transforms和animations来增加新的层,现在我们只使用少量的CSS就可以实现顺畅漂亮的动画。如果你用过这些CSS属性,你很可能已经了解CPU,GPU和硬件加速了。下面我们快速过一下这些概念:

  • The CPU, or the Central Processing Unit, is the piece of hardware that processes pretty much every computer operation. It’s otherwise known as the motherboard.
  • The GPU, or the Graphics Processing Unit, is the piece of hardware associated with processing and rendering graphics. The GPU is designed to perform complex graphical computations and offloads some serious process weight from the CPU.
  • Hardware acceleration is a general term for offloading CPU processes onto another dedicated piece of hardware. In the world of CSS transitions, transforms, and animations, it implies that we’re offloading the process onto the GPU, and hence speeding it up. This occurs by pushing the element to a layer of its own, where it can be rendered independently while undergoing its animation.

它是怎么提高性能和动画质量的呢?在基于webkit的浏览器中,当执行一下CSS操作时,经常会看到闪烁,即2D transform和animation。我们需要欺骗浏览器,让它把这些操作当作3D来执行,这样就可以把这些操作从CPU转移到GPU上做计算。因为3D的计算会自动放到GPU上。

什么是will-change?

CSS 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。

用好这个属性并不是很容易:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。

  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。

  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。

  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

定义 value
初始值 auto
适用元素 all elements
是否是继承属性 否
适用媒体 all
计算值 as specified
Animation type discrete
正规顺序 the unique non-ambiguous order defined by the formal grammar

语法

标准语法: auto | <animateable-feature>
其中
<animateable-feature> = scroll-position | contents | <custom-ident>

1
2
3
4
5
6
7
8
9
10
will-change: auto
will-change: scroll-position
will-change: contents
will-change: transform // Example of <custom-ident
will-change: opacity // Example of <custom-ident>
will-change: left, top // Example of two <animateable-feature>

will-change: unset
will-change: initial
will-change: inherit

取值

auto
表示没有特别指定哪些属性会变化,浏览器需要自己去猜,然后使用浏览器经常使用的一些常规方法优化。

<animateable-feature> 可以是以下值:

scroll-position
表示开发者希望在不久后改变滚动条的位置或者使之产生动画。

contents
表示开发者希望在不久后改变元素内容中的某些东西,或者使它们产生动画。

<custom-ident>
表示开发者希望在不久后改变指定的属性名或者使之产生动画。如果属性名是简写,则代表所有与之对应的简写或者全写的属性。

示例

1
2
3
.sidebar {
will-change: transform;
}

以上示例在样式表中直接添加了 will-change 属性,会导致浏览器将对应的优化工作一直保存在内存中,这其实是不必要的,前面我们已经看过为什么应该避免这样的做法。下面是另一个展示如何使用脚本正确地应用 will-change 属性的示例,在大部分的场景中,你都应该这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var el = document.getElementById('element');

// 当鼠标移动到该元素上时给该元素设置 `will-change` 属性
el.addEventListener('mouseenter', hintBrowser);
// 当 CSS 动画结束后清除 `will-change` 属性
el.addEventListener('animationEnd', removeHint);

function hintBrowser() {
// 填写上那些你知道的,会在 CSS 动画中发生改变的 CSS 属性名们
this.style.willChange = 'transform, opacity';
}

function removeHint() {
this.style.willChange = 'auto';
}

但是,如果某个应用在按下键盘的时候会翻页,比如相册或者幻灯片一类的,它的页面很大很复杂,此时在样式表中写上 will-change 是合适的。这会使浏览器提前准备好过渡动画,当键盘按下的时候就能立即看到灵活轻快的动画。

1
2
3
.slide {
will-change: transform;
}

mac配置完ssh依然提示Enter passphrase for key解决方法

Posted on 2019-04-28

已经配置git ssh key,但在git pull/push的时候仍然需要输入密码,并且提示:

Enter passphrase for key 'xxxx'

解决办法:

输入命令

ssh-add -K xxx

中高级前端大厂面试秘籍(转)

Posted on 2019-04-28

上篇
中篇
下篇

上篇

CSS

盒模型

页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing进行设置。根据计算宽高的区域可分为:

content-box (W3C 标准盒模型)
border-box (IE 盒模型)
padding-box
margin-box (浏览器未实现)

作者:郭东东
链接:https://juejin.im/post/5c64d15d6fb9a049d37f9c20
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

深入浅出webpack(转)

Posted on 2019-04-28

原文地址

手写一个webpack插件(转)

Posted on 2019-04-28

原文地址

webpack本质上是一种事件流的机制,核心是Tapable。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。

Tapable是什么?

Tapable暴露了很多Hook(钩子)类,为插件提供挂载。

1
2
3
4
5
6
7
8
9
10
11
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require('tapable');

Tapable用法

  1. 新建钩子new Hook();
  2. 使用tap/tapAsync/tapPromise绑定钩子
  3. call/callAsync执行绑定事件
1
2
3
4
5
6
// 1
const hook1 = new SyncHook(['arg1', 'arg2', 'arg3']);
// 2
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3));
// 3
hoo1.call(1, 2, 3); // 1, 2, 3

举个例子

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
37
38
const { SyncHook, AsyncParallelHook } = require('tapable');

class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newSpeed']),
break: new SyncHook(),
caculateRoutes: new AsyncParallelHook(['source', 'target', 'routesList'])
};
}
}

const myCar = new Car();

myCar.hooks.break.tap('WarningLampPlugin', () => console.log('WarningLampPlugin'));

myCar.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log(`Accelerate to ${newSpeed}`));

myCar.hooks.calculateRoutes.tapPromise('calculateRoutes tapPromise', (source, target, routesList, callback) => {
return new Promise((resolve, rejct) => {
setTimeout(() => {
console.log(`tapPromise to ${source} ${target} ${routesList}`);
resolve();
}, 1000)
})
});

myCar.hooks.break.call();
myCar.hooks.accelerate.call('hello');

console.time('cost');

myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
console.timeEnd('cost');
}).catch(err => {
console.error(err);
console.timeEnd('cost');
})

进阶一下

上面学习了Tapable的用法,但它和webpack/plugin有什么关系呢?

我们将刚才但代码稍作改动,拆成两个文件:Compiler.js、MyPlugin.js.

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
37
38
39
40
41
42
43
44
const {
SyncHook,
AsyncParallelHook
} = require('tapable');

class Compiler {
constructor(options) {
this.hooks = {
accelerate: new SyncHook(['newSpeed']),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(['source', 'target', 'routesList'])
};

let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
}

run() {
console.time('cost');
this.accelerate('hello');
this.break();
this.caculateRoutes('i', 'love', 'tapable');
}

accelerate(param) {
this.hooks.accelerate.call(param);
}

break() {
this.hooks.break.call();
}

calculateRoutes() {
const args = Array.from(arguments);
this.hooks.calculateRoutes.callAsync(...args, err=> {
console.time('cost');
if (err) console.error(err);
});
}
}

module.exports = Compiler;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Compiler = require('./Compiler');

class MyPlugin {
constructor() {}

apply(compiler) {
compiler.hooks.break.tap('WarningLampPlugin', () => console.log('WarningLampPlugin'));
compiler.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log(`Accelerating to ${newSpeed}`));
compiler.hooks.calculateRoutes.tapAsync('calculateRoutes tapAsync', (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source} ${target} ${routesList}`);
callback();
}, 2000);
});
}
}

const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options);
compiler.run();

Plugin基础

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

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

1
2
3
4
5
6
7
8
9
class PluginDemo {
constructor() {}

apply(compiler) {
compiler.hooks.compilation.tap('PluginDemo', compilation => {});
}
}

module.exports = PluginDemo;

在使用这个plugin时,相关代码如下:

1
2
3
4
5
6
7
const PluginDemo = require('./PluginDemo');

module.exports = {
plugins: [
new PluginDemo(options);
]
}

Compiler和Compilation

在开发plugin时最常用的两个对象就是Compiler和Compilation,它们是Plugin和webpack之间的桥梁。

Compiler

Compiler对象包含了webpack环境所有的配置信息,包含options,loaders,plugins等,这个对象在webpack启动的时候被实例化,它在全局是唯一的。

Compilation

Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当webpack以开发模式运行时,每当检测到一个文件变化,一次新的Compilation将被创建。Compilation对象也提供了很多事件回调供插件做扩展。通过Compilation对象也可以读取到Compiler对象。

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

常用API

Writing a Plugin

揭开redux和react-redux的神秘面纱(转)

Posted on 2019-04-26

节流与防抖(转)

Posted on 2019-04-26

节流
防抖

debounce

有两种防抖模式:

  • 延迟执行
  • 立即执行

应用场景

  • 输入搜索 - 延迟执行
  • 点击按钮搜索 - 立即执行

输入搜索

输入框中实时键入文本搜索,适合延迟执行。用户在输入完整的搜索内容后,会停止一段时间,这时候发起搜索行为。

需求:

  1. 监听到用户开始输入后,并不马上执行,等一段时间后再执行
  2. 等待时间中,有新的输入事件,重置计时器
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
37
38
39
40
41
42
43
44
45
46
47
48
/**
* start--> A{计时器是否为空}
* A--> |是| B(添加计时器:过了 等待时间 后,执行函数并清除计时器)
* A--> |否| C(重置计时器)
* B--> 结束
* @param {Function} func
* @param {Integer} wait 毫秒
*/
function debounce(func, wait = 0) {
if (typeof func !== 'function') {
throw new TypeError('debounce传入的参数必须是函数');
}

let timer = null, curThis = null, curArgs = null;

function invokeFunc() {
func.apply(curThis, curArgs);
}

function clearTimer() {
clearTimeout(timer);
timer = null;
}

function addTimer() {
timer = setTimeout(() => {
invokeFunc();
clearTimer();
}, wait)
}

function resetTimer() {
clearTimer();
addTimer();
}

function debounced(...args) {
curThis = this;
curArgs = args;
if (timer) {
resetTimer();
} else {
addTimer();
}
}

return debounced;
}

点击搜索

用户点击搜索按钮,立即执行搜索,并启动定时器,一段时间后清除定时器。一段时间内多次点击,只执行一次,忽略后面的行为,并重置定时器。

需求

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
37
38
39
40
41
42
43
44
45
/**
* start--> IF1{计时器是否为空}
* IF1--> |是| A(执行搜索)
* A--> B(添加定时器:过了 等待时间 后清除定时器)
* IF1--> |否| C(清除定时器)
* C--> B
* B--> 结束
*/
function debounce(func, wait = 0) {
if (typeof func !== 'function') {
throw new TypeError('debounce传入的参数必须是函数');
}

let timer = null, curThis = null, curArgs = null;

function addTimer() {
timer = setTimeout(() => {
clearTimer();
}, wait)
}

function clearTimer() {
clearTimeout(timer);
timer = null;
}

function invokeFunc() {
func.apply(curThis, curArgs);
}

function debounced(...args) {
curThis = this;
curArgs = args;

if (timer) {
clearTimer();
} else {
invokeFunc();
}

addTimer();
}

return debounced;
}

节流

频繁触发的事件,事件处理函数在一定的时间间隔内只执行一次。

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
/**
* start--> IF1{timer is null?}
* IF1--> |Yes| A(invoke callback)
* A--> B(add timer, remove timer after wait ms)
* B--> end
* IF1---> |No| end
* @param {Function} func
* @param {Integer} wait ms
*/
function throttle(func, wait) {
let timer = null, curThis = null, curArgs = null;

function invokeFunc() {
func.apply(curThis, curArgs);
}

function addTimer() {
timer = setTimout(() => {
clearTimeout(timer);
timer = null;
}, wait);
}

function throttled(...args) {
curThis = this;
curArgs = args;
if (timer) {
invokeFunc();
addTimer();
}
}

return throttled;
}

总结

节流和防抖类似,都能有效优化系统性能,不过使用业务场景有所区别:

  • 防抖既可用于在多次触发的事件(如文本框输入),也可用于频繁触发的事件(如滚动条)
  • 节流只用在频繁触发的事件上

Node.js 12发布!快来看看有哪些新动向吧(转)

Posted on 2019-04-26

原文链接
官方博客

V8升级到7.4

Hello TLS 1.3

更合适到默认堆内存限制

使用llhttp替换了默认堆http解析器

更容易的Native模块——持续进行中

Worker Threads

诊断报告

Heap Dumps

启动时间优化

ES6 Module支持

新的编译器和最小化平台

手把手教你写一个Webpack Loader(转)

Posted on 2019-04-26

原文链接
官方参考writing-a-loader

首先我们来看一下为什么需要loader,以及它能干什么?
webpack 只能理解 JavaScript 和 JSON 文件。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。

本质上来说,loader 就是一个 node 模块,这很符合 webpack 中「万物皆模块」的思路。既然是 node 模块,那就一定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块resource的时候调用该函数。在这个函数内部,我们可以通过传入 this 上下文给 Loader API 来使用它们。最终装换成可以直接引用的模块。

xml-loader实现

前面我们已经知道,由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
一个简单的loader源码如下

1
2
3
4
5
module.exports = function(source) {
// source 为 compiler 传递给 `Loader` 的一个文件的原内容
// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 `Loader` 没有做任何转换
return source;
};

由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:

1
2
3
4
5
6
7
8
9
10
const xml2js = require('xml2js');
const parser = new xml2js.Parser();

module.exports = function(source) {
this.cacheable && this.cacheable();
const self = this;
parser.parseString(source, function (err, result) {
self.callback(err, !err && "module.exports = " + JSON.stringify(result));
});
};

这里我们事简单实现一个xml-loader;

注意:如果是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的 require,换句话说,它一定是一段可执行的 JS 脚本 (用字符串来存储),更准确来说,是一个 node 模块的 JS 脚本,所以我们需要用module.exports =导出。
整个过程相当于这个 loader 把源文件

// 这里是 source 模块

转化为

1
2
// example.js
module.exports = '这里是 source 模块';

然后交给 require 调用方:

1
2
3
// applySomeModule.js
var source = require('example.js');
console.log(source); // 这里是 source 模块

写完后我们要怎么在本地验证呢?下面我们来写个简单的demo进行验证。

验证

首先我们创建一个根目录xml-loader,此目录下 npm init -y生成默认的package.json文件 ,在文件中配置打包命令

1
2
3
"scripts": {
"dev": "webpack-dev-server"
},

之后npm i -D webpack webpack-cli,安装完webpack,在根目录 创建配置文件webpack.config.js

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
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.xml$/,
use: ['xml-loader'],
}
]
},
resolveLoader: {
modules: [path.join(__dirname, '/src/loader')]
},
devServer: {
contentBase: './dist',
overlay: {
warnings: true,
errors: true
},
open: true
}
}

在根目录创建一个src目录,里面创建index.js,

1
2
3
4
5
6
7
8
9
10
11
import data from './foo.xml';

function component() {
var element = document.createElement('div');
element.innerHTML = data.note.body;
element.classList.add('header');
console.log(data);
return element;
}

document.body.appendChild(component());

同时还有一个foo.xml文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder dd</heading>
<body>Call Cindy on Tuesday dd</body>
</note>

最后把上面的xml-loader放到src/loader文件夹下。
完整的demo源码请看
最终我们的运行效果如下图

图片描述

至此一个简单的webpack loader就实现完成了。当然最终使用你可以发布到npm上。

一些议论知识补充

获得Loader的options

当我们配置loader时我们经常会看到有这样的配置

1
2
3
4
5
6
7
8
9
ules: [{
test: /\.html$/,
use: [ {
loader: 'html-loader',
options: {
minimize: true
}
}],
}]

那么我们在loader中怎么获取这写配置信息呢?答案是loader-utils。这个由webpack提供的工具。下面我们来看下使用方法

1
2
3
4
5
6
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};

没错就是这么简单。

加载本地Loader

path.resolve

可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件

1
2
3
4
5
6
7
8
9
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}

ResolveLoader

这个就是上面我用到的方法。ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。
假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

1
2
3
4
5
6
module.exports = {
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

npm link

npm link 专门用于开发和调试本地 npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 npm 模块代码,在项目中也能使用到编辑后的代码。

完成 npm link 的步骤如下:

  • 确保正在开发的本地 npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  • 在本地 npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的package.json 文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 npm 模块一样使用本地的 Loader 了。(npm link不是很熟,复制被人的)

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。

如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:

1
2
3
4
5
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};

处理二进制数据

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:

1
2
3
4
5
6
7
8
9
module.exports = function(source) {
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

在转换步骤是异步时,你可以这样:

1
2
3
4
5
6
7
8
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};

2019前端开发手册(转)

Posted on 2019-04-25

原文链接

1234…6

沙雕爹爹

Personal tech blog.

55 posts
45 tags
© 2019 沙雕爹爹
Powered by Hexo
|
Theme — NexT.Muse v5.1.4